Virtual Inputs
Anomaly.Client.Api.Input.VirtualInputs lets mods inject input into the game’s UnityEngine.Input pipeline — without each mod re-patching Input.GetKey / Input.GetAxis / etc. by hand. Use it for controller support, accessibility modes, gesture systems, and custom input devices.
The framework installs eight Harmony postfixes covering Input.GetKey/GetKeyDown/GetKeyUp/GetMouseButton/GetMouseButtonDown/GetMouseButtonUp/GetAxis/GetAxisRaw. All are postfixes, so vanilla input is preserved when present and your provider only adds to it.
VirtualInputs is for injecting into existing engine input (driving WASD from a stick, driving Mouse0 from a gesture). For named user-rebindable bindings with bindings.json overrides, use InputRegistry. They serve different purposes; mods often use both.
Provider types
Section titled “Provider types”using Anomaly.Client.Api.Input;using UnityEngine;
// Button — provider returns current pressed state. OR'd into Input.GetKey(KeyCode).var fireHandle = VirtualInputs.RegisterButton( id: "mymod.fire", key: KeyCode.Mouse0, provider: () => Gamepad.RightTrigger > 0.5f);
// Mouse button — same shape for Input.GetMouseButton(int). 0 = left, 1 = right, 2 = middle.var aimHandle = VirtualInputs.RegisterMouseButton( id: "mymod.aim", button: 1, provider: () => Gamepad.LeftTrigger > 0.5f);
// Axis — provider returns current axis value. Summed with Input.GetAxis() and clamped to [-1, 1].var horizontalHandle = VirtualInputs.RegisterAxis( id: "mymod.horizontal", axisName: "Horizontal", provider: () => Gamepad.LeftStick.x);API:
public static IDisposable RegisterButton(string id, KeyCode key, Func<bool> provider);public static IDisposable RegisterMouseButton(string id, int button, Func<bool> provider);public static IDisposable RegisterAxis(string id, string axisName, Func<float> provider);public static bool Unregister(string id);public static int UnregisterAllForMod(string modId);Each Register* call returns an IDisposable handle. Retain and dispose the handle when the feature disables; the runtime also removes providers at session boundaries. Unregister(id) removes one by id; UnregisterAllForMod(modId) removes everything whose id starts with modId..
Down / Up transitions
Section titled “Down / Up transitions”Providers return current state — they don’t track “newly pressed” or “newly released.” The framework polls button and mouse-button providers once per client update and infers KeyDown / KeyUp / MouseButtonDown / MouseButtonUp from the previous-frame state. Axis providers are evaluated when the corresponding engine axis is read, so they should be inexpensive and free of side effects.
This means consumer code is just:
public bool TriggerHeld => Gamepad.RightTrigger > 0.5f;Not:
// You don't need to do this.public bool TriggerWasPressedThisFrame => _curr && !_prev;A throwing button or mouse provider is caught and treated as false for that poll; a throwing axis provider contributes zero. Other providers continue. The current implementation does not log provider exceptions.
Multi-mod merge semantics
Section titled “Multi-mod merge semantics”The merge behaviour is explicit so two mods on the same input don’t fight:
| Surface | Merge rule |
|---|---|
| Buttons / mouse buttons | Any provider returning true → Input.GetKey returns true. Down / Up fires on any provider’s transition. |
| Axes | Every provider’s value is summed with vanilla and clamped to [-1, 1]. |
Two mods both pushing Horizontal +0.7 cap at +1.0, not +1.4. Two mods both holding the fire button just both report fire — neither wins.
Vanilla input always wins on false → true transitions. The framework’s postfix only contributes additively; it cannot suppress a key press the user is making with their keyboard.
// Two mods coexisting on Horizontal — both contribute, sum is clamped.VirtualInputs.RegisterAxis("moda.horizontal", "Horizontal", () => 0.7f);VirtualInputs.RegisterAxis("modb.horizontal", "Horizontal", () => 0.7f);// Input.GetAxis("Horizontal") now returns 1.0 (clamped from 1.4).Lifecycle
Section titled “Lifecycle”The runtime removes all providers on runtime reset, disconnect, and every single-scene transition; global reset is internal. Old registration handles are stale after a reset. Re-register providers at the appropriate ClientEvents.Ready boundary when they should continue. Ready does not guarantee a local hub; provider callbacks that use gameplay state must check their own prerequisites. Disconnect also covers failed handshake/retry attempts, so clearing local state must be safe to repeat.
using System;using Anomaly.Client.Api.Events.Args;using Anomaly.Client.Api.Events.Handlers;using Anomaly.Client.Api.Input;using MelonLoader;using UnityEngine;using UnityEngine.SceneManagement;
public sealed class Core : MelonMod{ private IDisposable _fireHandle;
public override void OnInitializeMelon() { ClientEvents.Ready += OnReady; ClientEvents.Disconnected += OnDisconnected; ClientEvents.SceneLoaded += OnSceneLoaded; }
public override void OnDeinitializeMelon() { ClientEvents.Ready -= OnReady; ClientEvents.Disconnected -= OnDisconnected; ClientEvents.SceneLoaded -= OnSceneLoaded; _fireHandle?.Dispose(); _fireHandle = null; }
private void OnReady(ClientReadyEventArgs _) { _fireHandle?.Dispose(); _fireHandle = VirtualInputs.RegisterButton( "mymod.fire", KeyCode.Mouse0, ReadControllerTrigger); }
private void OnDisconnected(ClientDisconnectedEventArgs _) { _fireHandle?.Dispose(); _fireHandle = null; }
private void OnSceneLoaded(SceneLoadedEventArgs args) { // Single-scene loads clear providers before dispatching this event. if (args.SceneMode == LoadSceneMode.Single) _fireHandle = null; }
private static bool ReadControllerTrigger() { // Replace with a read from your controller/device SDK. return false; }}For using-block scoping (gesture system that registers a stick only while a window is open):
void RunGestureCalibration(){ using (VirtualInputs.RegisterAxis("mymod.calib_x", "Horizontal", GestureProviderX)) using (VirtualInputs.RegisterAxis("mymod.calib_y", "Vertical", GestureProviderY)) { RunCalibrationFlow(); } // Providers removed automatically.}For explicit cleanup while disabling or disposing your mod:
VirtualInputs.UnregisterAllForMod("mymod");Relationship to InputRegistry
Section titled “Relationship to InputRegistry”| Need | Use |
|---|---|
User-rebindable named keybind (“Jump”, “Reload”) with bindings.json overrides | InputRegistry |
| Drive WASD from a stick / drive Mouse0 from a gesture / drive an axis from a custom device | VirtualInputs |
| Both at once (e.g. a controller mod also exposes a “Toggle gyro” rebindable key) | Both |
InputRegistry is described in Client API Tour — Input. The two systems compose cleanly — InputRegistry polls Input.GetKey(KeyCode) internally, so a VirtualInputs.RegisterButton provider feeds into rebindable bindings just like a real keyboard.
Continue to Local and Dev Mods for the build and launch workflow.