Skip to content

Keys and client events

Lesson 14.2

Client mods react to what the player does at the keyboard and what the game window shows. This lesson covers keybinds, the client tick, and the client events for chat and keys.

A keybind

cl.on("lifecycle.client_setup", function()
  local key = cl.keybind.register{
    id = "my_mod:say_hi",
    default_key = "key.keyboard.g",
    category = "key.categories.my_mod",
  }

  cl.on("tick.client_post", function()
    while key:consume_click() do
      cl.log("G was pressed")
    end
  end)
end)
  • Register keybinds in lifecycle.client_setup. They are client-only.
  • default_key uses vanilla’s key names: "key.keyboard.g", "key.keyboard.f6", "key.keyboard.left.alt", "key.keyboard.keypad.5", and for mouse buttons "key.mouse.left", "key.mouse.right", "key.mouse.middle", "key.mouse.4" and "key.mouse.5" (the side buttons). "key.keyboard.unknown" starts it unbound.
  • key:consume_click() returns true once for each press since the last check, then false. Check it every client tick; the while handles two quick presses in one tick.
  • key:is_down() is true while the key is held, for things like “zoom while holding”.

By default a keybind only reacts in the game, not while a menu or the chat is open. context = "gui" makes it work only in menus, and context = "any" everywhere.

In the Controls screen

Every mod keybind appears in Options > Controls > Key Binds, under its category, next to Minecraft’s own. Players can bind it to any key or mouse button and reset it, and the game saves their choice in options.txt like any other key. So always read the key through key:consume_click() and key:is_down(), never by checking a fixed key yourself. key:key() tells you what it is bound to right now, for example to show it in a hint:

cl.log("Press " .. key:key() .. " to say hi")   --> Press key.keyboard.g to say hi

Give the keybind and its category a name in your language file. The keys are key. plus the id with : replaced by ., and the category as you wrote it:

{
  "key.my_mod.say_hi": "Say hi",
  "key.categories.my_mod": "My Mod"
}

Without language entries, the Controls screen shows label if you gave one (label = "Say hi" in register), else the id made readable (“Say Hi”), and your mod’s name as the category when you left category out.

The client tick

tick.client_post runs 20 times a second in the game window, also on servers without your mod. It is the right place for checking keys and updating anything your HUD shows:

local height = 0

cl.on("tick.client_post", function()
  local me = cl.entity.client_player()
  if me then
    height = math.floor(me.pos.y)
  end
end)

cl.entity.client_player() is you, the player at this computer, or nil when no world is open. On the client you can read it like any player: pos, health, inventory_get. Changing things (give, teleport) is for the server.

Chat on the client

client.chat_send happens when you press Enter in chat, before the message leaves your game. Cancel it to keep the message to yourself, which makes chat-style commands for client mods possible:

cl.on("client.chat_send", function(e)
  if e.message == "!where" then
    e:cancel()                          -- never sent to the server
    local me = cl.entity.client_player()
    cl.log("I am at", cl.pos(me.pos))
  end
end)

client.chat_received happens when a message arrives. You can change e.message or cancel it to hide it:

-- Hide join and leave messages.
cl.on("client.chat_received", function(e)
  if e.message:find("joined the game") or e.message:find("left the game") then
    e:cancel()
  end
end)

e.message is the text without colours, and e.overlay is true for action-bar messages.

Raw keys

client.key reports every key press and release, before the game handles it:

cl.on("client.key", function(e)
  if e.action == 1 and not e.screen_open then
    cl.log("key code", e.key)
  end
end)
Field Means
key The key’s code, as in GLFW (71 is G, 32 is space).
action 1 pressed, 0 released, 2 held down and repeating.
modifiers Held modifier keys: add 1 shift, 2 control, 4 alt.
screen_open true while a menu or the chat is open.

Cancelling client.key hides the key from the game. Use it sparingly: players expect their keys to work. For your own features, a keybind is nearly always the better choice.

Other client events

Event Happens when
client.connect You join a server (e.address) or a singleplayer world.
client.disconnect You leave it.
client.screen_open A menu opens (e.screen_id).

Try it

Make a keybind on H that logs your current health. Holding Left Alt while you press it should log your position too.

Show a solution
cl.on("lifecycle.client_setup", function()
  local health_key = cl.keybind.register{
    id = "my_mod:show_health",
    default_key = "key.keyboard.h",
    category = "key.categories.my_mod",
  }
  local alt_key = cl.keybind.register{
    id = "my_mod:with_position",
    default_key = "key.keyboard.left.alt",
    category = "key.categories.my_mod",
  }

  cl.on("tick.client_post", function()
    while health_key:consume_click() do
      local me = cl.entity.client_player()
      if me then
        cl.log("health: %.1f", me.health)
        if alt_key:is_down() then
          cl.log("position:", cl.pos(me.pos))
        end
      end
    end
  end)
end)

The second keybind is only used for is_down, so it works as a modifier.

Recap: register keybinds in lifecycle.client_setup and check consume_click in tick.client_post; players rebind them in the Controls screen; client.chat_send, client.chat_received and client.key let client mods react to chat and keys.