From 4cd63eb3c8e3226aeb7cfe42829c0fa16bb084fd Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 30 Aug 2026 21:24:49 +0200 Subject: [PATCH] refactor: port multipart event handler Preserve event-bus identity and the annotated singleton surface. --- JAVA_MIGRATION.md | 7 ++ JAVA_MIGRATION_DIVERGENCES.md | 46 ++++++++++ JAVA_MIGRATION_HANDOFF.md | 24 ++++-- .../handler/MultipartEventHandler$.java | 76 +++++++++++++++++ .../handler/MultipartEventHandler.java | 48 +++++++++++ .../handler/MultipartEventHandler.scala | 84 ------------------- .../multipart/handler/proxies.scala | 4 +- 7 files changed, 194 insertions(+), 95 deletions(-) create mode 100644 src/main/scala/codechicken/multipart/handler/MultipartEventHandler$.java create mode 100644 src/main/scala/codechicken/multipart/handler/MultipartEventHandler.java delete mode 100644 src/main/scala/codechicken/multipart/handler/MultipartEventHandler.scala diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 069d988..b297b2c 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -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. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 9f64173..4fe5ea0 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -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. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index a36fae0..45f50be 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -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'"' '/ players = MinecraftServer.getServer().getConfigurationManager().playerEntityList; + MultipartSPH$.MODULE$.onTickEnd(JavaConverters.asScalaBufferConverter(players).asScala()); + } + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.java b/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.java new file mode 100644 index 0000000..b4a4bca --- /dev/null +++ b/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.java @@ -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); + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.scala b/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.scala deleted file mode 100644 index 67e16a8..0000000 --- a/src/main/scala/codechicken/multipart/handler/MultipartEventHandler.scala +++ /dev/null @@ -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 - ) - } -} diff --git a/src/main/scala/codechicken/multipart/handler/proxies.scala b/src/main/scala/codechicken/multipart/handler/proxies.scala index 118a0ff..6e4738d 100644 --- a/src/main/scala/codechicken/multipart/handler/proxies.scala +++ b/src/main/scala/codechicken/multipart/handler/proxies.scala @@ -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,