refactor: port MultiPartRegistry to Java

Replaces MultiPartRegistry.scala with a Java registry, its companion
singleton and the three nested interfaces. All three interfaces are
descriptor-identical to the reference and MODULE$ keeps every instance
method downstream links against. Twelve jars reference this type.

Both Scala-typed registerParts descriptors are preserved, since
ForgeRelocationFMP links against the Function2 form and ProjRed against
the IPartFactory2 Seq form. That leaves TileMultipart.partList as the last
Scala-typed descriptor in the inventory.

The two array overloads are declared varargs. The descriptor is unchanged
and ACC_VARARGS does not affect linkage, but MicroblockClass called the
Scala varargs form with a single string, which no longer resolves against
a plain array parameter.

readIDMap moves to java.util.List and the mangled accessors Scala emitted
for its closures are gone; neither is referenced by any jar. Eight anonfun
classes and one anonymous class are replaced by the two Java adapters the
deprecated overloads build.

Validated with 78 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite, which is the only layer where registration can run
at all since it needs FML's active mod container.
This commit is contained in:
Algent committed 2026-08-15 10:35:15 +02:00
1 parent ff380ee3bc
commit b9d5e6ab52
4 files changed
+439 -247

No files matched your search

+54
View File
@@ -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<String>` instead of `scala.collection.Seq<String>`, 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.
@@ -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<String, Object, TMultiPart> partFactory,
scala.collection.Seq<String> types) {
MultiPartRegistry.registerParts(partFactory, types);
}
public void registerParts(IPartFactory2 partFactory, String... types) {
MultiPartRegistry.registerParts(partFactory, types);
}
public void registerParts(IPartFactory2 partFactory, scala.collection.Seq<String> 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<String> 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);
}
}
@@ -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<Block> 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<String, IPartFactory2> typeMap = new HashMap<>();
private static final Map<String, Integer> nameMap = new HashMap<>();
private static Tuple2<String, IPartFactory2>[] idMap;
private static final IDWriter idWriter = new IDWriter();
private static final ArrayListMultimap<Block, IPartConverter> converters = ArrayListMultimap.create();
private static final Map<String, ModContainer> 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<String, Object, TMultiPart> partFactory,
scala.collection.Seq<String> 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<String> 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<Map.Entry<String, IPartFactory2>> 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<String, IPartFactory2> 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<String> readIDMap(PacketCustom packet) {
int k = packet.readInt();
idWriter.setMax(k);
idMap = new Tuple2[k];
nameMap.clear();
List<String> 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<String, IPartFactory2> 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;
}
}
@@ -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)
}