Skip to content

Input Interception

All GLFW input goes through tuxinjector for hotkey processing, key rebinding, mouse sensitivity scaling, and GUI overlay routing. It uses the same dlsym + PLT hook mechanism from Injection & Hooking, but there's a lot more going on in the actual processing pipeline.


Callback Interception

Minecraft (LWJGL3) registers GLFW callbacks for keyboard, mouse, cursor, and scroll events. We intercept the glfwSetXxxCallback calls, stash the game's original callback, and install our own wrapper instead:

Game: glfwSetKeyCallback(window, game_key_handler)
Hook: GAME_KEY_CALLBACK = game_key_handler
      real_glfwSetKeyCallback(window, tuxinjector_key_callback)
      return old_game_callback

From that point on, GLFW calls our tuxinjector_key_callback for every key event. The wrapper decides whether to eat the event (hotkey match, GUI has focus) or forward it to the game's original callback.

Intercepted Callbacks

GLFW Function Wrapper Purpose
glfwSetKeyCallback tuxinjector_key_callback Hotkeys, key rebinding, GUI keyboard input
glfwSetMouseButtonCallback tuxinjector_mouse_button_callback Mouse button rebinding, GUI click forwarding
glfwSetCursorPosCallback tuxinjector_cursor_pos_callback Mouse sensitivity scaling, GUI cursor tracking
glfwSetScrollCallback tuxinjector_scroll_callback Scroll consumption for GUI, hotkey scroll binds
glfwSetCharCallback tuxinjector_char_callback Character rebinding, GUI text input
glfwSetCharModsCallback tuxinjector_char_mods_callback Same thing but LWJGL3 prefers this variant
glfwSetInputMode intercept_set_input_mode Tracks cursor capture state (FPS vs menu)

Key Event Flow

Every key event runs through a multi-stage pipeline:

GLFW key event -> tuxinjector_key_callback:

1. update_key_state(key, action)           // Track pressed keys for Lua get_key()
2. INPUT_HANDLER.handle_key(key, ...)      // Hotkey engine + rebinding
   |-- consumed = true?  -> event swallowed (hotkey matched)
   '-- consumed = false  -> forward_key:
      |-- forward_key >= MOUSE_BUTTON_OFFSET?
      |   -> forward as mouse button event to game
      |   -> if original key is modifier, also forward key event
      '-- forward_key < MOUSE_BUTTON_OFFSET?
          -> forward as key event to game (using remapped keycode)
3. If action == PRESS and key was rebinded:
   '-- If original key has no character but target does:
       -> inject synthetic char event (emitTypedChar)

InputHandler Trait

Input processing is abstracted behind an InputHandler trait:

trait InputHandler: Send {
    fn handle_key(&mut self, key: i32, scancode: i32, action: i32, mods: i32)
        -> (consumed: bool, forward_key: i32);

    fn handle_mouse_button(&mut self, button: i32, action: i32, mods: i32)
        -> (consumed: bool, forward_button: i32);

    fn handle_cursor_pos(&mut self, x: f64, y: f64)
        -> Option<(f64, f64)>;    // None = consume, Some = forward

    fn handle_scroll(&mut self, x: f64, y: f64)
        -> bool;                   // true = consume
}

TuxinjectorInputHandler checks hotkeys first, then rebinding. If a hotkey matches, the event gets eaten and never reaches the game.


Key Rebinding

The rebinding system translates GLFW keycodes before the game sees them. It handles keyboard-to-keyboard, keyboard-to-mouse, and mouse-to-keyboard, so you can do things like rebind Mouse4 to F3.

Encoding

Mouse buttons get encoded as button + MOUSE_BUTTON_OFFSET (400) so they share the same keycode space as keyboard keys:

Input Encoded Value
GLFW_KEY_A (65) 65
GLFW_KEY_F3 (292) 292
GLFW_MOUSE_BUTTON_1 (0) 400
GLFW_MOUSE_BUTTON_4 (3) 403
GLFW_MOUSE_BUTTON_5 (4) 404

Forward and Reverse Maps

Two lookup maps handle everything:

Forward map: (from_key, to_key)    -- used in callbacks and char event remapping
Reverse map: (to_key, from_key)    -- used in glfwGetKey/glfwGetMouseButton polling

When a key event comes in, the forward map translates the physical keycode to the logical one. When the game polls a key via glfwGetKey, the reverse map goes the other way.

Cross-Device Rebinding

Things get interesting when you rebind across device types (mouse to keyboard or vice versa), because glfwGetKey and glfwGetMouseButton need to be cross-routed:

