refactor: port multipart event handler

Preserve event-bus identity and the annotated singleton surface.
This commit is contained in:
Algent committed 2026-08-30 21:24:49 +02:00
1 parent e9f244fefa
commit 4cd63eb3c8
7 files changed
+194 -95

No files matched your search

+7
View File
@@ -755,3 +755,10 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec
Scala bytecode and allowing virtual resolution to reach the server implementation.
- All 158 plain-JVM tests and all 81 Java 8 Forge dedicated-server tests pass. Mod lifecycle dispatch is startup-only,
so it is not a meaningful focused throughput or allocation target.
- Characterized `MultipartEventHandler` against untouched Scala. Two plain-JVM cases freeze both singleton types, all
twelve public event methods, priorities and the client-only highlight boundary; three Forge cases freeze companion
registration on both buses, chunk load/unload cleanup, queued watches and END-phase tick dispatch.
- Ported the singleton to a Java facade/companion pair with unchanged public names, descriptors and annotations. The
proxy now names `MultipartEventHandler$.MODULE$` explicitly, preserving the exact object registered on both buses.
- Server ticking still passes the configuration manager's live player list through Scala's Java-list buffer adapter;
no copy or new traversal was introduced. All 160 plain-JVM and all 84 Java 8 Forge dedicated-server tests pass.
+46
View File
@@ -1829,3 +1829,49 @@ exactly `MultipartMod.class` and `MultipartMod$.class`.
startup phase, reaches server-started tests, and shuts down cleanly.
- Raw `javap` comparison confirms identical public names/descriptors for `MultipartMod`, `MultipartMod$` and
`MultipartPH`; reflection confirms identical FML annotations.
## 2026-08-30 — MultipartEventHandler Java port
The ForgeMultipart event handler is now an annotated Java facade/companion pair. The server proxy registers
`MultipartEventHandler$.MODULE$` explicitly on both the FML and MinecraftForge event buses, preserving the same target
identity as the Scala object expression.
### Observable behavior
No known runtime divergence. Chunk-data loading still runs at `HIGHEST` priority and delegates to
`MultipartSaveLoad.loadTiles`. World unload still removes server packet state, clears the client tile cache for remote
worlds, and otherwise clears the loading-world reference. Watch and unwatch events still update the two-stage chunk
watcher state, and only the END server-tick phase flushes packets and promotes queued watches.
The highlight handler retains both `@SubscribeEvent` and `@SideOnly(CLIENT)`. Its null, block-hit and multipart-tile
guards remain short-circuiting; a successful `BlockMultipart.drawHighlight` call still cancels the event. This client
rendering path remains on the manual checklist because the automated harness is a dedicated server.
Server ticking still wraps the configuration manager's live `java.util.List` with Scala's `JavaConverters` buffer
adapter before calling `MultipartSPH.onTickEnd`. This preserves the original `scala.collection.Seq` contract without
copying the player list or adding a new traversal.
### ABI, annotations and source compatibility
`MultipartEventHandler` retains the exact six public static methods. `MultipartEventHandler$` retains `MODULE$` and
the matching six public instance methods. Every method still carries `@SubscribeEvent`; only `tileEntityLoad` uses
`HIGHEST`, the other five use `NORMAL`, none receive canceled events, and only `drawBlockHighlight` carries
`@SideOnly(CLIENT)`.
The in-repo Scala registrations now spell the handler value as `MultipartEventHandler$.MODULE$`. The registered
runtime identity is unchanged, and no frozen binary or audited source consumer names either handler class.
Accepted divergence: the Scala signature/marker attributes disappear, and the static Java facade gains only a private
constructor. These do not change the runtime annotation data or callable public surface. The emitted class list remains
exactly `MultipartEventHandler.class` and `MultipartEventHandler$.class`.
### Validation
- `MultipartEventHandlerCharacterizationTest`: 2 plain-JVM tests, unchanged from the Scala baseline.
- `MultipartEventHandlerFunctionalTest`: 3 Forge tests, unchanged from the Scala baseline.
- Complete plain-JVM suite: 160 tests, 0 failures, 0 errors.
- Java 8 Forge dedicated-server suite: 84 tests, 0 failures, 0 errors.
- The Forge run confirms the companion identity on both event buses and exercises load/unload, watch/unwatch and both
server-tick phases before shutting down cleanly.
- Raw `javap` comparison confirms identical public fields, method names and descriptors for both handler types;
reflection confirms identical event annotations, and both jars emit exactly the same two handler classes.
+15 -9
View File
@@ -12,8 +12,8 @@ what breaks, and what is left. The other documents hold the reasoning.
| `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client |
| `JAVA_MIGRATION_PROFILE.md` | The focused baseline, first measured result, findings, and exact rerun command |
Branch: `algent/java`. Base: `master`. 89 commits including separate characterization and port commits for
`MultipartMod`.
Branch: `algent/java`. Base: `master`. 91 commits including separate characterization and port commits for
`MultipartEventHandler`.
## The one rule that matters
@@ -67,7 +67,7 @@ awk -F'"' '/<testsuite /{t+=$4;f+=$8;e+=$10} END{print "tests="t" failures="f" e
grep -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' run/server/junit-out/TEST-*.xml
```
Current baseline: **158 plain-JVM tests, 81 Forge server tests, all passing at their last completed runs.**
Current baseline: **160 plain-JVM tests, 84 Forge server tests, all passing at their last completed runs.**
### ABI diff against the reference
@@ -235,7 +235,8 @@ All eight load-bearing `$class` helpers from the inventory, both registries, and
`MultiPartRegistry`, `TileMultipart`, `TMultiPart`, `TickScheduler`, `BlockMultipart`, the complete
`IRedstonePart`/`RedstoneInteractions` unit, `MicroRecipe`, and the `TPartialOcclusionTile`, `TSlottedTile`, and
`TRedstoneTile`, `TTileChangeTile`, `TFluidHandlerTile`, `TIInventoryTile`/`JInventoryTile`, `TileMultipartClient`, and
`TRandomDisplayTickTile` Java-trait ports, plus `MultipartCompatiblity`/`MCPCCompatModule` and `MultipartMod`.
`TRandomDisplayTickTile` Java-trait ports, plus `MultipartCompatiblity`/`MCPCCompatModule`, `MultipartMod`, and
`MultipartEventHandler`.
Plus the six marker interfaces: `TSlottedPart`, `IRandomDisplayTick`, `INeighborTileChange`, `TRandomUpdateTick`,
`ISidedHollowConnect`, `IMicroMaterialRender`, plus `MultipartHelper`, `TileCache`, `PacketScheduler` and the `ControlKeyModifer` pair.
@@ -250,7 +251,7 @@ across the port. It is also where the two shim constraints in the gotchas list w
`rayTraceAll`'s index production is now characterized, closing the gap the read-path cleanup left: the index written
into `ExtendedMOP.data` is what `reduceMOP` hands back to every click, activate, harvest and pick block.
97 Java files, 36 Scala files, ~5,241 Scala lines left (non-blank; that is the metric this figure has always used).
99 Java files, 35 Scala files, ~5,164 Scala lines left (non-blank; that is the metric this figure has always used).
## What is left, and in what order
@@ -342,6 +343,11 @@ methods and `MultipartMod$.MODULE$` retain their exact public descriptors and an
as the mod instance, and `MultipartPH.channel()` still returns that same companion with its original descriptor. Two
plain-JVM and two Forge tests freeze the shape, identity, completed lifecycle and server-stop cleanup.
`MultipartEventHandler` is now Java. Both singleton class names, all twelve public event methods, every event priority,
and the client-only highlight boundary retain their exact shape. The proxy now names `MultipartEventHandler$.MODULE$`
explicitly, and that same companion remains registered on both event buses. Two plain-JVM and three Forge tests freeze
the ABI, annotations, bus identity, load/unload cleanup, chunk watches and END-phase tick dispatch.
**Medium.** The `handler` packages on both sides, `ItemMicroPart`,
`MicroblockPlacement`, `PlacementGrids`, `BlockMicroMaterial`, `MissingMicroMaterial`, `ConfigContent`,
`DefaultContent`.
@@ -350,10 +356,10 @@ plain-JVM and two Forge tests freeze the shape, identity, completed lifecycle an
narrow generator relaxation: Java-trait parent linearization, explicit field-accessor recognition, and exclusion of
transient runtime caches from generated copying.
Take `multipart/handler/MultipartEventHandler.scala` next. Preserve both singleton types and event-bus identity; freeze
every `@SubscribeEvent` priority plus the client-only highlight annotation before changing it. Exercise load/unload,
chunk watch/unwatch and end-of-server-tick dispatch where the Forge harness can observe them, and keep the
characterization and port commits separate.
Take `microblock/handler/MicroblockMod.scala` next. Reuse the annotated facade/companion pattern, but also freeze the
two `angelicaCompat` accessors alongside `MODULE$`, the exact `@Mod` metadata and all five handler annotations. Confirm
that FML still uses the companion and completes the server lifecycle. Call `MicroblockProxy$.MODULE$` directly because
its client overrides carry `@SideOnly`, and keep the characterization and port commits separate.
**Phase 6/7, last.** `Microblock` and the microblock shape hierarchy, `MicroblockGenerator`, `MultipartGenerator`, and
all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-class fixtures before touching it.
@@ -0,0 +1,76 @@
package codechicken.multipart.handler;
import java.util.List;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.MovingObjectPosition.MovingObjectType;
import net.minecraftforge.client.event.DrawBlockHighlightEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkWatchEvent;
import net.minecraftforge.event.world.WorldEvent;
import codechicken.multipart.BlockMultipart;
import codechicken.multipart.TileCache;
import codechicken.multipart.TileMultipart;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import scala.collection.JavaConverters;
public final class MultipartEventHandler$ {
public static final MultipartEventHandler$ MODULE$ = new MultipartEventHandler$();
private MultipartEventHandler$() {}
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void tileEntityLoad(ChunkDataEvent.Load event) {
MultipartSaveLoad.loadTiles(event.getChunk());
}
@SubscribeEvent
public void worldUnLoad(WorldEvent.Unload event) {
MultipartSPH$.MODULE$.onWorldUnload(event.world);
if (event.world.isRemote) {
TileCache.clear();
} else {
MultipartSaveLoad.loadingWorld_$eq(null);
}
}
@SubscribeEvent
public void chunkWatch(ChunkWatchEvent.Watch event) {
MultipartSPH$.MODULE$.onChunkWatch(event.player, event.chunk);
}
@SubscribeEvent
public void chunkUnWatch(ChunkWatchEvent.UnWatch event) {
MultipartSPH$.MODULE$.onChunkUnWatch(event.player, event.chunk);
}
@SubscribeEvent
@SideOnly(Side.CLIENT)
public void drawBlockHighlight(DrawBlockHighlightEvent event) {
if (event.target != null && event.target.typeOfHit == MovingObjectType.BLOCK
&& event.player.worldObj.getTileEntity(
event.target.blockX,
event.target.blockY,
event.target.blockZ) instanceof TileMultipart
&& BlockMultipart
.drawHighlight(event.player.worldObj, event.player, event.target, event.partialTicks)) {
event.setCanceled(true);
}
}
@SubscribeEvent
@SuppressWarnings("unchecked")
public void serverTick(TickEvent.ServerTickEvent event) {
if (event.phase == TickEvent.Phase.END) {
List<EntityPlayerMP> players = MinecraftServer.getServer().getConfigurationManager().playerEntityList;
MultipartSPH$.MODULE$.onTickEnd(JavaConverters.asScalaBufferConverter(players).asScala());
}
}
}
@@ -0,0 +1,48 @@
package codechicken.multipart.handler;
import net.minecraftforge.client.event.DrawBlockHighlightEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkWatchEvent;
import net.minecraftforge.event.world.WorldEvent;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
public final class MultipartEventHandler {
private MultipartEventHandler() {}
@SubscribeEvent(priority = EventPriority.HIGHEST)
public static void tileEntityLoad(ChunkDataEvent.Load event) {
MultipartEventHandler$.MODULE$.tileEntityLoad(event);
}
@SubscribeEvent
public static void worldUnLoad(WorldEvent.Unload event) {
MultipartEventHandler$.MODULE$.worldUnLoad(event);
}
@SubscribeEvent
public static void chunkWatch(ChunkWatchEvent.Watch event) {
MultipartEventHandler$.MODULE$.chunkWatch(event);
}
@SubscribeEvent
public static void chunkUnWatch(ChunkWatchEvent.UnWatch event) {
MultipartEventHandler$.MODULE$.chunkUnWatch(event);
}
@SubscribeEvent
@SideOnly(Side.CLIENT)
public static void drawBlockHighlight(DrawBlockHighlightEvent event) {
MultipartEventHandler$.MODULE$.drawBlockHighlight(event);
}
@SubscribeEvent
public static void serverTick(TickEvent.ServerTickEvent event) {
MultipartEventHandler$.MODULE$.serverTick(event);
}
}
@@ -1,84 +0,0 @@
package codechicken.multipart.handler
import codechicken.multipart.{
TileCache,
TileMultipart,
MultiPartRegistry,
BlockMultipart
}
import cpw.mods.fml.common.eventhandler.{EventPriority, SubscribeEvent}
import net.minecraft.server.MinecraftServer
import codechicken.lib.packet.PacketCustom
import net.minecraftforge.event.world._
import java.util.EnumSet
import scala.collection.JavaConverters._
import java.util.List
import net.minecraft.entity.player.EntityPlayerMP
import cpw.mods.fml.relauncher.SideOnly
import cpw.mods.fml.relauncher.Side
import net.minecraftforge.client.event.DrawBlockHighlightEvent
import net.minecraft.util.MovingObjectPosition
import net.minecraft.util.MovingObjectPosition.MovingObjectType
import cpw.mods.fml.common.gameevent.TickEvent
object MultipartEventHandler {
@SubscribeEvent(priority = EventPriority.HIGHEST)
def tileEntityLoad(event: ChunkDataEvent.Load) {
MultipartSaveLoad.loadTiles(event.getChunk)
}
@SubscribeEvent
def worldUnLoad(event: WorldEvent.Unload) {
MultipartSPH.onWorldUnload(event.world)
if (event.world.isRemote) {
TileCache.clear()
} else {
MultipartSaveLoad.loadingWorld = null
}
}
@SubscribeEvent
def chunkWatch(event: ChunkWatchEvent.Watch) {
MultipartSPH.onChunkWatch(event.player, event.chunk)
}
@SubscribeEvent
def chunkUnWatch(event: ChunkWatchEvent.UnWatch) {
MultipartSPH.onChunkUnWatch(event.player, event.chunk)
}
@SubscribeEvent
@SideOnly(Side.CLIENT)
def drawBlockHighlight(event: DrawBlockHighlightEvent) {
if (
event.target != null && event.target.typeOfHit == MovingObjectType.BLOCK &&
event.player.worldObj
.getTileEntity(
event.target.blockX,
event.target.blockY,
event.target.blockZ
)
.isInstanceOf[TileMultipart]
) {
if (
BlockMultipart.drawHighlight(
event.player.worldObj,
event.player,
event.target,
event.partialTicks
)
)
event.setCanceled(true)
}
}
@SubscribeEvent
def serverTick(event: TickEvent.ServerTickEvent) {
if (event.phase == TickEvent.Phase.END)
MultipartSPH.onTickEnd(
MinecraftServer.getServer.getConfigurationManager.playerEntityList
.asInstanceOf[List[EntityPlayerMP]]
.asScala
)
}
}
@@ -78,8 +78,8 @@ class MultipartProxy_serverImpl {
def init() {}
def postInit() {
FMLCommonHandler.instance().bus().register(MultipartEventHandler)
MinecraftForge.EVENT_BUS.register(MultipartEventHandler)
FMLCommonHandler.instance().bus().register(MultipartEventHandler$.MODULE$)
MinecraftForge.EVENT_BUS.register(MultipartEventHandler$.MODULE$)
PacketCustom.assignHandler(MultipartSPH.channel, MultipartSPH)
PacketCustom.assignHandshakeHandler(
MultipartSPH.registryChannel,