2019-01-24 16:43:03 +08:00
// 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.
2018-04-13 17:19:50 +08:00
2019-03-20 10:34:06 +08:00
using System ;
2018-04-13 17:19:50 +08:00
using System.Collections.Generic ;
using System.Linq ;
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-11-07 16:24:05 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Input.Bindings ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Lists ;
2020-04-03 17:25:01 +08:00
using osu.Framework.Threading ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
2018-11-06 11:01:54 +08:00
using osu.Game.Configuration ;
2020-04-03 17:25:01 +08:00
using osu.Game.Extensions ;
2018-11-07 16:24:05 +08:00
using osu.Game.Input.Bindings ;
2019-04-08 17:32:05 +08:00
using osu.Game.Rulesets.Mods ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Timing ;
2018-11-06 11:01:54 +08:00
using osu.Game.Rulesets.UI.Scrolling.Algorithms ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.UI.Scrolling
{
/// <summary>
2019-03-20 10:29:16 +08:00
/// A type of <see cref="DrawableRuleset{TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
/// <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/> will scroll within the playfield.
2018-04-13 17:19:50 +08:00
/// </summary>
2019-03-20 10:29:16 +08:00
public abstract class DrawableScrollingRuleset < TObject > : DrawableRuleset < TObject > , IKeyBindingHandler < GlobalAction >
2018-04-13 17:19:50 +08:00
where TObject : HitObject
{
2018-11-07 16:24:05 +08:00
/// <summary>
/// The default span of time visible by the length of the scrolling axes.
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
/// </summary>
private const double time_span_default = 1500 ;
/// <summary>
/// The minimum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_min = 50 ;
/// <summary>
/// The maximum span of time that may be visible by the length of the scrolling axes.
/// </summary>
private const double time_span_max = 10000 ;
/// <summary>
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
/// </summary>
private const double time_span_step = 200 ;
2018-11-06 14:46:36 +08:00
protected readonly Bindable < ScrollingDirection > Direction = new Bindable < ScrollingDirection > ( ) ;
2018-11-07 16:24:05 +08:00
/// <summary>
/// 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 <see cref="TimeRange"/> = 1000.
/// </summary>
protected readonly BindableDouble TimeRange = new BindableDouble ( time_span_default )
{
Default = time_span_default ,
MinValue = time_span_min ,
MaxValue = time_span_max
} ;
2018-11-12 16:36:19 +08:00
protected virtual ScrollVisualisationMethod VisualisationMethod = > ScrollVisualisationMethod . Sequential ;
2018-11-06 14:46:36 +08:00
2018-11-07 16:24:05 +08:00
/// <summary>
2019-04-25 16:36:17 +08:00
/// Whether the player can change <see cref="TimeRange"/>.
2018-11-07 16:24:05 +08:00
/// </summary>
protected virtual bool UserScrollSpeedAdjustment = > true ;
2019-08-26 11:51:13 +08:00
/// <summary>
/// Whether <see cref="TimingControlPoint"/> beat lengths should scale relative to the most common beat length in the <see cref="Beatmap"/>.
/// </summary>
protected virtual bool RelativeScaleBeatLengths = > false ;
2018-04-13 17:19:50 +08:00
/// <summary>
2020-04-03 12:16:01 +08:00
/// The <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-04-03 12:16:01 +08:00
protected readonly SortedList < MultiplierControlPoint > ControlPoints = new SortedList < MultiplierControlPoint > ( Comparer < MultiplierControlPoint > . Default ) ;
2018-11-06 11:01:54 +08:00
2018-11-07 15:51:28 +08:00
protected IScrollingInfo ScrollingInfo = > scrollingInfo ;
2018-11-06 11:01:54 +08:00
2018-11-07 15:51:28 +08:00
[Cached(Type = typeof(IScrollingInfo))]
private readonly LocalScrollingInfo scrollingInfo ;
2018-04-13 17:19:50 +08:00
2019-12-12 14:58:11 +08:00
protected DrawableScrollingRuleset ( Ruleset ruleset , IBeatmap beatmap , IReadOnlyList < Mod > mods = null )
2019-04-08 17:32:05 +08:00
: base ( ruleset , beatmap , mods )
2018-04-13 17:19:50 +08:00
{
2018-11-07 15:51:28 +08:00
scrollingInfo = new LocalScrollingInfo ( ) ;
scrollingInfo . Direction . BindTo ( Direction ) ;
2018-11-07 16:24:05 +08:00
scrollingInfo . TimeRange . BindTo ( TimeRange ) ;
2018-11-07 15:51:28 +08:00
2018-11-12 16:36:19 +08:00
switch ( VisualisationMethod )
2018-11-06 11:01:54 +08:00
{
2018-11-12 16:36:19 +08:00
case ScrollVisualisationMethod . Sequential :
2020-04-03 12:16:01 +08:00
scrollingInfo . Algorithm = new SequentialScrollAlgorithm ( ControlPoints ) ;
2018-11-06 11:01:54 +08:00
break ;
2019-04-01 11:44:46 +08:00
2018-11-12 16:36:19 +08:00
case ScrollVisualisationMethod . Overlapping :
2020-04-03 12:16:01 +08:00
scrollingInfo . Algorithm = new OverlappingScrollAlgorithm ( ControlPoints ) ;
2018-11-06 11:01:54 +08:00
break ;
2019-04-01 11:44:46 +08:00
2018-11-12 16:36:19 +08:00
case ScrollVisualisationMethod . Constant :
2018-11-07 15:51:28 +08:00
scrollingInfo . Algorithm = new ConstantScrollAlgorithm ( ) ;
2018-11-06 11:01:54 +08:00
break ;
}
2018-04-13 17:19:50 +08:00
}
[BackgroundDependencyLoader]
private void load ( )
{
2019-11-25 18:01:24 +08:00
double lastObjectTime = Objects . LastOrDefault ( ) ? . GetEndTime ( ) ? ? double . MaxValue ;
2019-08-28 19:22:16 +08:00
double baseBeatLength = TimingControlPoint . DEFAULT_BEAT_LENGTH ;
2019-08-26 11:51:13 +08:00
if ( RelativeScaleBeatLengths )
{
IReadOnlyList < TimingControlPoint > timingPoints = Beatmap . ControlPointInfo . TimingPoints ;
double maxDuration = 0 ;
for ( int i = 0 ; i < timingPoints . Count ; i + + )
{
2019-08-26 15:25:23 +08:00
if ( timingPoints [ i ] . Time > lastObjectTime )
break ;
2019-08-26 11:51:13 +08:00
double endTime = i < timingPoints . Count - 1 ? timingPoints [ i + 1 ] . Time : lastObjectTime ;
double duration = endTime - timingPoints [ i ] . Time ;
if ( duration > maxDuration )
{
maxDuration = duration ;
2019-09-25 19:12:01 +08:00
// 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
2019-09-24 15:49:42 +08:00
baseBeatLength = timingPoints [ i ] . BeatLength / Beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier ;
2019-08-26 11:51:13 +08:00
}
}
}
// Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point
2018-04-13 17:19:50 +08:00
var lastTimingPoint = new TimingControlPoint ( ) ;
var lastDifficultyPoint = new DifficultyControlPoint ( ) ;
var allPoints = new SortedList < ControlPoint > ( Comparer < ControlPoint > . Default ) ;
allPoints . AddRange ( Beatmap . ControlPointInfo . TimingPoints ) ;
allPoints . AddRange ( Beatmap . ControlPointInfo . DifficultyPoints ) ;
2019-08-26 11:51:13 +08:00
// Generate the timing points, making non-timing changes use the previous timing change and vice-versa
2018-04-13 17:19:50 +08:00
var timingChanges = allPoints . Select ( c = >
{
2019-11-12 18:16:51 +08:00
if ( c is TimingControlPoint timingPoint )
2018-04-13 17:19:50 +08:00
lastTimingPoint = timingPoint ;
2019-11-12 18:16:51 +08:00
else if ( c is DifficultyControlPoint difficultyPoint )
2018-04-13 17:19:50 +08:00
lastDifficultyPoint = difficultyPoint ;
return new MultiplierControlPoint ( c . Time )
{
2018-10-01 17:12:26 +08:00
Velocity = Beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier ,
2019-08-26 11:51:13 +08:00
BaseBeatLength = baseBeatLength ,
2018-04-13 17:19:50 +08:00
TimingPoint = lastTimingPoint ,
DifficultyPoint = lastDifficultyPoint
} ;
} ) ;
2019-08-26 11:51:13 +08:00
// Trim unwanted sequences of timing changes
2018-04-13 17:19:50 +08:00
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 ) ;
2020-04-03 12:16:01 +08:00
ControlPoints . AddRange ( timingChanges ) ;
2018-04-13 17:19:50 +08:00
2020-04-03 12:16:01 +08:00
if ( ControlPoints . Count = = 0 )
ControlPoints . Add ( new MultiplierControlPoint { Velocity = Beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier } ) ;
}
protected override void LoadComplete ( )
{
2020-04-03 17:25:01 +08:00
base . LoadComplete ( ) ;
if ( ! ( Playfield is ScrollingPlayfield ) )
throw new ArgumentException ( $"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset<TObject>)}." ) ;
}
/// <summary>
2020-04-03 17:32:07 +08:00
/// Adjusts the scroll speed of <see cref="HitObject"/>s.
2020-04-03 17:25:01 +08:00
/// </summary>
/// <param name="amount">The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased.</param>
protected virtual void AdjustScrollSpeed ( int amount ) = > this . TransformBindableTo ( TimeRange , TimeRange . Value - amount * time_span_step , 200 , Easing . OutQuint ) ;
2018-11-07 16:24:05 +08:00
public bool OnPressed ( GlobalAction action )
{
if ( ! UserScrollSpeedAdjustment )
return false ;
switch ( action )
{
case GlobalAction . IncreaseScrollSpeed :
2020-04-03 17:25:01 +08:00
scheduleScrollSpeedAdjustment ( 1 ) ;
2018-11-07 16:24:05 +08:00
return true ;
2019-04-01 11:44:46 +08:00
2018-11-07 16:24:05 +08:00
case GlobalAction . DecreaseScrollSpeed :
2020-04-03 17:25:01 +08:00
scheduleScrollSpeedAdjustment ( - 1 ) ;
2018-11-07 16:24:05 +08:00
return true ;
}
return false ;
}
2020-04-03 17:25:01 +08:00
private ScheduledDelegate scheduledScrollSpeedAdjustment ;
2019-03-20 10:34:06 +08:00
2020-04-03 17:25:01 +08:00
public void OnReleased ( GlobalAction action )
{
scheduledScrollSpeedAdjustment ? . Cancel ( ) ;
scheduledScrollSpeedAdjustment = null ;
2019-03-20 10:34:06 +08:00
}
2020-04-03 17:25:01 +08:00
private void scheduleScrollSpeedAdjustment ( int amount )
2020-01-22 12:22:34 +08:00
{
2020-04-03 17:25:01 +08:00
scheduledScrollSpeedAdjustment ? . Cancel ( ) ;
scheduledScrollSpeedAdjustment = this . BeginKeyRepeat ( Scheduler , ( ) = > AdjustScrollSpeed ( amount ) ) ;
2020-01-22 12:22:34 +08:00
}
2018-11-07 16:24:05 +08:00
2018-11-07 15:43:34 +08:00
private class LocalScrollingInfo : IScrollingInfo
2018-11-06 14:46:36 +08:00
{
public IBindable < ScrollingDirection > Direction { get ; } = new Bindable < ScrollingDirection > ( ) ;
2018-11-07 15:51:28 +08:00
2018-11-07 16:24:05 +08:00
public IBindable < double > TimeRange { get ; } = new BindableDouble ( ) ;
2018-11-07 15:51:28 +08:00
public IScrollAlgorithm Algorithm { get ; set ; }
2018-11-06 14:46:36 +08:00
}
2018-04-13 17:19:50 +08:00
}
}