
Writing Lua Profiles
Tsukiko-chan wants your gamepad to work properly.
This guide explains how to write custom Lua transformation scripts.
How it works
The tool reads a frame of input events from the physical controller (all events
between two SYN_REPORT markers), passes it to your Lua script, and writes the
returned frames to the virtual controller.
Your script must return a single function that receives a frame table and
returns a new frame table. The input is always a single frame, but the output
may consist of multiple, concatenated frames separated by SYN_REPORTs.
SYN_REPORT is not present in the input frame. For output frames, it must be
present.
Frame format
The function receives a table with this structure:
{
frame = {
{ evtype = EV_KEY, code = BTN_A, value = 1, time = 12345678 },
{ evtype = EV_ABS, code = ABS_X, value = 1024, time = 12345678 },
...
}
}
Each event has:
evtype(integer) — Event type (e.g.EV_KEY,EV_ABS,EV_SYN)code(integer) — Event code (e.g.BTN_A,ABS_X)value(integer) — Event value (button state or axis position)time(integer) — Timestamp in microseconds
Return a table in the same format:
return {
frame = {
{ evtype = EV_KEY, code = BTN_SOUTH, value = 1, time = 12345678 },
{ evtype = EV_SYN, code = SYN_REPORT, value = 0, time = 0 },
}
}
The returned frame must end with a SYN_REPORT event. The mggr.common
module handles this automatically when you use process_frame.
Using mggr.common
The standard library provides composable building blocks:
local mggr = require "mggr.common"
Global variables
The tool injects some global tables before your script runs:
-
mggr_libevdev— evdev constants (EV_KEY,EV_ABS,BTN_A,ABS_X,SYN_REPORT, etc.). Use these instead of hardcoded numbers. -
mggr_source_axes— axis metadata from the physical controller. Keyed by axis ID, each entry hasminimumandmaximumfields. If the physical controller does not provide an axis, indexing the table with its ID will return nil.-- Example: mggr_source_axes[ABS_X] = { minimum = 0, maximum = 255 } -
mggr_limits— ranges for axes, triggers and d-pads of the virtual gamepad:mggr_limits.AXIS_NEG -- -32768 (analog stick minimum) mggr_limits.AXIS_POS -- 32767 (analog stick maximum) mggr_limits.DPAD_NEG -- -1 (D-Pad minimum) mggr_limits.DPAD_POS -- 1 (D-Pad maximum) mggr_limits.DPAD_REST -- 0 (D-Pad center) mggr_limits.TRIGGER_REST -- 0 (trigger minimum) mggr_limits.TRIGGER_MAX -- 255 (trigger maximum)
Event helpers
-
mggr.new_event(type, code, value [, time])— create a new event table. Time defaults to 0. -
mggr.ev_syn_report— pre-builtSYN_REPORTevent.
Mappers
Mappers are functions that transform a single event into a new event (or nil
to drop it). There are two trivial mappers:
-
mggr.identity(ev)— return the event unchanged. -
mggr.drop(ev)— returnnil(remove the event from the frame).
Other mappers require parameters (a button ID, an axis range, etc). The following functions take parameters and create anonymous mappers that perform the requested transformation:
-
mggr.map_button_to_button(target_button)— remap a button press to a different button:mggr.map_button_to_button(BTN_SOUTH) -- any button → BTN_SOUTH -
mggr.map_button_to_axis(axis, on, off)— convert a digital button to an analog axis value:mggr.map_button_to_axis(ABS_Z, 255, 0) -- press→255, release→0 -
mggr.scale_axis(axis_id, out_max, out_min)— rescale an analog axis from the physical controller’s range to a new range. Usesmggr_source_axesto determine the input range:mggr.scale_axis(ABS_X, 32767, -32768) -- scale to XBox range -
mggr.map_axis_to_axis(from_axis, to_axis, max, min)— remap an axis to a different axis, scaling to the new range:mggr.map_axis_to_axis(ABS_Z, ABS_RY, 32767, -32768) -
mggr.map_axis_halves_to_extremes(axis_id)— quantize a noisy axis: values in the lower half map tominimum, upper half tomaximum. Useful for fixing noisy triggers. -
mggr.chain(a, b, ...)— compose mappers. The rightmost mapper runs first:mggr.chain(a, b) -- equivalent to: function(ev) return a(b(ev)) end
MapperTable
MapperTable is a dispatch table that maps (evtype, code) pairs to mapper
functions:
local r = mggr.MapperTable:new(default_mapper)
-
MapperTable:new([default])— create a new table. Unmatched events are passed throughdefault(ormggr.identityif omitted). -
r[{ evtype, code }] = mapper_fn— register a mapper for a specific event type and code:
r[{ EV_KEY, BTN_NORTH }] = mggr.map_button_to_button(BTN_WEST)
r[{ EV_ABS, ABS_X }] = mggr.scale_axis(ABS_X, AXIS_POS, AXIS_NEG)
mggr.process_frame(mapper_table)— wraps aMapperTableinto the function that the C code expects. It iterates over all events in the frame, applies the matching mapper (or the default), collects non-nil results, appendsSYN_REPORT, and returns the new frame:
return mggr.process_frame(r)
Example: minimal profile
Drop all events except button remapping:
local mggr = require "mggr.common"
local r = mggr.MapperTable:new(mggr.drop) -- drop everything by default
r[{ mggr_libevdev.EV_KEY, mggr_libevdev.BTN_NORTH }] =
mggr.map_button_to_button(mggr_libevdev.BTN_SOUTH)
return mggr.process_frame(r)
Example: fix noisy triggers
local mggr = require "mggr.common"
local r = mggr.MapperTable:new()
r[{ mggr_libevdev.EV_ABS, mggr_libevdev.ABS_Z }] =
mggr.map_axis_halves_to_extremes(mggr_libevdev.ABS_Z)
r[{ mggr_libevdev.EV_ABS, mggr_libevdev.ABS_RZ }] =
mggr.map_axis_halves_to_extremes(mggr_libevdev.ABS_RZ)
return mggr.process_frame(r)
Example: full SixAxis → XBox profile
See the built-in sixaxis profile for a complete, commented example that
handles D-Pad conversion, button swapping, trigger scaling, and analog stick
normalization.
Tips
- Start from a copy of an existing built-in profile and modify it.
- Use
mggr.dropto remove events you don’t want (e.g. digital trigger events when you have analog ones). - The
chainfunction is useful for combining transformations: e.g. scale an axis, then invert it. - Run with
--verboseto see which events are received and sent.