diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 86390f4..3b349fa 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -559,3 +559,57 @@ replacement classes. startup calls `setupIDMap`, `calcMaxCuttingStrength` and the default content registration. - Error paths remain uncovered by tests: they call the microblock logger, which is null until `MicroblockProxy.preInit` runs, so they cannot execute headless. They are unchanged from the reference. + +## 2026-08-14 — MultiPartRegistry Java port + +Covers `MultiPartRegistry`, `MultiPartRegistry$` and the three nested interfaces. Twelve jars reference the type and +Schematica reaches the singleton reflectively. + +### Observable behavior + +No known divergence. `beforeServerStart` still sorts the type map by name so ids are indexes into it, part ids still +round trip through the shared `IDWriter`, converters are still grouped per block with the first non-null result +winning, registration still refuses to run once the registry is closed and still requires an active mod container, a +duplicate part id still throws, and `writePartID` and `getModContainer` still throw rather than returning null for an +unknown name. + +### Preserved Scala-typed descriptors + +Both remain, because ForgeRelocationFMP and ProjRed link against them: + +- `registerParts(Lscala/Function2;Lscala/collection/Seq;)V` +- `registerParts(Lcodechicken/multipart/MultiPartRegistry$IPartFactory2;Lscala/collection/Seq;)V` + +The `Seq` overload drains through `JavaConversions.seqAsJavaList`. The `Function2` overload applies the function with +a boxed `Boolean`, matching what the Scala closure received. + +### Array overloads become Java varargs + +`registerParts(IPartFactory, String[])` and `registerParts(IPartFactory2, String[])` are declared `String...`. The +descriptor is unchanged, verified against the reference dev jar; `ACC_VARARGS` is a compiler flag and does not affect +linkage. This was needed because `MicroblockClass` called the Scala varargs form with a single string, which no longer +resolves against a plain array parameter. It also makes the Java call sites more natural without adding API. + +### Changed and removed API + +- `readIDMap` returns `java.util.List` instead of `scala.collection.Seq`, as with + `MicroMaterialRegistry`. It is `private[multipart]` in Scala, so it has no static forwarder, and no jar references + it. +- The mangled public accessors Scala emitted so its closures could reach private state + (`codechicken$multipart$MultiPartRegistry$$typeMap()` and the four others) are gone. No jar references them. + +### Compiler artifacts + +Accepted divergence: the eight `MultiPartRegistry$$anonfun$*` classes and `MultiPartRegistry$$anon$1` are replaced by +two Java anonymous classes, `MultiPartRegistry$1` and `$2`, which are the two `IPartFactory2` adapters the deprecated +overloads build. + +### Validation + +- `MultiPartRegistryCharacterizationTest`: 5 tests, 0 failures, 0 errors, unchanged from the Scala baseline. +- Complete plain-JVM suite: 78 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. +- Java 8 Forge dedicated-server suite: 5 tests, 0 failures, 0 errors. This is the layer that matters for this class: + registration needs FML's active mod container, so real startup is the only place both factory overloads actually + run, and the suite resolves a part registered through each. +- Error paths that log remain uncovered headless, as before, because the multipart logger is null until preInit. diff --git a/src/main/scala/codechicken/multipart/MultiPartRegistry$.java b/src/main/scala/codechicken/multipart/MultiPartRegistry$.java new file mode 100644 index 0000000..137bdcc --- /dev/null +++ b/src/main/scala/codechicken/multipart/MultiPartRegistry$.java @@ -0,0 +1,98 @@ +package codechicken.multipart; + +import java.util.List; + +import net.minecraft.block.Block; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.packet.PacketCustom; +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.MultiPartRegistry.IPartConverter; +import codechicken.multipart.MultiPartRegistry.IPartFactory; +import codechicken.multipart.MultiPartRegistry.IPartFactory2; +import cpw.mods.fml.common.ModContainer; + +/** + * Scala companion singleton. Retained because compiled Scala consumers read MODULE$ and call these instance methods. + */ +public final class MultiPartRegistry$ { + + public static final MultiPartRegistry$ MODULE$ = new MultiPartRegistry$(); + + private MultiPartRegistry$() {} + + @Deprecated + public void registerParts(IPartFactory partFactory, String... types) { + MultiPartRegistry.registerParts(partFactory, types); + } + + @Deprecated + public void registerParts(scala.Function2 partFactory, + scala.collection.Seq types) { + MultiPartRegistry.registerParts(partFactory, types); + } + + public void registerParts(IPartFactory2 partFactory, String... types) { + MultiPartRegistry.registerParts(partFactory, types); + } + + public void registerParts(IPartFactory2 partFactory, scala.collection.Seq types) { + MultiPartRegistry.registerParts(partFactory, types); + } + + public void registerConverter(IPartConverter c) { + MultiPartRegistry.registerConverter(c); + } + + public void beforeServerStart() { + MultiPartRegistry.beforeServerStart(); + } + + public void writeIDMap(PacketCustom packet) { + MultiPartRegistry.writeIDMap(packet); + } + + public List readIDMap(PacketCustom packet) { + return MultiPartRegistry.readIDMap(packet); + } + + public boolean required() { + return MultiPartRegistry.required(); + } + + public boolean loaded() { + return MultiPartRegistry.loaded(); + } + + public void postInit() { + MultiPartRegistry.postInit(); + } + + public void writePartID(MCDataOutput data, TMultiPart part) { + MultiPartRegistry.writePartID(data, part); + } + + public TMultiPart readPart(MCDataInput data) { + return MultiPartRegistry.readPart(data); + } + + public TMultiPart loadPart(String name, NBTTagCompound nbt) { + return MultiPartRegistry.loadPart(name, nbt); + } + + @Deprecated + public TMultiPart createPart(String name, boolean client) { + return MultiPartRegistry.createPart(name, client); + } + + public TMultiPart convertBlock(World world, BlockCoord pos, Block block) { + return MultiPartRegistry.convertBlock(world, pos, block); + } + + public ModContainer getModContainer(String name) { + return MultiPartRegistry.getModContainer(name); + } +} diff --git a/src/main/scala/codechicken/multipart/MultiPartRegistry.java b/src/main/scala/codechicken/multipart/MultiPartRegistry.java new file mode 100644 index 0000000..516a965 --- /dev/null +++ b/src/main/scala/codechicken/multipart/MultiPartRegistry.java @@ -0,0 +1,287 @@ +package codechicken.multipart; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import net.minecraft.block.Block; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; + +import com.google.common.collect.ArrayListMultimap; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.packet.PacketCustom; +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.handler.MultipartProxy; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModContainer; +import scala.Tuple2; +import scala.collection.JavaConversions; + +/** This class handles the registration and internal ID mapping of all multipart classes. */ +public final class MultiPartRegistry { + + /** + * Interface to be registered for constructing parts. Every instance of every multipart is constructed from an + * implementor of this. + * + * @deprecated Use IPartFactory2 + */ + @Deprecated + public interface IPartFactory { + + /** + * Create a new instance of the part with the specified type name identifier. + * + * @param client If the part instance is for the client or the server + */ + TMultiPart createPart(String name, boolean client); + } + + /** Will replace IPartFactory in 1.8. */ + public interface IPartFactory2 { + + /** + * Create a new server instance of the part with the specified type name identifier. + * + * @param nbt The tag compound that will be passed to part.load, can be used to change the class of part + * returned + */ + TMultiPart createPart(String name, NBTTagCompound nbt); + + /** + * Create a new client instance of the part with the specified type name identifier. + * + * @param packet The packet that will be passed to part.readDesc, can be used to change the class of part + * returned + */ + TMultiPart createPart(String name, MCDataInput packet); + } + + /** An interface for converting existing blocks/tile entities to multipart versions. */ + public interface IPartConverter { + + /** + * Return true if this converter can handle the specific blockID (may or may not actually convert the block). + */ + Iterable blockTypes(); + + /** Return a multipart version of the block at pos in world. Return null if no conversion is possible. */ + TMultiPart convert(World world, BlockCoord pos); + } + + private static final Map typeMap = new HashMap<>(); + private static final Map nameMap = new HashMap<>(); + private static Tuple2[] idMap; + private static final IDWriter idWriter = new IDWriter(); + private static final ArrayListMultimap converters = ArrayListMultimap.create(); + private static final Map containers = new HashMap<>(); + + /** The state of the registry. 0 = no parts, 1 = registering, 2 = registered. */ + private static int state = 0; + + private MultiPartRegistry() {} + + /** + * Register a part factory with an array of types it is capable of instantiating. Must be called before postInit. + * + * @deprecated Use IPartFactory2 + */ + @Deprecated + public static void registerParts(IPartFactory partFactory, String... types) { + registerParts(new IPartFactory2() { + + @Override + public TMultiPart createPart(String name, MCDataInput packet) { + return partFactory.createPart(name, true); + } + + @Override + public TMultiPart createPart(String name, NBTTagCompound nbt) { + return partFactory.createPart(name, false); + } + }, types); + } + + /** + * Scala function version of registerParts. + * + * @deprecated Use IPartFactory2 + */ + @Deprecated + public static void registerParts(scala.Function2 partFactory, + scala.collection.Seq types) { + registerParts(new IPartFactory2() { + + @Override + public TMultiPart createPart(String name, MCDataInput packet) { + return partFactory.apply(name, Boolean.TRUE); + } + + @Override + public TMultiPart createPart(String name, NBTTagCompound nbt) { + return partFactory.apply(name, Boolean.FALSE); + } + }, types); + } + + /** Scala va-args version of registerParts. */ + public static void registerParts(IPartFactory2 partFactory, scala.collection.Seq types) { + registerParts(partFactory, JavaConversions.seqAsJavaList(types).toArray(new String[0])); + } + + /** + * Register a part factory with an array of types it is capable of instantiating. Must be called before postInit. + */ + public static void registerParts(IPartFactory2 partFactory, String... types) { + if (loaded()) { + throw new IllegalStateException("Parts must be registered in the init methods."); + } + state = 1; + + ModContainer container = Loader.instance().activeModContainer(); + if (container == null) { + throw new IllegalStateException( + "Parts must be registered during the initialization phase of a mod container"); + } + + for (String s : types) { + if (typeMap.containsKey(s)) { + throw new IllegalStateException("Part with id " + s + " is already registered."); + } + + typeMap.put(s, partFactory); + containers.put(s, container); + } + } + + /** Register a part converter instance. */ + public static void registerConverter(IPartConverter c) { + for (Block block : c.blockTypes()) { + converters.put(block, c); + } + } + + @SuppressWarnings("unchecked") + public static void beforeServerStart() { + List> entries = new ArrayList<>(typeMap.entrySet()); + entries.sort(Comparator.comparing(Map.Entry::getKey)); + + idMap = new Tuple2[entries.size()]; + for (int i = 0; i < entries.size(); i++) { + idMap[i] = new Tuple2<>(entries.get(i).getKey(), entries.get(i).getValue()); + } + + idWriter.setMax(idMap.length); + nameMap.clear(); + for (int i = 0; i < idMap.length; i++) { + nameMap.put(idMap[i]._1(), i); + } + } + + public static void writeIDMap(PacketCustom packet) { + packet.writeInt(idMap.length); + for (Tuple2 entry : idMap) { + packet.writeString(entry._1()); + } + } + + /** Reads the server's id map, returning the names this client has no factory for. */ + @SuppressWarnings("unchecked") + public static List readIDMap(PacketCustom packet) { + int k = packet.readInt(); + idWriter.setMax(k); + idMap = new Tuple2[k]; + nameMap.clear(); + List missing = new ArrayList<>(); + for (int i = 0; i < k; i++) { + String s = packet.readString(); + IPartFactory2 v = typeMap.get(s); + if (v == null) { + missing.add(s); + } else { + idMap[i] = new Tuple2<>(s, v); + nameMap.put(s, i); + } + } + return missing; + } + + /** Return true if any multiparts have been registered. */ + public static boolean required() { + return state > 0; + } + + /** Return true if no more parts can be registered. */ + public static boolean loaded() { + return state == 2; + } + + public static void postInit() { + state = 2; + } + + /** Writes the id of part to data. */ + public static void writePartID(MCDataOutput data, TMultiPart part) { + Integer id = nameMap.get(part.getType()); + if (id == null) { + throw new NoSuchElementException("None.get"); + } + idWriter.write(data, id); + } + + /** Uses instantiators to create a new part from the id read from data. */ + public static TMultiPart readPart(MCDataInput data) { + Tuple2 e = idMap[idWriter.read(data)]; + return e._2().createPart(e._1(), data); + } + + /** Uses instantiators to create a new part from a tag compound. */ + public static TMultiPart loadPart(String name, NBTTagCompound nbt) { + IPartFactory2 factory = typeMap.get(name); + if (factory == null) { + MultipartProxy.logger().error("Missing mapping for part with ID: " + name); + return null; + } + return factory.createPart(name, nbt); + } + + /** + * Uses instantiators to create a new part with specified identifier on side. + * + * @deprecated currently calls the nbt/packet version with a null parameter, use readPart or loadPart instead + */ + @Deprecated + public static TMultiPart createPart(String name, boolean client) { + IPartFactory2 factory = typeMap.get(name); + if (factory == null) { + MultipartProxy.logger().error("Missing mapping for part with ID: " + name); + return null; + } + return client ? factory.createPart(name, (MCDataInput) null) : factory.createPart(name, (NBTTagCompound) null); + } + + /** Calls converters to create a multipart version of the block at pos. */ + public static TMultiPart convertBlock(World world, BlockCoord pos, Block block) { + for (IPartConverter c : converters.get(block)) { + TMultiPart ret = c.convert(world, pos); + if (ret != null) { + return ret; + } + } + return null; + } + + public static ModContainer getModContainer(String name) { + ModContainer container = containers.get(name); + if (container == null) { + throw new NoSuchElementException("key not found: " + name); + } + return container; + } +} diff --git a/src/main/scala/codechicken/multipart/MultiPartRegistry.scala b/src/main/scala/codechicken/multipart/MultiPartRegistry.scala deleted file mode 100644 index 9acc8e1..0000000 --- a/src/main/scala/codechicken/multipart/MultiPartRegistry.scala +++ /dev/null @@ -1,247 +0,0 @@ -package codechicken.multipart - -import scala.collection.mutable.{Map => MMap} -import codechicken.lib.packet.PacketCustom -import codechicken.lib.data.MCDataOutput -import codechicken.lib.data.MCDataInput -import net.minecraft.world.World -import codechicken.lib.vec.BlockCoord -import scala.collection.mutable.ListBuffer -import cpw.mods.fml.common.ModContainer -import cpw.mods.fml.common.Loader -import net.minecraft.block.Block -import java.lang.Iterable -import com.google.common.collect.ArrayListMultimap -import scala.collection.JavaConversions._ -import net.minecraft.nbt.NBTTagCompound - -/** This class handles the registration and internal ID mapping of all multipart - * classes. - */ -object MultiPartRegistry { - - /** Interface to be registered for constructing parts. Every instance of every - * multipart is constructed from an implementor of this. - */ - @Deprecated - trait IPartFactory { - - /** Create a new instance of the part with the specified type name - * identifier - * @param client - * If the part instance is for the client or the server - */ - def createPart(name: String, client: Boolean): TMultiPart - } - - /** Will replace IPartFactory in 1.8 - */ - trait IPartFactory2 { - - /** Create a new server instance of the part with the specified type name - * identifier - * @param nbt - * The tag compound that will be passed to part.load, can be used to - * change the class of part returned - */ - def createPart(name: String, nbt: NBTTagCompound): TMultiPart - - /** Create a new client instance of the part with the specified type name - * identifier - * @param packet - * The packet that will be passed to part.readDesc, can be used to change - * the class of part returned - */ - def createPart(name: String, packet: MCDataInput): TMultiPart - } - - /** An interface for converting existing blocks/tile entities to multipart - * versions. - */ - trait IPartConverter { - - /** Return true if this converter can handle the specific blockID (may or - * may not actually convert the block) - */ - def blockTypes: Iterable[Block] - - /** Return a multipart version of the block at pos in world. Return null if - * no conversion is possible. - */ - def convert(world: World, pos: BlockCoord): TMultiPart - } - - private val typeMap = MMap[String, IPartFactory2]() - private val nameMap = MMap[String, Int]() - private var idMap: Array[(String, IPartFactory2)] = _ - private val idWriter = new IDWriter - private val converters = ArrayListMultimap.create[Block, IPartConverter]() - private val containers = MMap[String, ModContainer]() - - /** The state of the registry. 0 = no parts, 1 = registering, 2 = registered - */ - private var state: Int = 0 - - /** Register a part factory with an array of types it is capable of - * instantiating. Must be called before postInit - * @deprecated - * Use IPartFactory2 - */ - @Deprecated - def registerParts(partFactory: IPartFactory, types: Array[String]) { - registerParts(partFactory.createPart _, types: _*) - } - - /** Scala function version of registerParts - * @deprecated - * Use IPartFactory2 - */ - @Deprecated - def registerParts( - partFactory: (String, Boolean) => TMultiPart, - types: String* - ) { - registerParts( - new IPartFactory2 { - override def createPart(name: String, packet: MCDataInput) = - partFactory(name, true) - override def createPart(name: String, nbt: NBTTagCompound) = - partFactory(name, false) - }, - types: _* - ) - } - - /** Register a part factory with an array of types it is capable of - * instantiating. Must be called before postInit - */ - def registerParts(partFactory: IPartFactory2, types: Array[String]) { - registerParts(partFactory, types: _*) - } - - /** Scala va-args version of registerParts - */ - def registerParts(partFactory: IPartFactory2, types: String*) { - if (loaded) - throw new IllegalStateException( - "Parts must be registered in the init methods." - ) - state = 1 - - val container = Loader.instance.activeModContainer - if (container == null) - throw new IllegalStateException( - "Parts must be registered during the initialization phase of a mod container" - ) - - types.foreach { s => - if (typeMap.contains(s)) - throw new IllegalStateException( - "Part with id " + s + " is already registered." - ) - - typeMap.put(s, partFactory) - containers.put(s, container) - } - } - - /** Register a part converter instance - */ - def registerConverter(c: IPartConverter) { - c.blockTypes.foreach(converters.put(_, c)) - } - - private[multipart] def beforeServerStart() { - idMap = typeMap.toList.sortBy(_._1).toArray - idWriter.setMax(idMap.length) - nameMap.clear() - for (i <- 0 until idMap.length) - nameMap.put(idMap(i)._1, i) - } - - private[multipart] def writeIDMap(packet: PacketCustom) { - packet.writeInt(idMap.length) - idMap.foreach(e => packet.writeString(e._1)) - } - - private[multipart] def readIDMap(packet: PacketCustom): Seq[String] = { - val k = packet.readInt() - idWriter.setMax(k) - idMap = new Array(k) - nameMap.clear() - val missing = ListBuffer[String]() - for (i <- 0 until k) { - val s = packet.readString() - val v = typeMap.get(s) - if (v.isEmpty) - missing += s - else { - idMap(i) = (s, v.get) - nameMap.put(s, i) - } - } - return missing - } - - /** Return true if any multiparts have been registered - */ - private[multipart] def required = state > 0 - - /** Return true if no more parts can be registered - */ - def loaded = state == 2 - - private[multipart] def postInit() { - state = 2 - } - - /** Writes the id of part to data - */ - def writePartID(data: MCDataOutput, part: TMultiPart) { - idWriter.write(data, nameMap.get(part.getType).get) - } - - /** Uses instantiators to creat a new part from the id read from data - */ - def readPart(data: MCDataInput) = { - val e = idMap(idWriter.read(data)) - e._2.createPart(e._1, data) - } - - /** Uses instantiators to creat a new part from the a tag compound - */ - def loadPart(name: String, nbt: NBTTagCompound) = typeMap.get(name) match { - case Some(factory) => factory.createPart(name, nbt) - case None => - logger.error("Missing mapping for part with ID: " + name) - null - } - - /** Uses instantiators to create a new part with specified identifier on side - * @deprecated - * currently calls the nbt/packet version with a null parameter, use - * readPart or loadPart instead - */ - @Deprecated - def createPart(name: String, client: Boolean) = typeMap.get(name) match { - case Some(factory) => - if (client) factory.createPart(name, null: MCDataInput) - else factory.createPart(name, null: NBTTagCompound) - case None => - logger.error("Missing mapping for part with ID: " + name) - null - } - - /** Calls converters to create a multipart version of the block at pos - */ - def convertBlock(world: World, pos: BlockCoord, block: Block): TMultiPart = { - for (c <- converters.get(block)) { - val ret = c.convert(world, pos) - if (ret != null) - return ret - } - return null - } - - def getModContainer(name: String) = containers(name) -}