Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Add a held item

This guide adds an item a character holds in its right hand. The item is an item kind: a data entry that names a model and the abilities the holder gets while it is equipped. The model is a Blockbench export whose recipe says where the hand grips it. An archetype’s loadout, or the /entity equip command, puts the item in an equipment slot.

The example builds on the flagship game. It uses the flagship’s main-hand slot, its fire action and its player archetype. Everything it uses is on main; read Limits and not yet before you plan more.

What you’ll build

A mod, guide-held-item, that gives every player a practice rifle. Pressing F fires one shot along the view, at most one every half second. The complete mod is tools/test-mods/guide-held-item:

FileWhat it adds
mod.toml, Cargo.toml, src/lib.rsthe package and an empty guest
data/abilities/practice_shot.ronthe ability the rifle grants
data/tags/practice_rifle.ronthe tag that spaces the shots
data/items/practice_rifle.ronthe item kind
assets/guide-held-item/models/weapons/practice_rifle.model.ronthe model recipe, with its grip
data/archetypes/player.update.rona patch that puts the rifle in each player’s main hand
locales/en/guide-held-item.ftlthe item’s and the ability’s names

The model’s GLB is not checked in. You export it from the rifle template (tools/templates/rifle/README.md) in step 4. Until you do, the item equips and fires, but no model is drawn.

Prerequisites

  • A checkout that builds and runs the flagship (getting started (docs/development/getting-started.md)), including the wasm32-unknown-unknown target and wasm-tools.
  • The mod CLI: cargo install --path tools/cargo-mod --locked.
  • For the model: Blockbench 5 with the Manifold plugin, tools/blockbench-manifold/manifold_model.js, loaded from File → Plugins → Load Plugin from File.
  • Background: equipment and abilities in the gameplay contract, and characters and held items in the model guide.

Steps

1. Create the mod

A mod is a directory with a mod.toml. Its data/ directory sits beside the manifest. data_format = 1 declares the registry format once for the whole mod.

[package]
id = "guide-held-item"
name = "guide-held-item"
display_name = "Guide: Held Item"
version = "0.1.0"
data_format = 1
wasm = "guide_held_item.component.wasm"

[assets]
dir = "assets"

[deps]
flagship-game = "0.1"

[deps] names the flagship, whose keys the data uses (its player archetype, main-hand slot, fire action, damage type and shot counter), and loads its data first.

The engine loads a game module from its component, so the mod has a guest, even though this one declares nothing. A bare #[mod_entry] exports only manifold:core/plugin-lifecycle:

//! The example mod of the "Add a held item" guide
//! (`docs/guides/add-a-held-item.md`).
//!
//! Everything it adds is content: an item kind, its ability and a tag under
//! `data/`, a patch of the flagship's player archetype that puts the item in
//! the main hand, and the item's model recipe under `assets/`. The engine
//! loads a game module from its component, so the mod has a guest; it
//! declares nothing.

use manifold_mod_substrate::prelude::*;

#[derive(Default)]
#[mod_entry]
pub struct GuideHeldItem;

impl Plugin for GuideHeldItem {
    const ID: &'static str = "guide-held-item";

    fn build(&self, _app: &mut ModApp) {}
}

cargo mod build runs cargo build -p <name>, so the Cargo.toml package name must equal mod.toml’s name, and the crate must be a member of the workspace it builds in. Every id the mod declares in data/ starts with its mod id, guide-held-item:.

2. Declare the ability

An item grants abilities; it does not act by itself. This ability is one shot per press:

// One shot per press of the flagship's fire action (F). The ability owns
// `state.cycling` while it runs and is blocked by it, so a press during the
// half-second wait is refused.
Ability(
    id: "guide-held-item:practice_shot",
    tags: ["manifold:ability.attack"],
    input: Some("flagship-game:fire"),
    owned_tags: ["guide-held-item:state.cycling"],
    blocked: (any: ["guide-held-item:state.cycling"]),
    predict: Presentation,
    root: Serial([
        Trace(origin: Eyes, range: 60.0, shape: Ray, hit: (
            damage: Some((damage_type: "flagship-game:rifle", base: "4")), max_targets: 1)),
        BumpCounter("flagship-game:shots"),
        Noise(loudness: 24.0),
        Wait("0.5s"),
    ]),
    ui: Some((name: Some("guide-held-item.ability.practice_shot.name"))),
)
  • input names an input action by key. A player activates the ability on the action’s press edge. The data stage does not check the key; an unknown or non-button action is a server log warning, and the ability never activates from input.
  • Trace is a lag-compensated ray from the caster’s eyes socket (the camera’s eye when the caster has no animated model). It hits characters with hit volumes and stops at terrain.
  • BumpCounter("flagship-game:shots") bumps the presentation counter the flagship’s player binding table reads as shots, as the M14 does.
  • predict: Presentation lets the owner’s client predict the counter bump.
  • The tag the ability owns must be declared:
