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:
| File | What it adds |
|---|---|
mod.toml, Cargo.toml, src/lib.rs | the package and an empty guest |
data/abilities/practice_shot.ron | the ability the rifle grants |
data/tags/practice_rifle.ron | the tag that spaces the shots |
data/items/practice_rifle.ron | the item kind |
assets/guide-held-item/models/weapons/practice_rifle.model.ron | the model recipe, with its grip |
data/archetypes/player.update.ron | a patch that puts the rifle in each player’s main hand |
locales/en/guide-held-item.ftl | the 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 thewasm32-unknown-unknowntarget andwasm-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"))),
)
inputnames 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.Traceis a lag-compensated ray from the caster’seyessocket (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 asshots, as the M14 does.predict: Presentationlets 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"]),
)
| Field | Meaning | Checked by the data stage |
|---|---|---|
id | the item kind’s key | yes: in the mod’s namespace, unique |
name | a message key, Key(".."); defaults to <mod>.item.<id>.name | only that it is a string |
model | the model id the held item shows | no: models load after the data stage |
grants.abilities | granted while equipped, removed (and cancelled) when unequipped | yes: each must be an ability |
grants.stat_modifiers, grants.damage_hooks | stat modifiers and damage hooks while equipped | yes |
block | the block a pickup puts in a block inventory | no |
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.
- Copy
tools/templates/rifle/rifle.bbmodeland save the copy aspractice_rifle.bbmodel. Keep it with your sources; it is not a runtime file. - Open it in Blockbench. Its
gripgroup is tagged as thegripsocket (Manifold socket in the element panel) and sits where the right hand closes. Itsgrip_lgroup is where the left hand goes. Remodel freely, but keep both groups where the hands belong. - Choose Tools → Export Manifold Model. Set the assets root to
tools/test-mods/guide-held-item/assetsand the id toguide-held-item:weapons/practice_rifle. - The exporter writes the GLB and merges the recipe beside it. Tagged
groups and
socket_*locators become sockets, and timeline keyframes become events, each markedorigin: 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
gripnode lands on the wearer’shand_rsocket:world(item) = world(socket) · grip⁻¹. Neither model needs to know the other.gripdefaults to"grip". - The category.
manifold:two_hand_rifleholds the item inhand_r, links the rifle layermanifold:animations/humanoid_blocky/rifleinto the character’s weapon interface, and sends the left arm to thesupportnode (defaultgrip_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
griporsupportnode 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.ronfile 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:
/entity inspect @selflistsguide-held-item:practice_rifleinflagship-game:main_hand./entity def flagship-game:player --provenanceshows the loadout written bydata/archetypes/player.update.ron./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 @lastshows the boar’s health falling. A second press within half a second is refused./entity equip @self flagship-game:main_hand flagship-game:m14swaps in the M14, and F fires it instead./entity equip @self flagship-game:main_hand clearempties 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, amanifold:humanoid_blocky_v1character with the rifle clips. The repository does not ship it (issue #533); supply one throughMANIFOLD_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 equiplasts the session (gameplay). - One grip category and one profile.
manifold:two_hand_rifleandmanifold:humanoid_blocky_v1are built in. Grip categories cannot be declared in data yet, and every held item is placed athand_r(animation and models). - One drawn slot. The player binding table is the game’s
(
AppMetadata::player_bindings); the flagship’s readsflagship-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/marltonandflagship:weapons/m14are 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-gamereports the flagship’s guest-declared components as unregistered (issue #530). - No hot reload of data. Rebuild the
.modand restart after a data change. - Browser singleplayer cannot load installed mods; browsers get extra mods only by delivery from a server.
See also
- Gameplay: equipment and abilities
- Model assets: characters and held items, the weapon interface, exporting from Blockbench, binding tables
- Animated characters (
docs/architecture/animated-characters.md) - Data stage
- Engine capabilities
- Rifle template (
tools/templates/rifle/README.md)