Skip to content

API Input and Controls

Piggo41 edited this page Oct 16, 2026 · 7 revisions

Input and controls

Lua API / Input and controls

Native hotkeys create user-configurable commands in Minecraft's Controls screen. Named input maps separate game actions from physical keys and mouse buttons. Options → Controls opens Minecraft's key bindings by default; the BetaMoon tab shows named actions. Input maps observe press/release transitions and can consume an event before Minecraft performs its normal movement, inventory, GUI, or world action. Legacy key_input and mouse_input events remain observational and cannot consume native behavior.

On this page

Register a Controls hotkey

betamoon.input:hotkey(definition)

Registers a keyboard command and returns a hotkey handle. The command appears in Minecraft's Controls screen, and a player's saved remap is restored on later launches. Use hotkeys for global mod commands that players should configure alongside Minecraft's own keys; use an input map when an action needs context priority, held/released state, mouse input, modifiers, or native-input consumption.

Field Required Meaning
key yes Namespaced key, stored canonically as namespace:hotkey/name.
label yes User-facing name shown in Controls; at most 128 characters.
default yes One unmodified keyboard binding such as key.g; key.none starts unbound.
repeatWhileHeld no Invoke on each ModLoader input tick while held; default false invokes once per press.
local openBuilder = betamoon.input:hotkey {
    key = "mymod:open_builder",
    label = "Open Builder",
    default = "key.b",
}

openBuilder:on(function(event)
    showBuilder()
end)

The callback event has key, binding, and keyCode, where binding and keyCode reflect the player's current Controls assignment. getBinding() returns the current normalized binding, isActive() reports whether the script registration is live, on(nil) removes the callback, and close() detaches the registration. Hotkey callbacks run on gameplay ticks and do not consume Minecraft input; use a handled input-map action when consumption is required. A callback error disables that callback and reports the script error without affecting other hotkeys.

Script unload detaches the callback, but the Controls entry remains for the rest of the Minecraft session. Redeclaring the same key during hot reload reuses that entry and preserves its current remap. Changing repeatWhileHeld for a retained key requires a Minecraft restart; the label may change immediately. A changed default is used on a fresh process only when no saved assignment exists; it never overwrites a player's remap.

Register an input map

betamoon.input:map(definition)

Registers an owner-scoped map and returns an active input-map handle. key accepts namespace:name shorthand and is stored canonically as namespace:input_map/name. Action names use lowercase letters, digits, _, and -.

Field Required Meaning
key yes Namespaced input-map key.
actions yes Table from local action name to a binding string or action definition. A binding may be a string or array.
priority no Default context priority, -10000 through 10000; default 0.
captures no Native families captured by the default context.
label, description no Bounded display text in the BetaMoon Controls tab.
configurable no Show the map in the BetaMoon Controls tab; default true.

An action definition accepts default or bindings, plus optional label, description, and configurable (default true). modes may contain hold and toggle; defaultMode must be one of the allowed modes. Actions default to hold only. In toggle mode, each matching wheel event switches the action on or off.

local controls = betamoon.input:map {
    key = "mymod:builder",
    priority = 10,
    actions = {
        place = { default = "mouse.left" },
        rotate = { default = "key.r", label = "Rotate preview" },
        cancel = { default = "key.escape" },
        zoom = { default = { "mouse.wheel_up", "mouse.wheel_down" } },
        sprint = { default = "key.lcontrol", modes = { "hold", "toggle" }, defaultMode = "hold" },
    },
}

controls.key is the canonical key. Duplicate keys, duplicate action names, overlapping bindings, unknown fields, invalid devices, and maps outside a Lua script raise an error.

Physical bindings

Bindings are case-insensitive when declared and are returned in normalized lowercase form.

Form Examples
Keyboard key.r, key.escape, key.space, key.f5
Named mouse buttons mouse.left, mouse.right, mouse.middle
Numbered mouse buttons mouse.button1 through mouse.button32; button 1 is the left button
Wheel mouse.wheel, mouse.wheel_up, mouse.wheel_down
Required modifiers ctrl+key.r, shift+mouse.left, ctrl+shift+key.s

