using System; using System.Collections.Generic; using System.Linq; using OpenTK.Input; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.MathUtils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.UI { public class ScrollingPlayfield : Playfield where TObject : HitObject where TJudgement : Judgement { private const double time_span_default = 1500; private const double time_span_min = 50; private const double time_span_max = 10000; private const double time_span_step = 50; /// /// Gets or sets the range of time that is visible by the length of this playfield the scrolling axis direction. /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. /// private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default) { Default = time_span_default, MinValue = time_span_min, MaxValue = time_span_max }; public BindableDouble VisibleTimeRange { get { return visibleTimeRange; } set { visibleTimeRange.BindTo(value); } } public new readonly ScrollingHitObjectContainer HitObjects; protected ScrollingPlayfield(Axes scrollingAxes, float? customWidth = null) : base(customWidth) { base.HitObjects = HitObjects = new ScrollingHitObjectContainer(scrollingAxes) { RelativeSizeAxes = Axes.Both, VisibleTimeRange = VisibleTimeRange }; } private List> nestedPlayfields; public IEnumerable> NestedPlayfields => nestedPlayfields; protected void AddNested(ScrollingPlayfield otherPlayfield) { if (nestedPlayfields == null) nestedPlayfields = new List>(); nestedPlayfields.Add(otherPlayfield); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (state.Keyboard.ControlPressed) { switch (args.Key) { case Key.Minus: transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint); break; case Key.Plus: transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint); break; } } return false; } private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None) { this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing)); } private class TransformVisibleTimeRange : Transform> { private double valueAt(double time) { if (time < StartTime) return StartValue; if (time >= EndTime) return EndValue; return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); } public override string TargetMember => "VisibleTimeRange.Value"; protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time); protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value; } /// /// A collection of s. /// /// /// This container redirects any 's added to it to the /// which provides the speed adjustment active at the start time of the hit object. Furthermore, this container provides the /// necessary for the contained s. /// /// public class ScrollingHitObjectContainer : HitObjectContainer> { private readonly BindableDouble visibleTimeRange = new BindableDouble { Default = 1000 }; /// /// Gets or sets the range of time that is visible by the length of this container. /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. /// public Bindable VisibleTimeRange { get { return visibleTimeRange; } set { visibleTimeRange.BindTo(value); } } protected override Container> Content => content; /// /// The following is never used - it only exists for the purpose of being able to use AddInternal below. /// private Container> content; /// /// Hit objects that are to be re-processed on the next update. /// private readonly Queue> queuedHitObjects = new Queue>(); private readonly Axes scrollingAxes; /// /// Creates a new . /// /// The axes upon which hit objects should appear to scroll inside this container. public ScrollingHitObjectContainer(Axes scrollingAxes) { this.scrollingAxes = scrollingAxes; content = new Container>(); } public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment) { speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange); speedAdjustment.ScrollingAxes = scrollingAxes; AddInternal(speedAdjustment); } /// /// Adds a hit object to this . The hit objects will be kept in a queue /// and will be processed when new s are added to this . /// /// The hit object to add. public override void Add(DrawableHitObject hitObject) { if (!(hitObject is IScrollingHitObject)) throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}."); queuedHitObjects.Enqueue(hitObject); } protected override void Update() { base.Update(); // Todo: At the moment this is going to re-process every single Update, however this will only be a null-op // when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now. // An external count is kept because hit objects that can't be added are re-queued int count = queuedHitObjects.Count; while (count-- > 0) { var hitObject = queuedHitObjects.Dequeue(); var target = adjustmentContainerFor(hitObject); if (target == null) { // We can't add this hit object to a speed adjustment container yet, so re-queue it // for re-processing when the layout next invalidated queuedHitObjects.Enqueue(hitObject); continue; } if (hitObject.RelativePositionAxes != target.ScrollingAxes) throw new InvalidOperationException($"Make sure to set all {nameof(DrawableHitObject)}'s {nameof(RelativePositionAxes)} are equal to the correct axes of scrolling ({target.ScrollingAxes})."); target.Add(hitObject); } } /// /// Finds the which provides the speed adjustment active at the start time /// of a hit object. If there is no active at the start time of the hit object, /// then the first (time-wise) speed adjustment is returned. /// /// The hit object to find the active for. /// The active at 's start time. Null if there are no speed adjustments. private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => InternalChildren.OfType().FirstOrDefault(c => c.CanContain(hitObject)) ?? InternalChildren.OfType().LastOrDefault(); /// /// Finds the which provides the speed adjustment active at a time. /// If there is no active at the time, then the first (time-wise) speed adjustment is returned. /// /// The time to find the active at. /// The active at . Null if there are no speed adjustments. private SpeedAdjustmentContainer adjustmentContainerAt(double time) => InternalChildren.OfType().FirstOrDefault(c => c.CanContain(time)) ?? InternalChildren.OfType().LastOrDefault(); } } }