can you write a script that will comprehensively map wasd and my mouse as a controller output
--[[ rebind: min_sdk=3.11.0 rebind: name=Keyboard and Mouse to Joypad rebind: description=WASD drives the left stick, the mouse the right stick, and the mouse buttons pull the triggers. rebind: hardware_only=true rebind: mouse_block=true rebind: permissions=none ]] -- needs hardware mode and a Rebind Link whose USB profile includes a joypad; -- add Joypad under Devices > Flash identity before attaching a controller local cfg = rb.ui.schema({ toggle = rb.ui.keybind("F9", { label = "Engage / release" }), gain = rb.ui.slider(0.35, { min = 0.05, max = 1, step = 0.05, label = "Mouse gain" }), smoothing = rb.ui.slider(55, { min = 0, max = 90, suffix = "%", label = "Stick smoothing" }), }) local MOVE_KEYS = { W = { x = 0, y = -1 }, S = { x = 0, y = 1 }, A = { x = -1, y = 0 }, D = { x = 1, y = 0 }, } local IDLE_MS = 250 -- hand the axes back to a real controller after this local engaged = false local held = {} local trigger = { LT = 0, RT = 0 } local acc_x, acc_y = 0, 0 local rsx, rsy = 0, 0 local idle_ms, driving = 0, false local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end local function release_all() if driving then rb.hid.joypad_reset() -- axes a script sets stay set until this driving = false end acc_x, acc_y = 0, 0 rsx, rsy = 0, 0 trigger.LT, trigger.RT = 0, 0 held = {} idle_ms = 0 end rb.bind(cfg.toggle, function() engaged = not engaged if engaged then idle_ms = 0 rb.ui.notify("Joypad output on", "info") else release_all() rb.ui.notify("Joypad output off", "info") end end) for key in MOVE_KEYS do rb.bind(key, function() held[key] = true end, { when = function() return engaged end, release = function() held[key] = nil end, }) end -- mouse buttons pull the triggers local function click_trigger(button, axis) rb.bind(button, function() trigger[axis] = 1 end, { when = function() return engaged end, release = function() trigger[axis] = 0 end, }) end click_trigger("Mouse1", "RT") click_trigger("Mouse2", "LT") rb.on("move", function(dx, dy) if not engaged then return true -- cursor works normally end acc_x += dx acc_y += dy return false end) rb.on("tick", function(dt_ms) if not engaged or dt_ms <= 0 then return end -- right stick: smoothed mouse velocity, so fast flicks are not clipped local keep = cfg.smoothing / 100 local blend = 1 - keep rsx = rsx * keep + clamp(acc_x / dt_ms * cfg.gain, -1, 1) * blend rsy = rsy * keep + clamp(acc_y / dt_ms * cfg.gain, -1, 1) * blend acc_x, acc_y = 0, 0 if math.abs(rsx) < 0.02 then rsx = 0 end if math.abs(rsy) < 0.02 then rsy = 0 end -- left stick: WASD, diagonals scaled so they are not faster local lx, ly = 0, 0 for key, dir in MOVE_KEYS do if held[key] then lx += dir.x ly += dir.y end end if lx ~= 0 and ly ~= 0 then lx, ly = lx * 0.7071, ly * 0.7071 end local active = rsx ~= 0 or rsy ~= 0 or lx ~= 0 or ly ~= 0 or trigger.LT > 0 or trigger.RT > 0 idle_ms = if active then 0 else idle_ms + dt_ms if idle_ms >= IDLE_MS then if driving then rb.hid.joypad_reset() driving = false end return end driving = true rb.hid.axis("RightX", rsx) rb.hid.axis("RightY", rsy) rb.hid.axis("LeftX", lx) rb.hid.axis("LeftY", ly) rb.hid.axis("LT", trigger.LT) rb.hid.axis("RT", trigger.RT) end) rb.on("blur", release_all) rb.on("stop", release_all)
How it works
when, so with the script released WASD types normally.move handler accumulates deltas and swallows them (return false); the tick handler turns that accumulation into a stick deflection proportional to velocity, then applies an exponential moving average. gain sets how far the stick travels per pixel per ms, smoothing sets how much of the previous value is kept (higher = steadier, slightly laggier). Leftover sub-pixel movement stays in the accumulator, so nothing is lost.Mouse1 fully pulls RT, Mouse2 pulls LT.rb.hid.joypad_reset() drops what the script set, so a real controller has its axes back. An axis a script sets otherwise stays set, which is why the reset matters.blur and stop reset the joypad and clear held state, so no stick is left pushed when you release or the script stops.To add buttons, add remaps next to the binds — rb.bind.remap("Space", "PadA"), rb.bind.remap("Q", "PadLB"), and so on. Pad names are PadA, PadB, PadX, PadY, PadLB, PadRB, PadLT, PadRT, PadUp…PadRight. Left Ctrl + Left Alt + K still kills every script and releases held input.
Learn more