diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 3fcb6b3..8419c7c 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -1211,3 +1211,13 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec - A clean build passes all 260 plain-JVM tests and the Java 8 Forge server passes all 111 tests. The same three runtime classes and every callable public descriptor match the reference, including ProjectRed's companion registration and GuideNH's exact companion `create` method. `multipart/MultipartGenerator.scala` is next. +- Characterized `MultipartGenerator` against untouched Scala. Two plain-JVM cases freeze both public surfaces, + private Scala-map descriptors and companion call opcodes. Five Forge cases freeze side-specific hierarchy caches, + duplicate/failed registration, scratch clearing, class snapshots/reuse, tile upgrades/downgrades, vanilla-block + conversion and a precompiled Scala consumer exercising companion generation and pass-through registration. +- Ported the facade and companion directly to Java. All five Scala maps, both compiler-generated public accessors, + the companion-only `generateCompositeTile` descriptor and side-safe proxy callback remain. Direct iteration removes + six private closures; the ASM factory's sole source adjustment explicitly names `MultipartGenerator$.MODULE$`. +- A clean build passes all 262 plain-JVM tests and the Java 8 Forge server passes all 116 tests. Both supported public + surfaces match the reference. `multipart/asm/ScratchBitSet.scala` is the next isolated support target; the generated + microblock traits still require the documented abstract-Java-mixin and side-only-member prerequisites. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index c7fd26d..0c1b765 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -3044,3 +3044,52 @@ None. The clean reference and port jars contain the same three `MicroblockGenera initialization and dispatch of the external Scala microblock trait. - Raw `javap` comparison confirms the exact three-class list and identical callable public names/descriptors. - The existing ProjectRed illuminated-microblock row remains the full-pack/client manual checkpoint. + +## 2026-09-02 — Multipart generator Java port + +The `MultipartGenerator` facade and companion are now Java. `MultipartMixinFactory`, `ASMMixinFactory`, +`ScratchBitSet` and the compiler remain Scala; the factory callback only changes its source spelling to +`MultipartGenerator$.MODULE$.registerTileClass`. + +### Observable behavior + +No known runtime divergence. Trait discovery retains interface/class/superclass traversal order, distinct trait +names, separate client/server caches and the original lack of cache invalidation after late registration. Duplicate +registrations keep the first mapping. A failed trait lookup still leaves its side mapping inserted, and null traits +still exclude that side. Construction clears the same thread-local scratch set and adds the client bit only on the +client path. + +Registered tile classes still receive an independent bit-set snapshot and the proxy's virtual companion callback. +Composite construction reuses only an exact registered trait set; unregistered tile classes retain the original +missing-map-entry failure. Vanilla conversion and live tile upgrades/downgrades preserve packet/callback order, +invalidation, part ordering, rebinding and copying. The removal comparison retains Scala's value equality. + +### ABI and reflection compatibility + +The facade's seven methods and companion's fifteen methods retain their names and descriptors. The five private +Scala mutable-map fields, the public mangled map/cache accessors and the `ScratchBitSet` interface remain. GuideNH +and Schematica still reach `generateCompositeTile(TileEntity, scala.collection.Iterable, boolean)` only through +`MultipartGenerator$.MODULE$`; no new static generation entry point is introduced. Shipping static and companion +pass-through registration calls also remain exact. + +A frozen Scala 2.11.5 consumer compiled against the untouched reference calls both companion generation and default +pass-through registration, then exercises a generated forwarding method under Forge. + +Accepted classfile-only differences are the private Java facade constructor, ordinary Java singleton initialization +and removed Scala marker attributes. As with `MicroblockGenerator`, the private scratch `ThreadLocal` loses +`ACC_FINAL` so the public Scala-trait setter remains replaceable. Unreferenced private map getters are gone. + +### Compiler artifacts + +Six private Scala closure classes disappear: the `traitsForPart` closure and its four hierarchy/mapping helpers, plus +the `setTraits` traversal closure. The downstream inventory contains no references to them. Hierarchy discovery uses +a `LinkedHashSet`; part iteration uses the existing Scala iterator without extra collection adapters. + +### Validation + +- `MultipartGeneratorCharacterizationTest`: 2 plain-JVM tests, unchanged from the Scala baseline. +- `MultipartGeneratorFunctionalTest`: 5 Forge tests, unchanged from the Scala baseline. +- Clean complete plain-JVM suite: 262 tests, 0 failures, 0 errors. +- Java 8 Forge dedicated-server suite: 116 tests, 0 failures, 0 errors. +- Raw `javap` comparison confirms identical callable public names/descriptors and the expected six-closure removal. +- Real client conversion packets/render rebinding and GuideNH/Schematica previews remain manual checkpoints. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index 05202a5..5ddd497 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`. 148 commits including the API-cleanup plan and separate characterization and -port commits through the microblock generator. +Branch: `algent/java`. Base: `master`. 150 commits including the API-cleanup plan and separate characterization and +port commits through the multipart generator. ## The one rule that matters @@ -67,7 +67,7 @@ awk -F'"' '/, BitSet> tileTraitMap = new HashMap<>(); + private final Map interfaceTraitMap_c = new HashMap<>(); + private final Map interfaceTraitMap_s = new HashMap<>(); + private final Map, BitSet> partTraitMap_c = new HashMap<>(); + private final Map, BitSet> partTraitMap_s = new HashMap<>(); + private final int clientTraitId; + private ThreadLocal codechicken$multipart$asm$ScratchBitSet$$bitSets; + + private MultipartGenerator$() { + ScratchBitSet$class.$init$(this); + clientTraitId = MultipartMixinFactory.registerTrait("codechicken.multipart.TileMultipartClient"); + } + + @Override + public ThreadLocal codechicken$multipart$asm$ScratchBitSet$$bitSets() { + return codechicken$multipart$asm$ScratchBitSet$$bitSets; + } + + @Override + public void codechicken$multipart$asm$ScratchBitSet$_setter_$codechicken$multipart$asm$ScratchBitSet$$bitSets_$eq( + ThreadLocal value) { + codechicken$multipart$asm$ScratchBitSet$$bitSets = value; + } + + @Override + public BitSet getBitSet() { + return ScratchBitSet$class.getBitSet(this); + } + + @Override + public BitSet freshBitSet() { + return ScratchBitSet$class.freshBitSet(this); + } + + public Map codechicken$multipart$MultipartGenerator$$interfaceTraitMap(boolean client) { + return client ? interfaceTraitMap_c : interfaceTraitMap_s; + } + + public BitSet codechicken$multipart$MultipartGenerator$$traitsForPart(TMultiPart part, boolean client) { + Map, BitSet> cache = client ? partTraitMap_c : partTraitMap_s; + Option cached = cache.get(part.getClass()); + if (cached.isDefined()) return cached.get(); + + Set traits = new LinkedHashSet<>(); + collectTraits(part.getClass(), codechicken$multipart$MultipartGenerator$$interfaceTraitMap(client), traits); + BitSet bitset = new BitSet(); + for (String trait : traits) bitset.set(MultipartMixinFactory.getId(trait)); + cache.put(part.getClass(), bitset); + return bitset; + } + + private void collectTraits(Class type, Map map, Set traits) { + for (Class iface : type.getInterfaces()) collectTraits(iface, map, traits); + Option trait = map.get(ASMImplicits.nodeName(type.getName())); + if (trait.isDefined()) traits.add(trait.get()); + if (type.getSuperclass() != null) collectTraits(type.getSuperclass(), map, traits); + } + + private BitSet setTraits(TMultiPart part, boolean client) { + BitSet bitset = freshBitSet(); + bitset.or(codechicken$multipart$MultipartGenerator$$traitsForPart(part, client)); + if (client) bitset.set(clientTraitId); + return bitset; + } + + private BitSet setTraits(Iterable parts, boolean client) { + BitSet bitset = freshBitSet(); + for (Iterator it = parts.iterator(); it.hasNext();) { + bitset.or(codechicken$multipart$MultipartGenerator$$traitsForPart(it.next(), client)); + } + if (client) bitset.set(clientTraitId); + return bitset; + } + + /** Replaces the tile if the new part requires additional interfaces, then adds the part. */ + public TileMultipart addPart(World world, BlockCoord pos, TMultiPart part) { + Tuple2 convertedTile = TileMultipart.getOrConvertTile2(world, pos); + TileMultipart tile = convertedTile._1(); + boolean converted = (Boolean) convertedTile._2(); + BitSet bitset = setTraits(part, world.isRemote); + + TileMultipart next = tile; + if (next != null) { + if (converted) { + next.partList().apply(0).invalidateConvertedTile(); + world.setBlock(pos.x, pos.y, pos.z, MultipartProxy.block(), 0, 0); + silentAddTile(world, pos, next); + PacketCustom.sendToChunk( + new S23PacketBlockChange(pos.x, pos.y, pos.z, world), + world, + pos.x >> 4, + pos.z >> 4); + next.partList().apply(0).onConverted(); + next.writeAddPart(next.partList().apply(0)); + } + + BitSet tileTraits = tileTraitMap.apply(tile.getClass()); + bitset.andNot(tileTraits); + if (!bitset.isEmpty()) { + bitset.or(tileTraits); + next = (TileMultipart) MultipartMixinFactory.construct(bitset, Nil$.MODULE$); + tile.setValid(false); + silentAddTile(world, pos, next); + next.from(tile); + } + } else { + world.setBlock(pos.x, pos.y, pos.z, MultipartProxy.block(), 0, 0); + next = (TileMultipart) MultipartMixinFactory.construct(bitset, Nil$.MODULE$); + silentAddTile(world, pos, next); + } + next.addPart_impl(part); + return next; + } + + public void silentAddTile(World world, BlockCoord pos, TileEntity tile) { + Chunk chunk = world.getChunkFromBlockCoords(pos.x, pos.z); + if (chunk != null) chunk.func_150812_a(pos.x & 15, pos.y, pos.z & 15, tile); + } + + /** Reuses the tile only when it has exactly the interfaces required by the parts. */ + public TileMultipart generateCompositeTile(TileEntity tile, Iterable parts, boolean client) { + BitSet bitset = setTraits(parts, client); + if (tile instanceof TileMultipart && bitset.equals(tileTraitMap.apply(tile.getClass()))) { + return (TileMultipart) tile; + } + return (TileMultipart) MultipartMixinFactory.construct(bitset, Nil$.MODULE$); + } + + /** Replaces a tile whose remaining parts no longer require all its interfaces. */ + public TileMultipart partRemoved(TileMultipart tile) { + TileMultipart next = generateCompositeTile(tile, tile.partList(), tile.getWorldObj().isRemote); + if (!next.equals(tile)) { + tile.setValid(false); + silentAddTile(tile.getWorldObj(), new BlockCoord(tile), next); + next.from(tile); + next.notifyTileChange(); + } + return next; + } + + public void registerTrait(String marker, String trait) { + registerTrait(marker, trait, trait); + } + + public void registerTrait(String marker, String clientTrait, String serverTrait) { + marker = ASMImplicits.nodeName(marker); + clientTrait = ASMImplicits.nodeName(clientTrait); + serverTrait = ASMImplicits.nodeName(serverTrait); + registerSide(interfaceTraitMap_c, marker, clientTrait); + registerSide(interfaceTraitMap_s, marker, serverTrait); + } + + private void registerSide(Map map, String marker, String trait) { + if (trait == null) return; + if (map.contains(marker)) { + MultipartProxy.logger().error("Trait already registered for " + marker); + } else { + map.put(marker, trait); + MultipartMixinFactory.registerTrait(trait); + } + } + + public void registerPassThroughInterface(String name) { + registerPassThroughInterface(name, true, true); + } + + public void registerPassThroughInterface(String name, boolean client, boolean server) { + String trait = MultipartMixinFactory.generatePassThroughTrait(name); + if (trait == null) return; + registerTrait(name, client ? trait : null, server ? trait : null); + } + + public void registerTileClass(Class type, BitSet traits) { + BitSet copy = new BitSet(); + copy.or(traits); + tileTraitMap.put(type, copy); + MultipartProxy$.MODULE$.onTileClassBuilt(type); + } +} diff --git a/src/main/scala/codechicken/multipart/MultipartGenerator.java b/src/main/scala/codechicken/multipart/MultipartGenerator.java new file mode 100644 index 0000000..6d7e174 --- /dev/null +++ b/src/main/scala/codechicken/multipart/MultipartGenerator.java @@ -0,0 +1,53 @@ +package codechicken.multipart; + +import java.util.BitSet; + +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +import codechicken.lib.vec.BlockCoord; + +/** + * Manages dynamic construction of multipart container tiles. Register a mixin against a part's marker interface to add + * tile-level logic or interfaces whenever that part is present. Generated classes can be inspected with the ASM debug + * configuration option. + */ +public final class MultipartGenerator { + + private MultipartGenerator() {} + + public static BitSet getBitSet() { + return MultipartGenerator$.MODULE$.getBitSet(); + } + + public static BitSet freshBitSet() { + return MultipartGenerator$.MODULE$.freshBitSet(); + } + + /** Adds a tile without notifying neighbouring blocks or adding it to the tick list. */ + public static void silentAddTile(World world, BlockCoord pos, TileEntity tile) { + MultipartGenerator$.MODULE$.silentAddTile(world, pos, tile); + } + + /** Registers the same tile trait for both sides. */ + public static void registerTrait(String marker, String trait) { + MultipartGenerator$.MODULE$.registerTrait(marker, trait); + } + + /** Registers side-specific tile traits; either trait may be null to exclude that side. */ + public static void registerTrait(String marker, String clientTrait, String serverTrait) { + MultipartGenerator$.MODULE$.registerTrait(marker, clientTrait, serverTrait); + } + + public static void registerPassThroughInterface(String name) { + MultipartGenerator$.MODULE$.registerPassThroughInterface(name); + } + + /** + * Adds the interface to the container and forwards its methods to its single implementing part. A second part + * implementing the same interface is rejected by the generated trait's occlusion check. + */ + public static void registerPassThroughInterface(String name, boolean client, boolean server) { + MultipartGenerator$.MODULE$.registerPassThroughInterface(name, client, server); + } +} diff --git a/src/main/scala/codechicken/multipart/MultipartGenerator.scala b/src/main/scala/codechicken/multipart/MultipartGenerator.scala deleted file mode 100644 index 94bfa34..0000000 --- a/src/main/scala/codechicken/multipart/MultipartGenerator.scala +++ /dev/null @@ -1,240 +0,0 @@ -package codechicken.multipart - -import net.minecraft.tileentity.TileEntity -import net.minecraft.world.World -import codechicken.lib.vec.BlockCoord -import codechicken.multipart.handler.{MultipartProxy, MultipartProxy$} -import codechicken.lib.packet.PacketCustom -import net.minecraft.network.play.server.S23PacketBlockChange -import java.util.BitSet -import scala.collection.mutable.Map -import codechicken.multipart.asm.{ScratchBitSet, MultipartMixinFactory} -import codechicken.multipart.asm.ASMImplicits._ - -/** This class manages the dynamic construction and allocation of container - * TileMultipart instances. - * - * Classes that extend TileMultipart, adding tile centric logic, optimisations - * or interfaces, can be registered to a marker interface on a part instance. - * When a part is added to the tile that implements the certain marker - * interface, the container tile will be replaced with a class that includes - * the functionality from the corresponding mixin class. - * - * Classes are generated in a similar fashion to the way scala traits are - * compiled. To see the output, simply enable the config option and look in the - * asm/multipart folder of you .minecraft directory. - * - * There are several mixin traits that come with the API included in the - * scalatraits package. TPartialOcclusionTile is defined as class instead of - * trait to give an example for Java programmers. - */ -object MultipartGenerator extends ScratchBitSet { - private val tileTraitMap = Map[Class[_], BitSet]() - private val interfaceTraitMap_c = Map[String, String]() - private val interfaceTraitMap_s = Map[String, String]() - private val partTraitMap_c = Map[Class[_], BitSet]() - private val partTraitMap_s = Map[Class[_], BitSet]() - private val clientTraitId = - MultipartMixinFactory.registerTrait( - "codechicken.multipart.TileMultipartClient" - ) - - private def partTraitMap(client: Boolean) = - if (client) partTraitMap_c else partTraitMap_s - private def interfaceTraitMap(client: Boolean) = - if (client) interfaceTraitMap_c else interfaceTraitMap_s - - private def traitsForPart(part: TMultiPart, client: Boolean) = - partTraitMap(client).getOrElseUpdate( - part.getClass, { - def heirachy(clazz: Class[_]): Seq[Class[_]] = { - var superClasses: Seq[Class[_]] = - clazz.getInterfaces.flatMap(c => heirachy(c)) :+ clazz - if (clazz.getSuperclass != null) - superClasses = superClasses ++ heirachy(clazz.getSuperclass) - return superClasses - } - - val map = interfaceTraitMap(client) - val traits = - heirachy(part.getClass).flatMap(c => map.get(c.nodeName)).distinct - - val bitset = new BitSet - traits.map(MultipartMixinFactory.getId).foreach(bitset.set) - bitset - } - ) - - private def setTraits(part: TMultiPart, client: Boolean): BitSet = - setTraits(Seq(part), client) - - private def setTraits(parts: Iterable[TMultiPart], client: Boolean) = { - val bitset = freshBitSet - parts.foreach(p => bitset.or(traitsForPart(p, client))) - if (client) bitset.set(clientTraitId) - bitset - } - - /** Check if part adds any new interfaces to tile, if so, replace tile with a - * new copy and call tile.addPart(part) returns true if tile was replaced - */ - private[multipart] def addPart( - world: World, - pos: BlockCoord, - part: TMultiPart - ): TileMultipart = { - val gtile = TileMultipart.getOrConvertTile2(world, pos) - val tile = gtile._1 - val converted = gtile._2.asInstanceOf[Boolean] - val bitset = setTraits(part, world.isRemote) - - var ntile = tile - if (ntile != null) { - if (converted) { // perform client conversion - ntile.partList.apply(0).invalidateConvertedTile() - world.setBlock(pos.x, pos.y, pos.z, MultipartProxy.block, 0, 0) - silentAddTile(world, pos, ntile) - PacketCustom.sendToChunk( - new S23PacketBlockChange(pos.x, pos.y, pos.z, world), - world, - pos.x >> 4, - pos.z >> 4 - ) - ntile.partList.apply(0).onConverted() - ntile.writeAddPart(ntile.partList.apply(0)) - } - - val tileTraits = tileTraitMap(tile.getClass) - bitset.andNot(tileTraits) - if (!bitset.isEmpty) { - bitset.or(tileTraits) - ntile = MultipartMixinFactory.construct(bitset) - tile.setValid(false) - silentAddTile(world, pos, ntile) - ntile.from(tile) - } - } else { - world.setBlock(pos.x, pos.y, pos.z, MultipartProxy.block, 0, 0) - ntile = MultipartMixinFactory.construct(bitset) - silentAddTile(world, pos, ntile) - } - ntile.addPart_impl(part) - return ntile - } - - /** Adds a tile entity to the world without notifying neighbor blocks or - * adding it to the tick list - */ - def silentAddTile(world: World, pos: BlockCoord, tile: TileEntity) { - val chunk = world.getChunkFromBlockCoords(pos.x, pos.z) - if (chunk != null) - chunk.func_150812_a(pos.x & 15, pos.y, pos.z & 15, tile) - } - - /** Check if tile satisfies all the interfaces required by parts. If not, - * return a new generated copy of tile - */ - private[multipart] def generateCompositeTile( - tile: TileEntity, - parts: Iterable[TMultiPart], - client: Boolean - ) = { - val bitset = setTraits(parts, client) - if ( - tile != null && tile - .isInstanceOf[TileMultipart] && bitset == tileTraitMap(tile.getClass) - ) - tile.asInstanceOf[TileMultipart] - else - MultipartMixinFactory.construct(bitset) - } - - /** Check if there are any redundant interfaces on tile, if so, replace tile - * with new copy - */ - private[multipart] def partRemoved(tile: TileMultipart) = { - val ntile = - generateCompositeTile(tile, tile.partList, tile.getWorldObj.isRemote) - if (ntile != tile) { - tile.setValid(false) - silentAddTile(tile.getWorldObj, new BlockCoord(tile), ntile) - ntile.from(tile) - ntile.notifyTileChange() - } - ntile - } - - /** register s_trait to be applied to tiles containing parts implementing - * s_interface - */ - def registerTrait(s_interface: String, s_trait: String): Unit = - registerTrait(s_interface, s_trait, s_trait) - - /** register traits to be applied to tiles containing parts implementing - * s_interface c_trait for client worlds (may be null) s_trait for server - * worlds (may be null) - */ - def registerTrait( - s_interface$ : String, - c_trait$ : String, - s_trait$ : String - ) { - val s_interface = nodeName(s_interface$) - val c_trait = nodeName(c_trait$) - val s_trait = nodeName(s_trait$) - - def registerSide(map: Map[String, String], traitName: String) = if ( - traitName != null - ) { - if (map.contains(s_interface)) - MultipartProxy.logger.error( - "Trait already registered for " + s_interface - ) - else { - map.put(s_interface, traitName) - MultipartMixinFactory.registerTrait(traitName) - } - } - registerSide(interfaceTraitMap_c, c_trait) - registerSide(interfaceTraitMap_s, s_trait) - } - - def registerPassThroughInterface(s_interface: String): Unit = - registerPassThroughInterface(s_interface, true, true) - - /** A passthrough interface, is an interface to be implemented on the - * container tile instance, for which all calls are passed directly to the - * single implementing part. Registering a passthrough interface is - * equivalent to defining a mixin class as follows. - * 1. field 'impl' which contains the reference to the corresponding part - * 2. occlusionTest is overriden to prevent more than one part with - * s_interface existing in the block space 3. implementing s_interface - * and passing all calls directly to the part instance. - * - * This allows compatibility with APIs that expect interfaces on the tile - * entity. - */ - def registerPassThroughInterface( - s_interface: String, - client: Boolean, - server: Boolean - ) { - val tType = MultipartMixinFactory.generatePassThroughTrait(s_interface) - if (tType == null) - return - - registerTrait( - s_interface, - if (client) tType else null, - if (server) tType else null - ) - } - - private[multipart] def registerTileClass( - clazz: Class[_ <: TileMultipart], - traits: BitSet - ) { - tileTraitMap.put(clazz, traits.copy) - MultipartProxy$.MODULE$.onTileClassBuilt(clazz) - } -} diff --git a/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala b/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala index f911096..82cbc52 100644 --- a/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala +++ b/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala @@ -6,7 +6,7 @@ import codechicken.lib.asm.{ObfMapping, CC_ClassWriter} import org.objectweb.asm.{Label, FieldVisitor, MethodVisitor} import org.objectweb.asm.Opcodes._ import org.objectweb.asm.tree.{MethodNode, ClassNode} -import codechicken.multipart.{MultipartGenerator, TileMultipart} +import codechicken.multipart.{MultipartGenerator$, TileMultipart} import java.util.BitSet import ASMImplicits._ @@ -245,5 +245,5 @@ object MultipartMixinFactory extends ASMMixinFactory(classOf[TileMultipart]) { clazz: Class[_ <: TileMultipart], traitSet: BitSet ) = - MultipartGenerator.registerTileClass(clazz, traitSet) + MultipartGenerator$.MODULE$.registerTileClass(clazz, traitSet) }