2026-09-14 12:01:54 +02:00
|
|
|
# Writing and placing a custom part
|
|
|
|
|
|
|
|
|
|
[API index](../API.md) · [Factory registration](PART_REGISTRATION.md)
|
|
|
|
|
|
|
|
|
|
This guide connects the ordinary custom-part lifecycle from registration through placement, saved state, client
|
|
|
|
|
state and removal. The [compiling example](../../src/functionalTest/java/codechicken/multipart/examples/PartRegistrationExample.java)
|
|
|
|
|
uses one integer as representative state; replace its empty geometry and rendering with the capabilities your part
|
|
|
|
|
needs.
|
|
|
|
|
|
2026-10-15 16:39:36 +02:00
|
|
|
Existing part binaries remain supported; use the [release notes](../RELEASE_NOTES.md) when rebuilding source.
|
2026-09-14 12:01:54 +02:00
|
|
|
|
|
|
|
|
## 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:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
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](PART_REGISTRATION.md) 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:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
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](PART_LOADING.md) and [composite generation](COMPOSITE_GENERATION.md) before using them.
|