- drop per-guide Validation sections; manual gates stay in MANUAL_CHECKS - drop audit/provenance lines (reference checkouts, dated refreshes, commit pins) - state version status and Scala-free compile check once in API.md - replace API.md status prose with link to migration status - fix RELEASE_NOTES dangling sentence - rewrite README for GTNH fork and Java port
4.6 KiB
Writing and placing a custom part
API index · Factory registration
This guide connects the ordinary custom-part lifecycle from registration through placement, saved state, client state and removal. The compiling example uses one integer as representative state; replace its empty geometry and rendering with the capabilities your part needs.
Existing part binaries remain supported; use the release notes when rebuilding source.
Register and construct
Give every published part type a stable persistent ID and register its factory during common preInit or init, on
both client and server:
MultiPartRegistry.registerPartFactory(new PartRegistrationExample(), "yourmod:part");
The factory returns a fresh, unbound part. It should select the implementation, not finish loading it: FMP calls
load after the NBT factory path and readDesc after the packet factory path. Preserve existing IDs and packet
prefixes when migrating. See factory registration for payload ownership and failure ordering.
A basic part extends TMultiPart, returns its registered ID from getType(), and overrides only the capabilities and
callbacks it needs. The base class ticks by default, so a part without per-tick logic should return false from
doesTick() as the example does. Add collision/ray-trace boxes, drops, rendering, slots, redstone or other capability
interfaces according to the part's real behavior.
Place on the server
Choose the target from the item or interaction, then check and place on the server:
TileMultipart tile = PartRegistrationExample.place(world, pos, initialValue);
if (tile == null) {
return false;
}
The example returns null on the client or when TileMultipart.canPlacePart rejects the candidate. A successful
TileMultipart.addPart installs or updates the generated container, binds the part, calls onAdded, notifies the
world and clients, and returns the current tile. Retain that return value: adding a capability can replace a previous
tile instance. A successful canPlacePart is only a check; do not mutate inventory or other durable state until
addPart succeeds.
Persist and synchronize state
Use separate callbacks for disk and network state:
| Purpose | Write | Read | When |
|---|---|---|---|
| Saved server state | save(NBTTagCompound) |
load(NBTTagCompound) |
World save and server reconstruction |
| Initial client state | writeDesc(MCDataOutput) |
readDesc(MCDataInput) |
Chunk description and part addition |
| Later client changes | sendDescUpdate() |
Base read calls readDesc |
Buffered part update |
The example's setValue is server-owned and performs the three actions needed because its value is assumed to affect
all three concerns:
sendDescUpdate();
tile().notifyPartChange(this);
tile().markDirty();
sendDescUpdate synchronizes the description payload; the base client read then calls readDesc and marks the tile
for rendering. notifyPartChange calls sibling-part callbacks and updates the block, neighbors and lighting.
markDirty requests persistence. Use only the calls the changed state requires, but keep packet reads exactly
symmetric with writes. Run authoritative mutation on the server; the client applies network state in readDesc or a
custom read implementation.
Lifecycle callbacks
onAdded / onRemoved describe placement and removal. Their base implementations call onWorldJoin /
onWorldSeparate. Server chunk load/unload and movement can call the world-level hooks again, while initial client
reconstruction calls onWorldJoin without onAdded. Put repeatable external-network registration in the world-level
hooks, and one-time placement/removal effects in the narrower hooks. Preserve the base call when overriding a narrow
hook unless deliberately replacing its world-level behavior.
preRemove runs before the part leaves storage and before its binding is cleared. Remove on the server and retain the
returned tile because dropping the last capability can replace the container, while removing the final part returns
null after the block is removed:
TileMultipart current = part.tile();
TileMultipart replacement = current.remPart(part);
Ordinary placement and removal already perform container synchronization and notifications. The generation,
setPartList and loadPartList APIs are for controlled reconstruction and have different ownership rules; see
part loading and composite generation before using them.