
# Packets

The `packets` global sends; the `packet_send` and `packet_receive` [events](events.md#network)
observe. Fields mirror the wire format, so coordinates arrive as raw `x, y, z` scalars rather
than [vec3](vec3.md).

```lua
module:event("packet_receive", function(e)
    if e.name == "sound" then e:cancel() end   -- mute server-played sounds
end)
```

Packets are matched by stable snake_case names, the same strings `e.name` reports and
`packets.send` accepts. Every vanilla packet is mapped, in every protocol phase (handshake,
status, login, configuration, play); only a packet a mod adds arrives under its protocol id
(a name with a colon) carrying no decoded fields, still cancellable. Ids are strings
(`"minecraft:stone"`), uuids are strings, enums are lowercase names, text components arrive
as plain strings and lists are 1-based arrays.

| Function | Does |
|----------|------|
| `packets.list()` | Sorted names `packets.send` can build |
| `packets.send(name, fields)` | Builds and sends a C2S packet, `true` on success |

`packets.send` raises on an unknown name and returns `false` when a required game object is
missing. It goes through the normal client pipeline, so it fires `packet_send` as well: a
module that cancels its own scripted packets swallows them.

```lua
local pos = player:position()
packets.send("move_position", { x = pos.x, y = pos.y + 0.05, z = pos.z, on_ground = false })
packets.send("client_command", { mode = "start_sprinting" })
packets.send("player_action", { action = "swap_item_with_offhand", x = 0, y = 0, z = 0 })

local target = world:player("Notch")
if target then
    packets.send("attack_entity", { entity_id = target:id() })
end
```

For attacking, breaking blocks and clicking slots, [`interaction`](interaction.md) is the
better tool: it gets swing timers and sequence numbers right. Raw packets are for when you
want the wire format yourself.

## Sendable packets

Optional fields show their defaults. `hand` is `"main"` / `"off"` (default `"main"`), `side`
is `"up"` · `"down"` · `"north"` · `"south"` · `"east"` · `"west"` (default `"up"`), and
`sequence` defaults to `0`.

<details open>
<summary>All 67 sendable packets</summary>

| Name | Fields |
|------|--------|
| `move_full` | `x, y, z, yaw, pitch, on_ground = true, horizontal_collision = false` |
| `move_position` | `x, y, z, on_ground = true, horizontal_collision = false` |
| `move_look` | `yaw, pitch, on_ground = true, horizontal_collision = false` |
| `move_on_ground` | `on_ground = true, horizontal_collision = false` |
| `client_command` | `mode` — `"stop_sleeping"` · `"start_sprinting"` · `"stop_sprinting"` · `"start_riding_jump"` · `"stop_riding_jump"` · `"open_inventory"` · `"start_fall_flying"`; `jump_height = 0` |
| `client_status` | `mode` — `"perform_respawn"` · `"request_stats"` |
| `hand_swing` | `hand` |
| `player_action` | `action` — `"start_destroy_block"` · `"abort_destroy_block"` · `"stop_destroy_block"` · `"drop_all_items"` · `"drop_item"` · `"release_use_item"` · `"swap_item_with_offhand"` · `"stab"`; `x, y, z, side, sequence` |
| `attack_entity` | `entity_id, sneaking = false` |
| `interact_entity` | `entity_id, hand, sneaking = false` |
| `interact_entity_at` | `entity_id, x, y, z` (hit point), `hand, sneaking = false` |
| `interact_block` | `x, y, z, side, hit_x/hit_y/hit_z` (default block center), `inside = false, hand, sequence` |
| `interact_item` | `hand, yaw, pitch` (default current rotation), `sequence` |
| `select_slot` | `slot` — hotbar `0–8` |
| `close_screen` | `sync_id` |
| `teleport_confirm` | `id` |
| `keep_alive` | `id` |
| `command` | `command` — without the leading `/` |
| `player_input` | `forward, backward, left, right, jump, sneak, sprint` — all `false` |
| `sign_update` | `x, y, z, front = true, line1..line4 = ""` — the text a sign editor would submit |
| `pick_item_from_block` | `x, y, z, include_data = false` |
| `pick_item_from_entity` | `entity_id, include_data = false` |
| `block_entity_tag_query` | `x, y, z, transaction_id = 0` |
| `bundle_item_selected` | `slot, index = -1` |
| `change_difficulty` | `difficulty` — `"peaceful"` · `"easy"` · `"normal"` · `"hard"` |
| `change_game_mode` | `mode` — `"survival"` · `"creative"` · `"adventure"` · `"spectator"` |
| `chat_ack` | `offset` |
| `chunk_batch_received` | `chunks_per_tick = 9` |
| `client_tick_end` | — |
| `command_suggestion` | `command, id = 0` |
| `configuration_acknowledged` | — (tells the server you left the play phase: it will reconfigure you) |
| `container_button_click` | `sync_id, button` |
| `container_click` | `sync_id, slot, button = 0, mode = "pickup", revision = 0` — `mode` is `"pickup"` · `"quick_move"` · `"swap"` · `"clone"` · `"throw"` · `"quick_craft"` · `"pickup_all"`; sent without predicted slot changes, so the server resyncs the container |
| `container_slot_state_changed` | `slot, sync_id, state = true` |
| `debug_subscription_request` | `subscriptions` — list of subscription ids |
| `edit_book` | `slot, pages` (list of strings), `title` (signs the book when present) |
| `entity_tag_query` | `entity_id, transaction_id = 0` |
| `jigsaw_generate` | `x, y, z, levels = 1, keep_jigsaws = false` |
| `lock_difficulty` | `locked = true` |
| `move_vehicle` | `x, y, z, yaw = 0, pitch = 0, on_ground = false` |
| `paddle_boat` | `left = false, right = false` |
| `place_recipe` | `sync_id, recipe, use_max_items = false` — `recipe` is the display index from `recipe_book_add` |
| `player_abilities` | `flying = false` |
| `player_loaded` | — |
| `recipe_book_change_settings` | `book` — `"crafting"` · `"furnace"` · `"blast_furnace"` · `"smoker"`; `open = false, filtering = false` |
| `recipe_book_seen_recipe` | `recipe` |
| `rename_item` | `name` |
| `seen_advancements` | `tab` — advancement tab id; omit it to report the screen closed |
| `select_trade` | `index` |
| `set_beacon` | `primary, secondary` — effect ids, both optional |
| `set_command_block` | `x, y, z, command, mode = "redstone"` (`"sequence"` · `"auto"` · `"redstone"`), `track_output = true, conditional = false, automatic = false` |
| `set_command_minecart` | `entity_id, command, track_output = true` |
| `set_creative_mode_slot` | `slot, item, count = 1` — omit `item` to clear the slot |
| `set_game_rule` | `entries` — list of `{ rule, value }` |
| `set_jigsaw_block` | `x, y, z, name, target, pool, final_state = "minecraft:air", joint = "rollable", selection_priority = 0, placement_priority = 0` |
| `set_structure_block` | `x, y, z, update = "update_data", mode = "data", name = "", offset_x/y/z = 0, size_x/y/z = 0, mirror = "none", rotation = "none", data = "", ignore_entities = true, strict = false, show_air = false, show_bounding_box = true, integrity = 1, seed = 0` |
| `set_test_block` | `x, y, z, mode = "start", message = ""` |
| `spectator_action` | `entity_id` — omit it to stop spectating |
| `teleport_to_entity` | `uuid` |
| `test_instance_block_action` | `x, y, z, action` — `"init"` · `"query"` · `"set"` · `"reset"` · `"save"` · `"export"` · `"run"`; `test, size_x/y/z = 0, rotation = "none", ignore_entities = false` |
| `client_information` | `language, view_distance, chat_visibility, chat_colors, model_customisation, main_hand, text_filtering, allows_listing, particle_status` — each defaults to your current options |
| `custom_click_action` | `id, payload` — optional SNBT compound |
| `custom_payload` | `brand` — only the brand payload can be built |
| `pong` | `id` |
| `resource_pack` | `id` (pack uuid), `action` — `"successfully_loaded"` · `"declined"` · `"failed_download"` · `"accepted"` · `"downloaded"` · `"invalid_url"` · `"failed_reload"` · `"discarded"` |
| `cookie_response` | `key, payload` — optional string |
| `ping_request` | `time` — default now |

</details>

## Decoded event fields

Both events always carry `name`. A `?` marks a field that is only present when the packet
carries it; `x/y/z` stands for the three scalars (`vel_x/y/z` for `vel_x`, `vel_y`, `vel_z`).

**Outgoing.** Sendable packets decode with the same fields they take, except that `move_*`
carry `x/y/z` only when the position changed and `yaw/pitch` only when the look changed,
`interact_entity_at` arrives as `interact_entity` (`entity_id, hand, x/y/z, sneaking`),
`container_click` adds `changed` (predicted slot count) and `carried_item?/carried_count?`,
`custom_payload` carries `channel` and `brand?`, and `cookie_response` carries `key, size?`.
These go out too but cannot be built, because they are signed or belong to a phase without
a game connection:

<details open>
<summary>All 12 observe-only outgoing packets</summary>

| Name | Fields |
|------|--------|
| `chat` | `message, timestamp, salt, signed, offset` |
| `command_signed` | `command, timestamp, salt, signatures, offset` |
| `chat_session_update` | `session_id, expires_at` |
| `accept_code_of_conduct` | — |
| `finish_configuration` | — |
| `select_known_packs` | `packs (list)` |
| `intention` | `protocol_version, host, port, intention` |
| `hello` | `name, uuid` |
| `key` | — |
| `custom_query_answer` | `transaction_id, understood` |
| `login_acknowledged` | — |
| `status_request` | — |

</details>

**Incoming.**

<details open>
<summary>All 154 incoming packets</summary>

| Name | Fields |
|------|--------|
| `keep_alive` | `id` |
| `disconnect` | `reason` |
| `player_position_look` | `teleport_id, x, y, z, vel_x, vel_y, vel_z, yaw, pitch` |
| `entity_velocity` | `entity_id, vel_x, vel_y, vel_z` |
| `entity_position` | `entity_id, x, y, z, yaw, pitch, on_ground` |
| `entity_position_sync` | `entity_id, x, y, z, yaw, pitch, on_ground` |
| `entity_damage` | `entity_id, source_cause_id, source_direct_id, source_type, source_x/y/z?` — `source_type` is a damage type id |
| `damage_tilt` | `entity_id, yaw` |
| `health_update` | `health, food, saturation` |
| `explosion` | `x, y, z, radius, block_count, knockback_x/y/z?` — knockback only when you were pushed |
| `block_update` | `x, y, z, block` — block id |
| `select_slot` | `slot` |
| `open_screen` | `sync_id, kind, title` — `kind` is a menu type id like `"minecraft:generic_9x6"` |
| `container_content` | `sync_id, revision, size, items (list of {item, count}), carried_item, carried_count` — the server syncing every slot of a handler |
| `container_slot` | `sync_id, revision, slot, item, count` — a single slot update |
| `open_sign_editor` | `x, y, z, front` — cancel it and answer with `sign_update` to write a sign without the editor |
| `add_entity` | `entity_id, uuid, kind, x, y, z, vel_x/y/z, yaw, pitch, head_yaw, data` |
| `animate` | `entity_id, action` |
| `award_stats` | `stats (list of {stat, value})` |
| `block_changed_ack` | `sequence` |
| `block_destruction` | `breaker_id, x/y/z, progress` |
| `block_entity_data` | `x/y/z, kind, tag` — `tag` is SNBT |
| `block_event` | `x/y/z, type, data, block` |
| `boss_event` | `operation, id, title, progress, color, overlay, darken_screen, play_music, world_fog` — `operation` is `"add"` · `"remove"` · `"update_progress"` · `"update_name"` · `"update_style"` · `"update_properties"`; the other fields follow the operation |
| `bundle` | `packets (list of {name, ...})` — every packet of the bundle, decoded like a top-level one. Sub-packets do not fire on their own: match them here |
| `bundle_delimiter` | — |
| `change_difficulty` | `difficulty, locked` |
| `chunks_biomes` | `chunks (list of {x, z})` |
| `chunk_batch_finished` | `size` |
| `chunk_batch_start` | — |
| `clear_titles` | `reset` |
| `commands` | — |
| `command_suggestions` | `id, start, length, suggestions (list)` |
| `close_screen` | `sync_id` |
| `container_data` | `sync_id, property, value` |
| `cooldown` | `group, duration` |
| `custom_chat_completions` | `action, entries (list)` |
| `debug_block_value` | `x/y/z, subscription` |
| `debug_chunk_value` | `x, z, subscription` |
| `debug_entity_value` | `entity_id, subscription` |
| `debug_event` | `subscription` |
| `debug_sample` | `type, sample (list)` |
| `delete_chat` | `id` |
| `disguised_chat` | `message, chat_type, sender_name, target_name?` |
| `entity_event` | `entity_id, event` |
| `forget_level_chunk` | `x, z` |
| `game_event` | `event, value` — `event` like `"start_raining"`, `"change_game_mode"`, `"win_game"` |
| `game_rule_values` | `values (map)` — rule id → value |
| `game_test_highlight_pos` | `x/y/z, relative_x/y/z` |
| `initialize_border` | `center_x, center_z, old_size, size, lerp_time, max_size, warning_time, warning_blocks` |
| `level_chunk_with_light` | `x, z`, plus the [light fields](#light) |
| `level_event` | `type, x/y/z, data, global` |
| `level_particles` | `particle, x, y, z, spread_x, spread_y, spread_z, speed, count, override_limiter, always_show` |
| `light_update` | `x, z`, plus the [light fields](#light) |
| `login` | `entity_id, hardcore, dimensions (list), max_players, view_distance, simulation_distance, reduced_debug_info, show_death_screen, limited_crafting, online_mode, enforces_secure_chat, dimension, dimension_type, seed, game_mode, previous_game_mode?, debug, flat, portal_cooldown, sea_level, death_dimension?, death_x/y/z?` — `entity_id` is your own |
| `low_disk_space_warning` | — |
| `map_item_data` | `map_id, scale, locked, decorations? (list of {type, x, y, rotation, name?}), patch_x?, patch_y?, patch_width?, patch_height?` |
| `merchant_offers` | `sync_id, level, xp, show_progress, can_restock, offers (list of {item, count, cost_item, cost_count, cost_b_item, cost_b_count, uses, max_uses})` |
| `mount_screen_open` | `sync_id, columns, entity_id` |
| `move_entity_pos` | `entity_id, dx?, dy?, dz?, yaw?, pitch?, on_ground` — `dx/dy/dz` are block deltas |
| `move_entity_pos_rot` | `entity_id, dx?, dy?, dz?, yaw?, pitch?, on_ground` — `dx/dy/dz` are block deltas |
| `move_entity_rot` | `entity_id, dx?, dy?, dz?, yaw?, pitch?, on_ground` |
| `move_minecart_along_track` | `entity_id, steps (list of {x/y/z, vel_x/y/z, yaw, pitch, weight})` |
| `move_vehicle` | `x/y/z, yaw, pitch` |
| `open_book` | `hand` |
| `place_ghost_recipe` | `sync_id` |
| `player_abilities` | `invulnerable, flying, can_fly, instabuild, fly_speed, walk_speed` |
| `player_chat` | `sender, index, global_index, message, timestamp, salt, signed, unsigned_content?, chat_type, sender_name, target_name?` |
| `player_combat_end` | `duration` |
| `player_combat_enter` | — |
| `player_combat_kill` | `entity_id, message` |
| `player_info_remove` | `uuids (list)` |
| `player_info_update` | `actions (list), entries (list of {uuid, name?, listed, latency, game_mode?, display_name?, show_hat, list_order})` |
| `player_look_at` | `from_anchor, x, y, z, entity_id?` |
| `player_rotation` | `yaw, pitch, relative_yaw, relative_pitch` |
| `projectile_power` | `entity_id, power` |
| `recipe_book_add` | `replace, recipes (list of {recipe, notification, highlight})` |
| `recipe_book_remove` | `recipes (list)` |
| `recipe_book_settings` | `<book>_open, <book>_filtering` — one pair per book: `crafting`, `furnace`, `blast_furnace`, `smoker` |
| `remove_entities` | `entity_ids (list)` |
| `remove_mob_effect` | `entity_id, effect` |
| `reset_score` | `owner, objective?` |
| `respawn` | `keep_attributes, keep_entity_data, dimension, dimension_type, seed, game_mode, previous_game_mode?, debug, flat, portal_cooldown, sea_level, death_dimension?, death_x/y/z?` |
| `rotate_head` | `entity_id, head_yaw` |
| `section_blocks_update` | `blocks (list of {x/y/z, block})` |
| `select_advancements_tab` | `tab?` |
| `server_data` | `motd, has_icon` |
| `set_action_bar_text` | `text` |
| `set_border_center` | `x, z` |
| `set_border_lerp_size` | `old_size, size, lerp_time` |
| `set_border_size` | `size` |
| `set_border_warning_delay` | `delay` |
| `set_border_warning_distance` | `blocks` |
| `set_camera` | `entity_id` |
| `set_chunk_cache_center` | `x, z` |
| `set_chunk_cache_radius` | `radius` |
| `set_cursor_item` | `item, count` |
| `set_default_spawn_position` | `dimension, x/y/z, yaw, pitch` |
| `set_display_objective` | `slot, objective?` |
| `set_entity_data` | `entity_id, values (list of {id, value})` — `value` is the tracked value as a string |
| `set_entity_link` | `entity_id, holder_id` |
| `set_equipment` | `entity_id, slots (list of {slot, item, count})` |
| `set_experience` | `progress, total, level` |
| `set_objective` | `objective, method, display_name?, render_type?` — `method` is `0` add · `1` remove · `2` change |
| `set_passengers` | `entity_id, passengers (list)` |
| `set_player_inventory` | `slot, item, count` |
| `set_player_team` | `team, action?, player_action?, players (list), display_name?, prefix?, suffix?, name_tag_visibility?, collision_rule?, color?, options?` |
| `set_score` | `owner, objective, score, display?` |
| `set_simulation_distance` | `distance` |
| `set_subtitle_text` | `text` |
| `set_time` | `game_time, clocks (list of {clock, total_ticks, rate})` |
| `set_titles_animation` | `fade_in, stay, fade_out` |
| `set_title_text` | `text` |
| `sound` | `sound, category, x, y, z, volume, pitch, seed` |
| `sound_entity` | `sound, category, entity_id, volume, pitch, seed` |
| `start_configuration` | — |
| `stop_sound` | `sound?, category?` |
| `system_chat` | `text, overlay` |
| `tab_list` | `header, footer` |
| `tag_query` | `transaction_id, tag?` — `tag` is SNBT |
| `take_item_entity` | `item_entity_id, collector_id, amount` |
| `test_instance_block_status` | `status, size_x/y/z?` |
| `ticking_state` | `tick_rate, frozen` |
| `ticking_step` | `steps` |
| `update_advancements` | `reset, show, added (list), removed (list), progress (list)` |
| `update_attributes` | `entity_id, attributes (list of {id, base, modifiers})` |
| `update_mob_effect` | `entity_id, effect, amplifier, duration, visible, ambient, show_icon` |
| `update_recipes` | `item_sets` |
| `waypoint` | `operation, id` — `operation` is `"track"` · `"untrack"` · `"update"` |
| `clear_dialog` | — |
| `custom_payload` | `channel, brand?` |
| `custom_report_details` | `details (map)` |
| `ping` | `id` |
| `resource_pack_pop` | `id?` |
| `resource_pack_push` | `id, url, hash, required, prompt?` |
| `server_links` | `links (list of {type, url})` |
| `show_dialog` | `dialog` |
| `store_cookie` | `key, size` |
| `transfer` | `host, port` |
| `update_tags` | `registries (list)` |
| `cookie_request` | `key` |
| `code_of_conduct` | `text` |
| `finish_configuration` | — |
| `registry_data` | `registry, entries (list)` |
| `reset_chat` | — |
| `select_known_packs` | `packs (list)` |
| `update_enabled_features` | `features (list)` |
| `custom_query` | `transaction_id, channel` |
| `hello` | `server_id, should_authenticate` |
| `login_compression` | `threshold` |
| `login_disconnect` | `reason` |
| `login_finished` | `name, uuid` |
| `pong_response` | `time` |
| `status_response` | `description, online?, max?, version?, protocol?, enforces_secure_chat` |

</details>

## Light

`light_update` and `level_chunk_with_light` carry the chunk's light the way the server sent it.

| Field | Holds |
|-------|-------|
| `sky_mask`, `block_mask` | Section bits that come with light data |
| `empty_sky_mask`, `empty_block_mask` | Section bits whose light is all zero |
| `sky_light`, `block_light` | One 2048-byte string per bit of the matching mask, in the same order |

A mask is a list of bit numbers, and bit `i` is section `world:min_section() - 1 + i`: light
sections reach one past each end of the [chunk column](world.md#chunks). `sky_light[k]` is the
light of section bit `sky_mask[k]`. Each string packs one `0–15` level per block, local block
`x, y, z` at nibble `y * 256 + z * 16 + x`, low half of the byte first:

```lua
local function light_at(data, x, y, z)
    local nibble = y * 256 + z * 16 + x
    local byte = data:byte(bit32.rshift(nibble, 1) + 1)
    return bit32.band(bit32.rshift(byte, bit32.band(nibble, 1) * 4), 15)
end

local lit = {}

module:event("packet_receive", function(e)
    if e.name ~= "light_update" and e.name ~= "level_chunk_with_light" then return end
    for k, bit in ipairs(e.block_mask) do
        if light_at(e.block_light[k], 8, 8, 8) > 0 then
            lit[#lit + 1] = { x = e.x, z = e.z, section_bit = bit }
        end
    end
end)
```

:::caution
Packet events run on network threads. Collect data there and act in `tick`; never draw or
call `projection` from them.
:::
