// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.States; using osu.Framework.Timing; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; using osuTK.Input; using static osu.Game.Input.Handlers.ReplayInputHandler; using JoystickState = osu.Framework.Input.States.JoystickState; using KeyboardState = osu.Framework.Input.States.KeyboardState; using MouseState = osu.Framework.Input.States.MouseState; namespace osu.Game.Rulesets.UI { public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler where T : struct { protected override InputState CreateInitialState() { var state = base.CreateInitialState(); return new RulesetInputManagerInputState(state.Mouse, state.Keyboard, state.Joystick); } protected readonly KeyBindingContainer KeyBindingContainer; protected override Container Content => KeyBindingContainer; protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique); } #region Action mapping (for replays) public override void HandleInputStateChange(InputStateChangeEvent inputStateChange) { if (inputStateChange is ReplayStateChangeEvent replayStateChanged) { foreach (var action in replayStateChanged.ReleasedActions) KeyBindingContainer.TriggerReleased(action); foreach (var action in replayStateChanged.PressedActions) KeyBindingContainer.TriggerPressed(action); } else { base.HandleInputStateChange(inputStateChange); } } #endregion #region IHasReplayHandler private ReplayInputHandler replayInputHandler; public ReplayInputHandler ReplayInputHandler { get => replayInputHandler; set { if (replayInputHandler != null) RemoveHandler(replayInputHandler); replayInputHandler = value; UseParentInput = replayInputHandler == null; if (replayInputHandler != null) AddHandler(replayInputHandler); } } #endregion #region Clock control private ManualClock clock; private IFrameBasedClock parentClock; protected override void LoadComplete() { base.LoadComplete(); //our clock will now be our parent's clock, but we want to replace this to allow manual control. parentClock = Clock; ProcessCustomClock = false; Clock = new FramedClock(clock = new ManualClock { CurrentTime = parentClock.CurrentTime, Rate = parentClock.Rate, }); } /// /// Whether we are running up-to-date with our parent clock. /// If not, we will need to keep processing children until we catch up. /// private bool requireMoreUpdateLoops; /// /// Whether we are in a valid state (ie. should we keep processing children frames). /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. /// private bool validState; protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; private bool isAttached => replayInputHandler != null && !UseParentInput; private const int max_catch_up_updates_per_frame = 50; private const double sixty_frame_time = 1000.0 / 60; public override bool UpdateSubTree() { requireMoreUpdateLoops = true; validState = true; int loops = 0; while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame) { updateClock(); if (validState) { base.UpdateSubTree(); UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); } } return true; } private void updateClock() { if (parentClock == null) return; clock.Rate = parentClock.Rate; clock.IsRunning = parentClock.IsRunning; var newProposedTime = parentClock.CurrentTime; try { if (Math.Abs(clock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) { newProposedTime = clock.Rate > 0 ? Math.Min(newProposedTime, clock.CurrentTime + sixty_frame_time) : Math.Max(newProposedTime, clock.CurrentTime - sixty_frame_time); } if (!isAttached) { clock.CurrentTime = newProposedTime; } else { double? newTime = replayInputHandler.SetFrameFromTime(newProposedTime); if (newTime == null) { // we shouldn't execute for this time value. probably waiting on more replay data. validState = false; requireMoreUpdateLoops = true; clock.CurrentTime = newProposedTime; return; } clock.CurrentTime = newTime.Value; } requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime; } finally { // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed Clock.ProcessFrame(); } } #endregion #region Setting application (disables etc.) private Bindable mouseDisabled; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); } protected override bool Handle(UIEvent e) { switch (e) { case MouseDownEvent mouseDown when mouseDown.Button == MouseButton.Left || mouseDown.Button == MouseButton.Right: if (mouseDisabled.Value) return false; break; case MouseUpEvent mouseUp: if (!CurrentState.Mouse.IsPressed(mouseUp.Button)) return false; break; } return base.Handle(e); } #endregion #region Key Counter Attachment public void Attach(KeyCounterCollection keyCounter) { var receptor = new ActionReceptor(keyCounter); Add(receptor); keyCounter.SetReceptor(receptor); keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); } public class ActionReceptor : KeyCounterCollection.Receptor, IKeyBindingHandler { public ActionReceptor(KeyCounterCollection target) : base(target) { } public bool OnPressed(T action) => Target.Children.OfType>().Any(c => c.OnPressed(action)); public bool OnReleased(T action) => Target.Children.OfType>().Any(c => c.OnReleased(action)); } #endregion protected virtual RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new RulesetKeyBindingContainer(ruleset, variant, unique); public class RulesetKeyBindingContainer : DatabasedKeyBindingContainer { public RulesetKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) { } } } /// /// Expose the in a capable . /// public interface IHasReplayHandler { ReplayInputHandler ReplayInputHandler { get; set; } } /// /// Supports attaching a . /// Keys will be populated automatically and a receptor will be injected inside. /// public interface ICanAttachKeyCounter { void Attach(KeyCounterCollection keyCounter); } public class RulesetInputManagerInputState : InputState where T : struct { public ReplayState LastReplayState; public RulesetInputManagerInputState(MouseState mouse = null, KeyboardState keyboard = null, JoystickState joystick = null) : base(mouse, keyboard, joystick) { } } }