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

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.

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

SurfaceMerge rule
Buttons / mouse buttonsAny provider returning true → Input.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 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");
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.

Continue to Local and Dev Mods for the build and launch workflow.