#if UNITY_EDITOR using ProjectM.Simulation; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Client { /// /// EDITOR-ONLY validation hook for driving the local player's without a /// real input device or a focused Game view. The Unity Input System ignores injected/real device /// input while the Game view is unfocused, which makes headless (MCP execute_code) or /// automated fire/move validation impossible through alone. /// /// This system runs in immediately AFTER the real gather and, /// when is set, overwrites the locally-owned player's input from static fields /// you can poke from a debugger / execute_code / an editor button. Because it writes the same /// the gather does, it drives the authentic command → prediction → /// AbilityFireSystem pipeline (not a shortcut), so it validates the real fire/move/auto-target path. /// /// /// Entirely wrapped in #if UNITY_EDITOR: it does not exist in player builds. Pair with /// Application.runInBackground = true so the unfocused editor keeps ticking. Usage from /// execute_code: ProjectM.Client.DebugInputInjectionSystem.Fire(); (one shot), /// ...SetMove(0f, 1f); (hold a move heading), ...SetAim(1f, 0f);, ...Stop();. /// /// [UpdateInGroup(typeof(GhostInputSystemGroup))] [UpdateAfter(typeof(PlayerInputGatherSystem))] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] public partial class DebugInputInjectionSystem : SystemBase { /// While true, this system overrides the local player's gathered input each frame. public static bool Active; /// Movement heading applied to PlayerInput.Move while . public static float2 Move; /// Aim vector applied to PlayerInput.Aim while (zero = face movement). public static float2 Aim; /// Frames remaining to hold the Fire event. Each held frame raises Fire.Set(); holding /// across several frames spans multiple network ticks so the one-shot event reliably reaches the /// command buffer (a single-frame pulse can be lost across the frame→tick boundary). 0 = idle. public static int FireFrames; /// Input scheme stamped onto injected input (). Defaults to Gamepad /// so injected aim still receives the server auto-target assist (the pre-scheme behaviour), keeping the /// editor fire / auto-target validation path intact. public static byte Scheme = InputSchemeId.Gamepad; /// Convenience: hold Fire for the next frames (also enables /// override). The ability cooldown still gates how many shots actually result. public static void Fire(int frames = 10) { Active = true; FireFrames = math.max(FireFrames, frames); } /// Frames remaining to hold the melee Attack event (07-19: melee swing validation). public static int AttackFrames; /// Convenience: hold the melee Attack for the next frames (also enables override). public static void Attack(int frames = 10) { Active = true; AttackFrames = math.max(AttackFrames, frames); } /// Per-socket fire holds (LANTERN 4-socket kit). Each held frame raises that socket's fire event. public static readonly int[] SocketFrames = new int[SocketId.Count]; /// Hold socket 's fire for the next frames (also enables override). public static void FireSocket(int i, int frames = 10) { Active = true; if ((uint)i < (uint)SocketId.Count) SocketFrames[i] = math.max(SocketFrames[i], frames); } /// Convenience: hold a planar move heading (also enables override). public static void SetMove(float x, float z) { Active = true; Move = new float2(x, z); } /// Convenience: hold an aim direction (also enables override). public static void SetAim(float x, float z) { Active = true; Aim = new float2(x, z); } /// Convenience: stop overriding and clear all injected input. public static void Stop() { Active = false; Move = default; Aim = default; FireFrames = 0; AttackFrames = 0; for (int i = 0; i < SocketId.Count; i++) SocketFrames[i] = 0; } protected override void OnCreate() { RequireForUpdate(); } protected override void OnUpdate() { if (!Active) return; bool fire = FireFrames > 0; if (FireFrames > 0) FireFrames--; bool attack = AttackFrames > 0; if (AttackFrames > 0) AttackFrames--; // LANTERN 4-socket kit: per-socket fire holds, decremented once per frame. bool s0 = SocketFrames[0] > 0; if (SocketFrames[0] > 0) SocketFrames[0]--; bool s1 = SocketFrames[1] > 0; if (SocketFrames[1] > 0) SocketFrames[1]--; bool s2 = SocketFrames[2] > 0; if (SocketFrames[2] > 0) SocketFrames[2]--; bool s3 = SocketFrames[3] > 0; if (SocketFrames[3] > 0) SocketFrames[3]--; float2 move = Move; float2 aim = Aim; // Keep the presentation bridge in sync with the injected scheme so the reticle/cursor match the // input the sim is actually using (the gather published a value earlier this frame; we override it). AimPresentation.Scheme = Scheme; foreach (var input in SystemAPI.Query>().WithAll()) { input.ValueRW.Move = move; input.ValueRW.Aim = aim; input.ValueRW.Scheme = Scheme; if (fire) input.ValueRW.Fire.Set(); if (attack) input.ValueRW.Attack.Set(); if (s0) input.ValueRW.Socket0.Set(); if (s1) input.ValueRW.Socket1.Set(); if (s2) input.ValueRW.Socket2.Set(); if (s3) input.ValueRW.Socket3.Set(); } } } } #endif