// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { [Cached] public partial class Column : ScrollingPlayfield, IKeyBindingHandler { public const float COLUMN_WIDTH = 80; public const float SPECIAL_COLUMN_WIDTH = 70; /// /// The index of this column as part of the whole playfield. /// public readonly int Index; public readonly Bindable Action = new Bindable(); public readonly ColumnHitObjectArea HitObjectArea; internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; private DrawablePool hitExplosionPool; private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; private GameplaySampleTriggerSource sampleTriggerSource; /// /// Whether this is a special (ie. scratch) column. /// public readonly bool IsSpecial; public readonly Bindable AccentColour = new Bindable(Color4.Black); public Column(int index, bool isSpecial) { Index = index; IsSpecial = isSpecial; RelativeSizeAxes = Axes.Y; Width = COLUMN_WIDTH; hitPolicy = new OrderedHitPolicy(HitObjectContainer); HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; } [Resolved] private ISkinSource skin { get; set; } [BackgroundDependencyLoader] private void load(GameHost host) { SkinnableDrawable keyArea; skin.SourceChanged += onSourceChanged; onSourceChanged(); Drawable background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both, }; InternalChildren = new[] { hitExplosionPool = new DrawablePool(5), sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), HitObjectArea, keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both, }, background, TopLevelContainer, new ColumnTouchInputArea(this) }; applyGameWideClock(background); applyGameWideClock(keyArea); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(50, 250); // Some elements don't handle rewind correctly and fixing them is non-trivial. // In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide // clock so they don't need to worry about rewind. // This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding. // // This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind. void applyGameWideClock(Drawable drawable) { drawable.Clock = host.UpdateThread.Clock; drawable.ProcessCustomClock = false; } } private void onSourceChanged() { AccentColour.Value = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black; } protected override void LoadComplete() { base.LoadComplete(); NewResult += OnNewResult; } protected override void Dispose(bool isDisposing) { // must happen before children are disposed in base call to prevent illegal accesses to the hit explosion pool. NewResult -= OnNewResult; base.Dispose(isDisposing); if (skin != null) skin.SourceChanged -= onSourceChanged; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs>(Action); return dependencies; } protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) { base.OnNewDrawableHitObject(drawableHitObject); DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject; maniaObject.AccentColour.BindTo(AccentColour); maniaObject.CheckHittable = hitPolicy.IsHittable; } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (result.IsHit) hitPolicy.HandleHit(judgedObject); if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); } public bool OnPressed(KeyBindingPressEvent e) { if (e.Action != Action.Value) return false; sampleTriggerSource.Play(); return true; } public void OnReleased(KeyBindingReleaseEvent e) { } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); public partial class ColumnTouchInputArea : Drawable { private readonly Column column; [Resolved(canBeNull: true)] private ManiaInputManager maniaInputManager { get; set; } private KeyBindingContainer keyBindingContainer; public ColumnTouchInputArea(Column column) { RelativeSizeAxes = Axes.Both; this.column = column; } protected override void LoadComplete() { keyBindingContainer = maniaInputManager?.KeyBindingContainer; } protected override bool OnTouchDown(TouchDownEvent e) { keyBindingContainer?.TriggerPressed(column.Action.Value); return true; } protected override void OnTouchUp(TouchUpEvent e) { keyBindingContainer?.TriggerReleased(column.Action.Value); } } } }