// Held while the practice shot runs; its ancestors are declared implicitly.
Tag(id: "guide-held-item:state.cycling", replicate: Public)

The damage type, the presentation counter and the action are the flagship’s. A mod for another game uses that game’s, or declares its own.

3. Declare the item kind

// A held item: the model the hand shows and the ability it grants while
// equipped.
ItemKind(
    id: "guide-held-item:practice_rifle",
    name: Key("guide-held-item.item.practice_rifle.name"),
    model: "guide-held-item:weapons/practice_rifle",
    grants: (abilities: ["guide-held-item:practice_shot"]),
)
FieldMeaningChecked by the data stage
idthe item kind’s keyyes: in the mod’s namespace, unique
namea message key, Key(".."); defaults to <mod>.item.<id>.nameonly that it is a string
modelthe model id the held item showsno: models load after the data stage
grants.abilitiesgranted while equipped, removed (and cancelled) when unequippedyes: each must be an ability
grants.stat_modifiers, grants.damage_hooksstat modifiers and damage hooks while equippedyes
blockthe block a pickup puts in a block inventoryno

Any other top-level field is an error. The name key maps to the Fluent message item-practice_rifle-name in locales/en/guide-held-item.ftl.

4. Make the model

A held item’s model is an ordinary GLB and recipe at assets/<namespace>/models/<path>, so the id guide-held-item:weapons/practice_rifle is the file assets/guide-held-item/models/weapons/practice_rifle.glb. Model it in the weapon frame: -Z towards the muzzle, +Y up, origin at the grip, 16 Blockbench units per metre.

  1. Copy tools/templates/rifle/rifle.bbmodel and save the copy as practice_rifle.bbmodel. Keep it with your sources; it is not a runtime file.
  2. Open it in Blockbench. Its grip group is tagged as the grip socket (Manifold socket in the element panel) and sits where the right hand closes. Its grip_l group is where the left hand goes. Remodel freely, but keep both groups where the hands belong.
  3. Choose Tools → Export Manifold Model. Set the assets root to tools/test-mods/guide-held-item/assets and the id to guide-held-item:weapons/practice_rifle.
  4. The exporter writes the GLB and merges the recipe beside it. Tagged groups and socket_* locators become sockets, and timeline keyframes become events, each marked origin: Export(..). Fix any rows in Tools → Manifold Diagnostics.

The exporter never writes the item block; it is yours. It is what makes the model a held item:

    // A held item: the engine puts the `grip` node on the wearer's `hand_r`
    // socket, the category links the rifle animation layer, and the off hand
    // reaches for `grip_l`.
    item: Some((
        grip: "grip",
        category: "manifold:two_hand_rifle",
        support: Some("grip_l"),
    )),
  • The grip. The engine places the item so its grip node lands on the wearer’s hand_r socket: world(item) = world(socket) · grip⁻¹. Neither model needs to know the other. grip defaults to "grip".
  • The category. manifold:two_hand_rifle holds the item in hand_r, links the rifle layer manifold:animations/humanoid_blocky/rifle into the character’s weapon interface, and sends the left arm to the support node (default grip_l). The layer’s clips (rifle_idle, rifle_run, the aim sweep) belong to the character; a character without them plays its unarmed default (the weapon interface).
  • Diagnostics. A missing grip or support node is MODEL018. An unknown category is a MODEL015 warning, and the item then links no layer.

The committed recipe already holds the entries an export of the unmodified template produces, plus this item block. After the export, check the result (before it, validation reports the missing GLB):

cargo mod validate tools/test-mods/guide-held-item/mod.toml --model guide-held-item:weapons/practice_rifle

The mod’s .gitignore keeps the GLB out of the repository, as the template’s GLB is kept out.

5. Equip it from the player archetype

An archetype’s manifold-gameplay:loadout lists (slot, item kind) pairs. Each entity spawned from it starts with those items equipped and their abilities granted. The flagship’s player starts with the M14; this patch replaces its loadout:

// Every player starts with the practice rifle in the main hand instead of
// the M14. The binding table draws this slot's item at `hand_r`.
[
    Patch(target: "flagship-game:player", ops: [
        Set(path: ["components", "manifold-gameplay:loadout"], value: (items: [
            ("flagship-game:main_hand", "guide-held-item:practice_rifle"),
        ])),
    ]),
]
  • A *.update.ron file runs in the update stage, after every mod’s declarations, so it can patch another mod’s entry (data stage).
  • The data stage checks each pair: the slot must be an equipment slot and the item an item kind.
  • Use flagship-game:main_hand. The flagship’s player binding table reads only that slot as the held item. An item in another slot still grants its abilities, but nothing draws it.

6. Build and validate

cargo mod build --manifest tools/test-mods/guide-held-item/mod.toml

This validates the models (a recipe whose GLB is not exported yet is skipped), builds the guest and writes tools/test-mods/guide-held-item/guide-held-item.mod with data/, assets/ and locales/ packed in.

