Items and slots
Give items by name, customize their components, and clear inventory slots without building buffers.
A Slot is the wire shape for one item stack: count, item ID, and the data components to add or remove. Six play packets carry it (ContainerSetSlot, ContainerSetContent, SetPlayerInventory, SetCursorItem, SetEquipment, MerchantOffers). Writing one by hand means knowing the 26.2 component names and their wire shapes. helpers.createSlot handles the shortcuts and validates the rest.
import { helpers } from "mc-protocol";
// Run on a server-side client connection already in play state.
await client.sendPacket("SetPlayerInventory", {
slotId: 0, // first hotbar slot
contents: helpers.createSlot("diamond_sword", {
name: { text: "Excalibur", color: "gold" },
lore: ["Forged in TypeScript"],
unbreakable: true,
glint: true,
}),
});
The result is a plain object, not a Buffer. Pass it straight to sendPacket; the Slot writer derives the added and removed component counts from the arrays.
createSlot(item, options?)
Pass an item name such as "diamond_sword" or "minecraft:diamond_sword". Numeric item IDs still work. The helper returns { itemCount, itemId, components, removeComponents }, with the name resolved to its 26.2 ID. Everything in options is optional.
| Option | Type | Default | Component it sets |
|---|---|---|---|
count |
integer, 1 to 2147483647 | 1 |
none (stack size) |
name |
SlotText |
unset | custom_name |
lore |
SlotText[] |
unset | lore (the array is copied) |
damage |
integer, 0 to 2147483647 | unset | damage |
unbreakable |
boolean |
unset | unbreakable: true adds it, false removes it, omitted leaves the item default |
glint |
boolean |
unset | enchantment_glint_override |
components |
Record<string, unknown> |
{} |
any component by codec name, see Advanced components |
removeComponents |
string[] |
[] |
components to strip from the item default |
Validation
createSlot throws before anything reaches the wire when:
- An item name is unknown, has the wrong case, or uses a namespace other than
minecraft(RangeError). - A numeric item ID,
count, ordamageis not an integer in0to2147483647(RangeError). countis0(RangeError, pointing you atemptySlot()).- A component name is not a string (
TypeError). - A component name is not in the 26.2
SlotComponentTypeenum (RangeError: Unknown enum value: …). - The same component is both added and removed (
RangeError: Cannot both add and remove component: …).
Names are checked against the bundled registry. Numeric IDs are checked for range, not registry membership. Stack-size and durability limits remain your responsibility; advanced component payloads are passed to the existing wire codec.
Shortcut precedence
Shortcut options always win over the same key in components:
namereplacescomponents.custom_name;lorereplacescomponents.lore;damagereplacescomponents.damage;glintreplacescomponents.enchantment_glint_override.unbreakable: truesetscomponents.unbreakable(its wire payload is empty, so the value isundefined).unbreakable: falsedeletesunbreakablefromcomponentsand adds it toremoveComponents. Because the shortcut removed the add first, this never trips the add-and-remove check on its own.unbreakableomitted touches nothing; whatever you put incomponentsorremoveComponentsstands.
emptySlot()
Returns { itemCount: 0 }. Use it to clear a slot; no item ID or components are written.
await client.sendPacket("SetPlayerInventory", { slotId: 0, contents: helpers.emptySlot() });
getItemId(name)
For packet fields that need a numeric item ID rather than a Slot, use the same lookup directly:
helpers.getItemId("diamond_sword"); // 964
helpers.getItemId("minecraft:diamond_sword"); // 964
helpers.getItemId("sulfur_cube_bucket"); // 1052
The library bundles all 1,537 vanilla item IDs from the official 26.2 / protocol 776 server’s reports/registries.json. No download or setup is needed at runtime. Names are case-sensitive; typos and modded namespaces throw instead of silently becoming air. "air" resolves to 0, but use emptySlot() when clearing an inventory slot.
These are item IDs, not the block state IDs that ChunkHelper.setBlock takes. They are pinned to 26.2, not interchangeable across Minecraft versions. The regeneration script records the official server download and checksum.
Text values
name and each lore line accept a SlotText: a plain string, or an NBT-compatible text component object such as { text: "Excalibur", color: "gold", italic: false }. Both are written with the NBT mapping described under TextComponent and NBT; a bare string becomes an NBT string tag, an object a compound.
Advanced components
Anything without a shortcut goes in components, keyed by the unnamespaced codec name (rarity, not minecraft:rarity). The value is the wire shape the 26.2 component writer expects. The full list of names and shapes is the SlotComponentType enum and writeSlotComponent switch in src/Core/Helpers/ProtocolTypes.ts.
function enchantedBooks(enchantmentId: number) {
return helpers.createSlot("enchanted_book", {
count: 16,
components: {
rarity: "rare", // enum: "common" | "uncommon" | "rare" | "epic", or its index
stored_enchantments: { enchantments: [{ id: enchantmentId, level: 3 }] }, // enchantment registry ID
max_stack_size: 16, // VarInt
},
removeComponents: ["repair_cost"],
});
}
Shapes worth knowing:
- Empty components (
unbreakable,creative_slot_lock,intangible_projectile) take no data; useundefined. - Enums (
rarity, and enum fields inside structs) accept either the string name or the numeric index when writing. - Structs are plain objects with the fields the writer reads, for example
tooltip_display: { hideTooltip: false, hiddenComponents: [] }orcustom_model_data: { floats: [], flags: [], strings: [], colors: [] }. - Nested slots use wrapper objects:
bundle_contentsandcontainertake{ contents: [helpers.createSlot("stone")] };charged_projectilestakes{ projectiles: [helpers.createSlot("arrow")] }.use_remaindertakes a slot directly. - NBT components (
custom_data,item_name) take any valuewriteNbthandles.
getItemId only resolves items. Other registry-backed IDs, such as enchantments and potions, still require their own registry data.
What you get back
helpers.createSlot("stone", { count: 2, name: "Hi", unbreakable: true });
// {
// itemCount: 2,
// itemId: 1,
// components: [
// { type: "custom_name", data: "Hi" },
// { type: "unbreakable", data: undefined },
// ],
// removeComponents: [],
// }
Slots decoded from incoming packets differ in two ways. The reader adds the wire counts (addedComponentCount, removedComponentCount), and every enum is returned as { value, translation }, so a decoded component’s type is { value: 6, translation: "custom_name" } rather than the bare string. The writer accepts either form, and also the componentsToAdd and componentsToRemove aliases, so a decoded slot can be sent back out unchanged.
Serverbound ContainerClick does not carry this type. It uses HashedSlot (itemId, itemCount, then { type, hash } pairs with no component payloads), which createSlot cannot build. SetCreativeModeSlot uses UntrustedSlot, which keeps the Slot layout but carries each component’s data as a length-prefixed raw Buffer, so createSlot output is not directly valid there either.
Types
All exported through the helpers namespace:
| Type | Shape |
|---|---|
Slot |
{ itemCount: 0 } or { itemCount: number; itemId: number; components: SlotComponent[]; removeComponents: { type: string }[] } |
SlotOptions |
the options table above |
SlotComponent |
{ type: string; data?: unknown } |
SlotText |
string | Record<string, unknown> |
Slot describes writer input: type is the string name. It is not the decoded shape, where type is the { value, translation } wrapper described above.
import type { helpers } from "mc-protocol";
function hotbar(items: helpers.Slot[]) { /* … */ }