Skip to content

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.

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..

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.

The merge behaviour is explicit so two mods on the same input don’t fight:

SurfaceMerge rule
Buttons / mouse buttonsAny provider returning trueInput.GetKey returns true. Down / Up fires on any provider’s transition.
AxesEvery 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).

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");
NeedUse
User-rebindable named keybind (“Jump”, “Reload”) with bindings.json overridesInputRegistry
Drive WASD from a stick / drive Mouse0 from a gesture / drive an axis from a custom deviceVirtualInputs
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.