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. Disposing removes the provider; mods that hold for the session don’t have to dispose. 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 every registered provider once per frame from ClientRuntime.Update and infers KeyDown / KeyUp / MouseButtonDown / MouseButtonUp from the previous-frame state.
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 provider that throws is caught in the per-frame tick (logged once and skipped). Throwing doesn’t break the input pipeline for other providers.
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 framework calls VirtualInputs.Reset() on ClientEvents.Disconnected so providers don’t leak across server sessions. If your providers should persist across sessions, re-register them in the next ClientEvents.Ready.
public class Core : MelonMod{ private IDisposable _fireHandle;
public override void OnInitializeMelon() { ClientEvents.Ready += _ => { _fireHandle = VirtualInputs.RegisterButton( "mymod.fire", KeyCode.Mouse0, () => Gamepad.RightTrigger > 0.5f); };
ClientEvents.Disconnected += _ => { // _fireHandle was already invalidated by VirtualInputs.Reset(). // Safe to clear the field; don't call Dispose on a stale handle. _fireHandle = null; }; }}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 mass cleanup on hot-reload:
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.
That’s it for the modding chapters. For the supported public assemblies and namespaces, see the API and Namespace Index.