cargo mod validate tools/test-mods/guide-held-item/mod.toml --base game/flagship-game runs the data stage offline with the flagship’s data first. It resolves the item, ability, tag and patch, but it also reports the three flagship player archetype components that only the flagship’s guest registers (flagship-game:inventory, carried-stack and game-mode), because offline validation loads no guest (issue #530). A MODEL208 warning in the engine’s rifle layer is not yours either. The game itself is the full check: invalid data fails startup with every diagnostic.

Test it in game

Native singleplayer refuses a second game module (issue #528), so test on a shared dedicated world (docs/development/running-and-testing.md). Install the .mod in the server’s mods directory. The development server uses target/dev-session/data with the pack baseline:

mkdir -p target/dev-session/data/baseline/mods
cp tools/test-mods/guide-held-item/guide-held-item.mod target/dev-session/data/baseline/mods/
python scripts/dev-session.py server

The server loads the mod at boot; invalid data stops it and prints every diagnostic with its file and line. It then prints a RELAY_TICKET. Join from a native client with its own state directory, so that its mods directory does not hold a second copy of the mod (issue #528):

python scripts/dev-session.py native --state-dir ../manifold-client --ticket TICKET --test

MANIFOLD_STARTUP=ready means the client joined; the server delivered the mod at join. Browser clients need the delivery archive from cargo mod publish (Node and jco); cargo mod build has no browser bundle.

The console commands below need Admin. On a dedicated server, that is a signed-in client whose platform username is in the server’s saves/ops.ron (target/dev-session/data/baseline/saves/ops.ron for the development server); development identities (--username) are never Admin. Open the console with Backquote:

  1. /entity inspect @self lists guide-held-item:practice_rifle in flagship-game:main_hand.
  2. /entity def flagship-game:player --provenance shows the loadout written by data/archetypes/player.update.ron.
  3. /entity spawn flagship-game:boar at @look, aim at the boar and press F. The client logs each server-confirmed hit as a [gameplay] line, and /entity inspect @last shows the boar’s health falling. A second press within half a second is refused.
  4. /entity equip @self flagship-game:main_hand flagship-game:m14 swaps in the M14, and F fires it instead. /entity equip @self flagship-game:main_hand clear empties the hand; F then does nothing.

Seeing the item in a hand. The client draws a held item only on another character, posed by the player model. Three things must hold:

  • the GLB is exported (step 4);
  • the catalog has the flagship’s player model, flagship:player/marlton, a manifold:humanoid_blocky_v1 character with the rifle clips. The repository does not ship it (issue #533); supply one through MANIFOLD_ASSET_DEV_DIR (quick in-world test);
  • you look at someone else. Your own body is not drawn.

Join a second client, with its own --state-dir and the same ticket. Each player spawns with the rifle, so no command is needed.

Without the player model you can still check the model: /model spawn guide-held-item:weapons/practice_rifle places it, and the Asset Lab (F7) shows its grip and grip_l sockets. To iterate on the GLB without rebuilding the .mod, point MANIFOLD_ASSET_DEV_DIR at tools/test-mods/guide-held-item/assets; native builds reload changed models.

Limits and not yet

The capability map has the current status of each row below.

  • Item kinds are not inventory items. The flagship’s inventory and hotbar hold block ids. Item instances, stacks, containers and pickup are design only (docs/superpowers/specs/2026-09-30-items-overview-design.md) (items).
  • The hotbar does not change what is held. Only a loadout and set-equipment (/entity equip) do. Guests have no gameplay interface yet, so a mod cannot equip from code.
  • Equipment is not saved. A player rejoins with the archetype’s loadout; /entity equip lasts the session (gameplay).
  • One grip category and one profile. manifold:two_hand_rifle and manifold:humanoid_blocky_v1 are built in. Grip categories cannot be declared in data yet, and every held item is placed at hand_r (animation and models).
  • One drawn slot. The player binding table is the game’s (AppMetadata::player_bindings); the flagship’s reads flagship-game:main_hand.
  • No item icons or hit feedback. Item and ability names are message keys that no shipped UI shows yet, and hit markers and damage numbers reach the client but are not drawn (issue #532).
  • No first-person view. You never see your own held item.
  • No player or weapon model in the repository. The flagship’s flagship:player/marlton and flagship:weapons/m14 are local assets, so a stock checkout draws no player model and no held item (issue #533).
  • No native singleplayer. Native singleplayer refuses a second game module, and a native client that also has the mod installed refuses the delivered copy (issue #528; several game modules).
  • Offline validation. cargo mod validate --base game/flagship-game reports the flagship’s guest-declared components as unregistered (issue #530).
  • No hot reload of data. Rebuild the .mod and restart after a data change.
  • Browser singleplayer cannot load installed mods; browsers get extra mods only by delivery from a server.

See also