Required modifiers may be held in addition to other modifiers. For example, ctrl+key.r still matches while Shift is also held. mouse.wheel reports the signed raw wheel delta; directional wheel bindings match only their direction.

Action callbacks

controls:on(action, callback)

Sets the default context's callback for an action and returns controls. Passing nil removes it. A callback runs only for semantic transitions, not once per render frame or repeatedly while held.

controls:on("place", function(event)
    if event.phase == "pressed" then
        beginPlacement()
    end
    return betamoon.callbackResults.handled
end)

The callback receives:

Field Meaning
map, action Canonical map key and local action name.
phase pressed or released. Wheel input is a pressed pulse.
pressed, held, released State snapshot for the current input cycle. A wheel pulse is pressed and released, but not held.
amount 1/0 for digital input, or signed raw wheel delta.
cycle Monotonic input-cycle number.
mode, active Effective hold/toggle mode and logical activity. held remains strictly physical.
activated, deactivated Logical activation changes during this cycle.
device, code Physical source. Omitted for a synthesized lifecycle release.

Return callbackResults.pass or nil to continue to lower contexts and Minecraft. handled and deny both stop lower input contexts and consume the native event; use handled for an action your game performed and deny for an action intentionally refused. An error disables only that context and falls back to the next context or native input.

Action state

controls:getState(action)

Returns a detached { cycle, pressed, held, released, amount, mode, active, activated, deactivated } snapshot. pressed and released last for one input cycle. held tracks physical input even in toggle mode. In toggle mode, active flips on each new physical press or matching wheel event. A screen, focus, world, or player change clears active state. Holding an action does not call Lua repeatedly.

local movement = controls:getState("place")
if movement.held then
    updatePreview()
end

controls:getBindings(action)

Returns an array of the action's current normalized binding strings. The array may be empty when the player has unbound the action.

Context layers and capture

controls:activate(options)

Adds another context for the same map and returns a context handle. Options accept priority and captures. Higher priority runs first; at equal priority, the newest active context runs first. Each context has token, priority, on(action, callback), isActive(), and close().

local wheel = controls:activate {
    priority = 100,
    captures = { "movement", "look", "world_actions" },
}

wheel:on("cancel", function()
    wheel:close()
    return betamoon.callbackResults.handled
end)

Capture names are:

Family Native behavior suppressed while active
actions Native input for physical events matching this map's actions.
movement Forward/back/strafe/jump/sneak key forwarding.
look Mouse-derived player rotation.
world_actions Attack, use, place, and held block breaking.
inventory Inventory/drop keys, hotbar number keys, and wheel slot selection.
gui Keyboard and mouse events while a GUI screen is open, except the engine-owned Controls screens.

Captures are independent: a spell wheel can suppress movement and look without blocking the inventory or every named action. Closing a context immediately reveals the next context.

Rebinding

controls:rebind(action, binding, options)

Replaces the effective binding with a string or array and returns controls. By default, a physical binding that can activate together with another action in the same map raises a conflict error. This includes aliases such as mouse.left/mouse.button1 and modifier subsets such as key.r/ctrl+key.r. { allowConflict = true } permits it.

controls:rebind("rotate", "key.g")
controls:rebind("cancel", { "key.escape", "mouse.right" }, { allowConflict = true })

controls:resetBinding(action)

Restores the declaration's default binding for this session and returns controls. rebind and resetBinding remain session scoped; neither writes player preferences.

Saved bindings and modes

controls:setPreferredBindings(action, bindings, options) saves a player binding override and applies it immediately. Use a string, an array of up to eight bindings, or an empty array to unbind. The optional { allowConflict = true } uses the same conflict rule as rebind. resetPreferredBindings(action) removes the saved override and applies the declaration default.

