From 6410cc00280a05f0d62ea5055a4fbe9d4ee9419b Mon Sep 17 00:00:00 2001 From: Algent Date: Thu, 27 Aug 2026 18:02:41 +0300 Subject: [PATCH] test: characterize multipart ordering and move lifecycle --- JAVA_MIGRATION.md | 20 +- JAVA_MIGRATION_CONSUMER_AUDIT.md | 7 +- JAVA_MIGRATION_HANDOFF.md | 14 +- .../test/MultipartHelperFunctionalTest.java | 22 +- .../TileMultipartLifecycleFunctionalTest.java | 207 ++++++++++++++++++ .../TileMultipartCharacterizationTest.java | 3 + 6 files changed, 258 insertions(+), 15 deletions(-) create mode 100644 src/functionalTest/java/codechicken/multipart/test/TileMultipartLifecycleFunctionalTest.java diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 55a3121..5ccc83a 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -23,15 +23,15 @@ No trustworthy tool will produce a maintainable Java port automatically. A decom ## Current status -The binary and source-level consumer audits are complete. Phase 0 still lacks broad NBT/packet fixtures and a +The binary and source-level consumer audits are complete. Phase 0 still lacks exact description-packet fixtures and a pre-optimization profile. The low-coupling Phase 3 queue is complete; before entering measured hot-path or generated tile work, the immediate milestone is the audit-derived compatibility gate below. That audit found and this branch has now repaired one existing regression: Schematica 1.12.6 reflects the original private `MultiPartRegistry$` field `codechicken$multipart$MultiPartRegistry$$typeMap` and casts it to a Scala mutable map. The companion again exposes that exact private field as a live view of the canonical Java `HashMap`, and a test performs -Schematica's reflective lookup. The remaining source-only member guards are also frozen; tile ordering and lifecycle -characterization is next. +Schematica's reflective lookup. The remaining source-only member guards and `TileMultipart` ordering/move lifecycle +are also frozen; compact mixed-part NBT and description-packet fixtures are next. Phase 1 has not properly started: GTNHLib is present but was added at `api` scope to satisfy a test compile rather than because a migration change needed it, and no Forge mod dependency was declared. Revisit that entry only when a @@ -303,7 +303,7 @@ Do this before another source conversion: - [x] Add exact member-level guards for GuideNH's `BlockMicroMaterial.block/meta` mixin fields, Et Futurum's mutable `ButtonPart.metaSideMap/sideMetaMap`, Iguana's `ItemSaw.harvestLevel`, and Galacticraft's name-only `registerMaterial` lookup. -- [ ] Freeze `TileMultipart` order and lifecycle observations: `parts`/`id` NBT, `partList`/`jPartList`, `partMap`, slot +- [x] Freeze `TileMultipart` order and lifecycle observations: `parts`/`id` NBT, `partList`/`jPartList`, `partMap`, slot rebinding, add/remove/replace callback order, moving the live tile, and `onMoved`. - [ ] Add compact mixed-part NBT and description-packet fixtures. Use representative fake parts in the existing test harness; do not add all downstream mods as test dependencies. @@ -394,7 +394,7 @@ The work should move from low to high risk while keeping a buildable mixed-langu Extensive tests are both possible and recommended here. Their purpose is not to prove that the current Scala behavior is ideal; it is to make its observable behavior explicit before translation, so every later difference is intentional. The repository now has a Java-8-compatible JUnit Jupiter suite and a small Forge integration runner. The current -baseline is 130 plain-JVM tests and 22 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest +baseline is 130 plain-JVM tests and 24 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest framework, so game-dependent coverage continues through the existing narrow Forge harness rather than a second test framework. @@ -611,3 +611,13 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec - Reproduced Galacticraft's name-only method scan and froze its dangerous assumption: exactly one public `MicroMaterialRegistry.registerMaterial` method may exist, it must accept `(IMicroMaterial, String)`, and `BlockMicroMaterial(Block, int)` must remain reflectively constructible. All 130 plain-JVM tests pass. +- Accepted the dedicated-server EULA locally in ignored `run/server/eula.txt`, enabling the Forge characterization + suite without adding a distributable acceptance file to the repository. +- Froze tile NBT order with two different built-in parts: the outer tile `id`, ordered `parts` entries and per-part + `id`/payload values survive reconstruction, and `jPartList` plus `partMap` reproduce the same order and slots. +- Froze add/remove callback order, list publication, slot binding and detachment in a live Forge world. The plain-JVM + replacement test also proves that a successful `canReplacePart` query does not mutate order or bindings. +- Reproduced MatterManipulator's live-tile relocation shape. Removing the tile first calls `onWorldSeparate` for each + part in list order; after reinsertion, `onMoved` calls each part's `onMoved` and default `onWorldJoin`, again in list + order. The original generated tile, part instances, order, slot map and tile references survive. +- The expanded baseline is 130 plain-JVM tests and 24 Forge server tests, all passing. diff --git a/JAVA_MIGRATION_CONSUMER_AUDIT.md b/JAVA_MIGRATION_CONSUMER_AUDIT.md index fa03329..028a587 100644 --- a/JAVA_MIGRATION_CONSUMER_AUDIT.md +++ b/JAVA_MIGRATION_CONSUMER_AUDIT.md @@ -444,6 +444,10 @@ Part list order is not an internal detail: The Java port may add faster indexed storage, but all published views, serialization, binding, ray-hit indices, and packet consumers must observe one consistent order. +Current-branch verification: `MultipartHelperFunctionalTest` freezes ordered two-part NBT reconstruction and slot +rebinding. `TileMultipartLifecycleFunctionalTest` freezes add/remove callbacks and the MatterManipulator-shaped live +move, including ordered `onWorldSeparate` before ordered `onMoved`/`onWorldJoin`, without replacing the generated tile. + ### Material IDs and packets Numeric material IDs are runtime registry identities used in descriptions and custom-part packets. Names are the @@ -488,7 +492,8 @@ behavior: 1. Keep the existing `+678` ABI fixture as the exact binary floor and correct failures before source cleanup. 2. Add the hidden-member reflection checks, especially Schematica's `typeMap` compatibility and GuideNH's mixin fields, before converting their owners. -3. Characterize `TileMultipart` list/map/slot behavior and move lifecycle before optimizing storage or iteration. +3. Keep the completed `TileMultipart` list/map/slot and live-move characterization green before optimizing storage or + iteration. 4. Characterize pass-through interfaces and ProjectRed's external Scala trait before changing either generator. 5. Add mixed NBT/packet fixtures before converting microblocks and Minecraft part implementations. 6. Treat Scala runtime removal as downstream coordination work after the Java port, not as a requirement of it. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index a8a3079..7852819 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -57,7 +57,7 @@ awk -F'"' '/ events = new ArrayList<>(); + RecordingPart first = new RecordingPart("first", 2, events); + RecordingPart second = new RecordingPart("second", 7, events); + + try { + TileMultipart tile = TileMultipart.addPart(world, ADD_REMOVE_POS, first); + assertEquals(Arrays.asList("first.bind", "first.onAdded", "first.onWorldJoin"), events); + assertEquals(Arrays.asList(first), tile.jPartList()); + assertSame(first, tile.partMap(2)); + + events.clear(); + TileMultipart sameTile = TileMultipart.addPart(world, ADD_REMOVE_POS, second); + assertSame(tile, sameTile); + assertEquals( + Arrays.asList("second.bind", "second.onAdded", "second.onWorldJoin", "first.changed:second"), + events); + assertEquals(Arrays.asList(first, second), tile.jPartList()); + assertSame(first, tile.partMap(2)); + assertSame(second, tile.partMap(7)); + + events.clear(); + assertSame(tile, tile.remPart(first)); + assertEquals( + Arrays.asList( + "first.preRemove", + "first.onRemoved", + "first.onWorldSeparate", + "second.changed:first"), + events); + assertEquals(Arrays.asList(second), tile.jPartList()); + assertNull(tile.partMap(2)); + assertSame(second, tile.partMap(7)); + assertNull(first.tile()); + } finally { + clear(world, ADD_REMOVE_POS); + } + } + + @Test + void movingTheLiveTileKeepsPartsSlotsAndGeneratedInterfaces() { + World world = world(); + clear(world, MOVE_FROM); + clear(world, MOVE_TO); + List events = new ArrayList<>(); + RecordingPart first = new RecordingPart("first", 3, events); + RecordingPart second = new RecordingPart("second", 8, events); + + try { + TileMultipart tile = TileMultipart.addPart(world, MOVE_FROM, first); + tile = TileMultipart.addPart(world, MOVE_FROM, second); + Block block = world.getBlock(MOVE_FROM.x, MOVE_FROM.y, MOVE_FROM.z); + int meta = world.getBlockMetadata(MOVE_FROM.x, MOVE_FROM.y, MOVE_FROM.z); + events.clear(); + + world.removeTileEntity(MOVE_FROM.x, MOVE_FROM.y, MOVE_FROM.z); + world.setBlockToAir(MOVE_FROM.x, MOVE_FROM.y, MOVE_FROM.z); + assertTrue(world.setBlock(MOVE_TO.x, MOVE_TO.y, MOVE_TO.z, block, meta, 3)); + tile.xCoord = MOVE_TO.x; + tile.yCoord = MOVE_TO.y; + tile.zCoord = MOVE_TO.z; + tile.validate(); + world.setTileEntity(MOVE_TO.x, MOVE_TO.y, MOVE_TO.z, tile); + tile.onMoved(); + + assertNull(world.getTileEntity(MOVE_FROM.x, MOVE_FROM.y, MOVE_FROM.z)); + assertSame(tile, world.getTileEntity(MOVE_TO.x, MOVE_TO.y, MOVE_TO.z)); + assertTrue(tile instanceof TSlottedTile); + assertEquals(Arrays.asList(first, second), tile.jPartList()); + assertSame(first, tile.partMap(3)); + assertSame(second, tile.partMap(8)); + assertSame(tile, first.tile()); + assertSame(tile, second.tile()); + assertEquals( + Arrays.asList( + "first.onWorldSeparate", + "second.onWorldSeparate", + "first.onMoved", + "first.onWorldJoin", + "second.onMoved", + "second.onWorldJoin"), + events); + assertEquals(MOVE_TO, first.movedTo); + assertEquals(MOVE_TO, second.movedTo); + } finally { + clear(world, MOVE_FROM); + clear(world, MOVE_TO); + } + } + + private static World world() { + World world = MinecraftServer.getServer().worldServers[0]; + world.getChunkFromBlockCoords(ADD_REMOVE_POS.x, ADD_REMOVE_POS.z); + return world; + } + + private static void clear(World world, BlockCoord pos) { + world.setBlockToAir(pos.x, pos.y, pos.z); + } + + private static final class RecordingPart extends TMultiPart implements TSlottedPart { + + private final String name; + private final int slot; + private final List events; + private BlockCoord movedTo; + + private RecordingPart(String name, int slot, List events) { + this.name = name; + this.slot = slot; + this.events = events; + } + + @Override + public String getType() { + return "mc_torch"; + } + + @Override + public int getSlotMask() { + return 1 << slot; + } + + @Override + public boolean doesTick() { + return false; + } + + @Override + public void bind(TileMultipart tile) { + super.bind(tile); + events.add(name + ".bind"); + } + + @Override + public void onAdded() { + events.add(name + ".onAdded"); + super.onAdded(); + } + + @Override + public void onWorldJoin() { + events.add(name + ".onWorldJoin"); + } + + @Override + public void onPartChanged(TMultiPart part) { + events.add(name + ".changed:" + ((RecordingPart) part).name); + } + + @Override + public void preRemove() { + events.add(name + ".preRemove"); + } + + @Override + public void onRemoved() { + events.add(name + ".onRemoved"); + super.onRemoved(); + } + + @Override + public void onWorldSeparate() { + events.add(name + ".onWorldSeparate"); + } + + @Override + public void onMoved() { + movedTo = new BlockCoord(tile()); + events.add(name + ".onMoved"); + super.onMoved(); + } + } +} diff --git a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java index faa4a1f..f002357 100644 --- a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java +++ b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java @@ -160,6 +160,9 @@ class TileMultipartCharacterizationTest { assertTrue(tile.canReplacePart(outgoing, new CountingPart("c"))); // A part already present, other than the outgoing one, cannot be added again. assertFalse(tile.canReplacePart(outgoing, other)); + assertEquals(Arrays.asList(outgoing, other), tile.jPartList(), "A replacement check must not mutate order"); + assertSame(tile, outgoing.tile()); + assertSame(tile, other.tile()); } @Test