Tsukiko-chan

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:

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:

Event helpers

Mappers

Mappers are functions that transform a single event into a new event (or nil to drop it). There are two trivial mappers:

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:

MapperTable

MapperTable is a dispatch table that maps (evtype, code) pairs to mapper functions:

local r = mggr.MapperTable:new(default_mapper)
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)
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