mirror of
https://github.com/GTNewHorizons/ForgeMultipart.git
synced 2026-10-26 14:24:01 +00:00
docs: add custom part lifecycle guide
This commit is contained in:
1 parent
7a9b05c6b1
commit
bf4343d1fc
4 files changed
+209
-4
No files matched your search
+1
-2
@@ -31,6 +31,7 @@ Existing reflective binaries remain supported until consumer release and pack ad
|
||||
|
||||
| Task | API and guide |
|
||||
| --- | --- |
|
||||
| Define, place, persist, synchronize and remove a custom part | `TMultiPart`, `canPlacePart`, `addPart`, `remPart` — [end-to-end custom-part lifecycle](api/CUSTOM_PARTS.md) |
|
||||
| Register block converters | `registerConverter(IPartConverter)` — [registration, state ownership and committed lifecycle](api/BLOCK_CONVERTERS.md) |
|
||||
| Register part factories during mod initialization | `registerPartFactory(IPartFactory2, String...)` — [factory timing, payload ownership and migration](api/PART_REGISTRATION.md) |
|
||||
| Find the factory registered for a part type | `getPartFactory(String)` — [lookup ownership and Schematica reflection migration](api/FACTORY_LOOKUP.md) |
|
||||
@@ -65,8 +66,6 @@ compile classpath for overload resolution, as the [loading guide](api/PART_LOADI
|
||||
|
||||
| Area | Starting points |
|
||||
| --- | --- |
|
||||
| Define a custom part | Extend [TMultiPart](../src/main/scala/codechicken/multipart/TMultiPart.java); implement the required capability interfaces. The [built-in parts](../src/main/scala/codechicken/multipart/minecraft) show Java implementations |
|
||||
| Find, place and remove parts | [TileMultipart](../src/main/scala/codechicken/multipart/TileMultipart.java): `getTile`, `canPlacePart`, `addPart`, `remPart`. Retain the tile returned by changes because generated capabilities can replace the instance |
|
||||
| Construct a server composite tile from parts, or restore saved multipart NBT | [MultipartHelper](../src/main/scala/codechicken/multipart/MultipartHelper.java): `createTileFromParts(Iterable)`, `createTileFromNBT(World, NBTTagCompound)` |
|
||||
| Register microblock materials | [MicroMaterialRegistry](../src/main/scala/codechicken/microblock/MicroMaterialRegistry.java) and [BlockMicroMaterial](../src/main/scala/codechicken/microblock/BlockMicroMaterial.java) |
|
||||
| Register generated tile traits or pass-through interfaces | [MultipartGenerator](../src/main/scala/codechicken/multipart/MultipartGenerator.java); see [custom tile-trait authoring](api/CUSTOM_TILE_TRAITS.md) and [safe capability access](api/TILE_TRAIT_ACCESS.md) |
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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.
|
||||
|
||||
The APIs shown here are available on the `algent/java` branch and do not yet have a minimum published dependency
|
||||
version. Existing part binaries remain supported; use the [release notes](../RELEASE_NOTES.md) 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:
|
||||
|
||||
```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.
|
||||
|
||||
## Validation boundary
|
||||
|
||||
`compileJavaApiExamples` compiles this example against the packaged dev jar with Java 8 and without Scala libraries.
|
||||
The Forge functional suite covers registration, placement, binding, NBT/description state, updates and final removal.
|
||||
Real geometry, interaction and rendering still need a physical-client check for the implementing mod.
|
||||
+65
-2
@@ -1,13 +1,17 @@
|
||||
package codechicken.multipart.examples;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import codechicken.lib.data.MCDataInput;
|
||||
import codechicken.lib.data.MCDataOutput;
|
||||
import codechicken.lib.vec.BlockCoord;
|
||||
import codechicken.multipart.MultiPartRegistry;
|
||||
import codechicken.multipart.MultiPartRegistry.IPartFactory2;
|
||||
import codechicken.multipart.TMultiPart;
|
||||
import codechicken.multipart.TileMultipart;
|
||||
|
||||
/** Compiling example for docs/api/PART_REGISTRATION.md; geometry and gameplay callbacks belong to the real part. */
|
||||
/** Compiling example for the part-registration and custom-part lifecycle guides. */
|
||||
public final class PartRegistrationExample implements IPartFactory2 {
|
||||
|
||||
public static final String PART_TYPE = "yourmod:part";
|
||||
@@ -27,11 +31,70 @@ public final class PartRegistrationExample implements IPartFactory2 {
|
||||
return new ExamplePart();
|
||||
}
|
||||
|
||||
private static final class ExamplePart extends TMultiPart {
|
||||
/** Call from the server-side item/action after choosing the target position. */
|
||||
public static TileMultipart place(World world, BlockCoord pos, int value) {
|
||||
ExamplePart part = new ExamplePart(value);
|
||||
if (world.isRemote || !TileMultipart.canPlacePart(world, pos, part)) {
|
||||
return null;
|
||||
}
|
||||
return TileMultipart.addPart(world, pos, part);
|
||||
}
|
||||
|
||||
public static final class ExamplePart extends TMultiPart {
|
||||
|
||||
private int value;
|
||||
|
||||
public ExamplePart() {}
|
||||
|
||||
public ExamplePart(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return PART_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doesTick() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Example for state that affects persistence, clients, sibling parts and rendering. */
|
||||
public void setValue(int value) {
|
||||
if (this.value == value) {
|
||||
return;
|
||||
}
|
||||
this.value = value;
|
||||
if (world() != null && !world().isRemote) {
|
||||
sendDescUpdate();
|
||||
tile().notifyPartChange(this);
|
||||
tile().markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(NBTTagCompound tag) {
|
||||
tag.setInteger("value", value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(NBTTagCompound tag) {
|
||||
value = tag.getInteger("value");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDesc(MCDataOutput packet) {
|
||||
packet.writeInt(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readDesc(MCDataInput packet) {
|
||||
value = packet.readInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -2,6 +2,7 @@ package codechicken.multipart.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
@@ -14,17 +15,22 @@ import java.util.NoSuchElementException;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import codechicken.lib.data.MCDataInput;
|
||||
import codechicken.lib.packet.PacketCustom;
|
||||
import codechicken.lib.vec.BlockCoord;
|
||||
import codechicken.multipart.MultiPartRegistry;
|
||||
import codechicken.multipart.MultiPartRegistry$;
|
||||
import codechicken.multipart.MultiPartRegistry.IPartFactory;
|
||||
import codechicken.multipart.MultiPartRegistry.IPartFactory2;
|
||||
import codechicken.multipart.TMultiPart;
|
||||
import codechicken.multipart.TileMultipart;
|
||||
import codechicken.multipart.examples.PartRegistrationExample;
|
||||
import codechicken.multipart.examples.PartRegistrationExample.ExamplePart;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import cpw.mods.fml.common.ModContainer;
|
||||
import scala.collection.JavaConversions;
|
||||
@@ -33,6 +39,8 @@ import scala.runtime.AbstractFunction2;
|
||||
|
||||
class PartRegistrationFunctionalTest {
|
||||
|
||||
private static final BlockCoord EXAMPLE_POS = new BlockCoord(42, 200, 32);
|
||||
|
||||
private static final List<BiConsumer<IPartFactory2, String[]>> ENTRIES = Arrays.asList(
|
||||
(factory, types) -> MultiPartRegistry.registerParts(factory, types),
|
||||
(factory, types) -> MultiPartRegistry$.MODULE$.registerParts(factory, types),
|
||||
@@ -109,6 +117,43 @@ class PartRegistrationFunctionalTest {
|
||||
assertEquals(17, packet.readUByte(), "This example has no constructor discriminator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void javaExamplePlacesPersistsSynchronizesAndRemovesState() {
|
||||
World world = MinecraftServer.getServer().worldServers[0];
|
||||
world.getChunkFromBlockCoords(EXAMPLE_POS.x, EXAMPLE_POS.z);
|
||||
world.setBlockToAir(EXAMPLE_POS.x, EXAMPLE_POS.y, EXAMPLE_POS.z);
|
||||
try {
|
||||
TileMultipart tile = PartRegistrationExample.place(world, EXAMPLE_POS, 7);
|
||||
assertNotNull(tile);
|
||||
ExamplePart part = (ExamplePart) tile.jPartList().get(0);
|
||||
assertSame(tile, part.tile());
|
||||
assertEquals(7, part.value());
|
||||
assertFalse(part.doesTick());
|
||||
|
||||
part.setValue(23);
|
||||
assertEquals(23, part.value());
|
||||
|
||||
NBTTagCompound saved = new NBTTagCompound();
|
||||
tile.writeToNBT(saved);
|
||||
TileMultipart loaded = TileMultipart.createFromNBT(saved);
|
||||
assertNotNull(loaded);
|
||||
assertEquals(23, ((ExamplePart) loaded.jPartList().get(0)).value());
|
||||
|
||||
PacketCustom description = new PacketCustom("test", 1);
|
||||
part.writeDesc(description);
|
||||
ExamplePart client = new ExamplePart();
|
||||
client.readDesc(new PacketCustom(description.getByteBuf().copy()));
|
||||
assertEquals(23, client.value());
|
||||
|
||||
TileMultipart current = part.tile();
|
||||
assertNull(current.remPart(part), "Removing the last part leaves no replacement tile");
|
||||
assertNull(part.tile());
|
||||
assertTrue(world.isAirBlock(EXAMPLE_POS.x, EXAMPLE_POS.y, EXAMPLE_POS.z));
|
||||
} finally {
|
||||
world.setBlockToAir(EXAMPLE_POS.x, EXAMPLE_POS.y, EXAMPLE_POS.z);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationIsLazyOwnedByTheActiveModAndDoesNotRetainTheArray() {
|
||||
assertEquals("ForgeMultipartTests", owner.getModId());
|
||||
|
||||
Reference in new issue
Block a user