#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); } /// 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; } protected override void OnCreate() { RequireForUpdate(); } protected override void OnUpdate() { if (!Active) return; bool fire = FireFrames > 0; if (FireFrames > 0) FireFrames--; 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(); } } } } #endif