Example: Mouse4 (button 3) rebound to F3 (keycode 292)

Forward map:  (403, 292)    // encoded Mouse4 -> F3
Reverse map:  (292, 403)    // F3 -> encoded Mouse4

Callback path:
  Mouse4 press -> handle_mouse_button(3, ...)
    encoded = 3 + 400 = 403
    remap_key(403) -> 292 (F3)
    forward_key = 292 < MOUSE_BUTTON_OFFSET
    -> forward_key_to_game(window, 292, ...)
    Game sees: F3 pressed

Polling path:
  Game calls glfwGetKey(window, 292)  // "Is F3 held?"
    physical_key_for(292) -> 403 (encoded Mouse4)
    403 >= MOUSE_BUTTON_OFFSET
    -> glfwGetMouseButton(window, 403 - 400 = 3)
    Returns: real mouse button 3 state

Combo keys:
  Mouse4 + C held -> game polls glfwGetKey(F3) + glfwGetKey(C)
    glfwGetKey(F3) -> route to glfwGetMouseButton(3) -> PRESS
    glfwGetKey(C)  -> route to real glfwGetKey(C)    -> PRESS
    Game sees: F3+C held

The reverse direction also works - if a keyboard key is rebound to a mouse button, glfwGetMouseButton gets routed to poll the keyboard key via glfwGetKey.


Character Rebinding

When a key gets rebound, its character output needs to change too. GLFW fires CharCallback separately from KeyCallback, so characters need their own rebinding pass:

Incoming char 'r' (codepoint 114):
1. codepoint_to_glfw_key(114) -> (82, shifted=false)   // 'r' -> GLFW_KEY_R
2. forward_remap(82) -> Some(292)                        // R -> F3
3. glfw_key_to_codepoint(292, false) -> None              // F3 has no char
4. Return 0 -> suppress char event

And when a non-character key (like F1) gets rebound to a character key (like A), the key callback has to inject a synthetic char event manually since GLFW wouldn't normally fire one for the original key.


Mouse Sensitivity Scaling

We intercept glfwSetCursorPosCallback and glfwGetCursorPos to scale mouse deltas in FPS mode:

Cursor event (x, y):
  if cursor_captured:
    delta_x = x - center_x
    delta_y = y - center_y
    scaled_x = center_x + delta_x * sensitivity_x
    scaled_y = center_y + delta_y * sensitivity_y
    forward (scaled_x, scaled_y) to game
  else:
    forward (x, y) unchanged   // Menu mode: no scaling

Sensitivity can be set globally, per-mode, or through Lua hotkey actions.

Cursor State Tracking

glfwSetInputMode is intercepted to know whether the cursor is captured (GLFW_CURSOR_DISABLED) or free (GLFW_CURSOR_NORMAL):

Cursor Mode Sensitivity GUI Cursor
GLFW_CURSOR_DISABLED (FPS) Active Hidden (forced visible when GUI opens)
GLFW_CURSOR_NORMAL (Menu) Inactive Already visible

When the GUI opens during FPS mode, we force the cursor to GLFW_CURSOR_NORMAL. When it closes, we restore GLFW_CURSOR_DISABLED. If the game tries to re-capture the cursor while the GUI is open, we just block that call.


GUI Input Routing

When the GUI overlay is visible, input gets routed to imgui instead of the game:

Key event + GUI visible:
  if key_capture_mode:
    push_captured_key(key)        // Record next key press for rebind config
  else if gui_wants_keyboard:
    push_gui_key(key, mods)       // Forward to imgui (text field has focus)
  else:
    check hotkeys only            // GUI toggle, etc.

Mouse button + GUI visible:
  push_gui_button_press/release() // Forward click to imgui

Cursor + GUI visible:
  forward raw position            // imgui needs cursor position for hover/click

Scroll + GUI visible:
  push_gui_scroll(dx, dy)         // Forward scroll to imgui

The GUI state flags (GUI_VISIBLE, GUI_WANTS_KEYBOARD, GUI_CAPTURE_MODE) are all atomic booleans, so they're lock-free and can be read from any thread.


Key State Tracking

A HashSet<i32> keeps track of which keys are currently held down. Gets updated on every key event and is what the Lua API's get_key() function reads from:

fn update_key_state(key: i32, action: i32) {
    if action == GLFW_PRESS:
        pressed_keys.insert(key)
    if action == GLFW_RELEASE:
        pressed_keys.remove(key)
}

// Lua: ts.get_key("w") -> is_key_pressed(87) -> pressed_keys.contains(87)