controls:getMode(action) returns hold or toggle. setMode(action, mode) saves an allowed choice; resetMode(action) returns to the declaration default. resetAllPreferences() removes saved binding and mode overrides for the entire map. These methods update live action state; a mode change clears active toggle state. Preferences are client-wide and survive script reload. A later script rebind can temporarily supersede a saved binding; a new player edit supersedes that session override. Saved controls live in the instance's betamoon/controls.json.

BetaMoon Controls tab

Open Options → Controls for Minecraft and mod hotkeys, then select the BetaMoon tab for named actions. Search or use the mouse wheel to find an action. A focused search box shows _ after the entered text. Hold Backspace to erase search text. Each binding has one row: click its displayed input to change it, + to add another, - to remove it, or R to restore that action's default bindings and mode. Actions with several bindings show their row number. After choosing a binding or +, press a key, mouse button, or scroll the wheel; held Ctrl, Shift, and Alt become part of the binding.

Escape or Cancel binding cancels capture, Delete/Backspace removes the selected binding during capture, and Bind Escape binds Escape explicitly. A conflict names the other action and asks you to choose Use anyway or Cancel. Hold or Toggle appears on a binding row only when both modes are allowed: Hold stays active while pressed, while a wheel binding acts as a short pulse. Toggle switches on or off with each press or matching wheel event. Click the button to switch modes. Reset all requires a second click. An action that allows only one mode has no mode button. Select the Minecraft tab to return to the standard key bindings.

Player controllers

betamoon.input.controllers:add(definition)

Registers a controller with a namespaced key and a stages table. The four stages are movement, look, worldActions, and inventory. Omitted stages use native. Each declared mode is native, modify, manual, or disabled. The returned controller has key, on(stage, callback), activate(options), isActive(), and close(). controllers:get(key) returns nil when absent; getRequired(key) raises an error.

controller:activate { player = ctx.player, priority = 100 } requires the current local player during focused gameplay and returns an activation. Higher priority wins separately for each stage; newest wins ties. An activation exposes submitIntent(intent), face(point), snapshot(), isActive(), and close(). It closes on focus loss, screen/world/player change, death, and script unload. Submissions are valid for the next input cycle only. Omit player to select the current local player when activating outside a player-scoped callback.

local controller = betamoon.input.controllers:add {
    key = "mymod:adventure",
    stages = { movement = "manual", worldActions = "manual", look = "manual" },
}

-- Call this from a gameplay callback that provides the local player handle.
local function beginAdventure(ctx)
    local active = controller:activate { player = ctx.player, priority = 100 }
    active:submitIntent { movement = { forward = 1, strafe = 0, jump = false, sneak = false } }
    return active
end

A modify movement callback receives nativeIntent and may return { movement = ... }, pass, or deny. Movement values are finite in -1..1. A look callback receives nativeFacing and may return { facing = { yaw = number, pitch = number } }; pitch is -90..90. World action callbacks receive nativeAction (attack, use, or heldBreaking), button, and held, and return pass or deny. Inventory callbacks receive open, drop, hotbar, or hotbarWheel. Every callback receives stage, tick, and a callback-scoped player handle. A manual stage defaults to zero movement, frozen look, or denied actions without an intent or explicit pass. Both discrete clicks and held breaking are covered. A callback error releases that stage to a lower controller or native control.

submitIntent { action = { kind = "attack", result = "pass" } } permits only the named native action on the next input cycle. Passing an attack never permits held breaking. face { x = ..., y = ..., z = ... } schedules player facing toward a point and returns false for a point at the player's eye. Camera rigs and route following are separate later capabilities.

See the named actions and controller example for a mode-aware walk action and a fixed Hold action shown together in Controls.

Lifecycle and limits

Input maps, contexts, and controllers are cleaned up automatically on script unload. Hotkey callbacks are detached while their Controls entries remain as described above. Focus, screen, world, and player replacement synthesize releases before physical state is cleared, preventing stuck held actions. A map supports at most 128 actions and eight effective bindings per action. The client supports up to 256 retained native hotkeys, 256 maps, 256 active contexts, 256 controller registrations, 256 controller activations, and 1024 semantic transitions in one cycle.

Clone this wiki locally