Files
ForgeMultipart/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java
T
Algent edd6136354 refactor: port MicroMaterialRegistry to Java
Replaces MicroMaterialRegistry.scala with a Java registry, its companion
singleton and the two nested interfaces. Both nested interfaces are
descriptor-identical to the reference and MODULE$ keeps every instance
method ProjRed links against. Ten jars reference this type and three more
reach it reflectively, so the surface was checked member by member rather
than by descriptor count.

getIdMap keeps its scala.Tuple2 array because extrautilities links against
it, so the registry still stores tuples internally. readIDMap moves from
scala.collection.Seq to java.util.List, which no jar references; the one
in-repo caller moved from mkString to String.join. That clears one of the
five Scala-typed descriptors the inventory identified.

Three removals, all verified unreferenced: the mangled public accessors
Scala emitted so its closure classes could reach private state,
IMicroMaterial$class, and the MissingMicroMaterial forwarders for the two
members it does not override. The last of those was caught by the
characterization failing to compile, not by inspection.

IMicroMaterial's loadIcons, canRenderInPass and isSolid become real Java
defaults, so implementors can inherit rather than restate them. Eleven
anonfun classes are gone.

Validated with 73 plain-JVM tests passing against both the Scala baseline
and the port, a clean build, and the Java 8 Forge dedicated-server suite,
which exercises setupIDMap and calcMaxCuttingStrength during real startup.
2026-08-14 22:08:30 +02:00

245 lines
8.7 KiB
Java

package codechicken.microblock;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Proxy;
import net.minecraft.block.Block.SoundType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraft.util.MovingObjectPosition;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import codechicken.lib.data.MCDataInput;
import codechicken.lib.data.MCDataOutput;
import codechicken.lib.data.MCDataOutputWrapper;
import codechicken.lib.vec.Cuboid6;
import codechicken.lib.vec.Vector3;
import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer;
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial;
/**
* The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the
* cases below only read it. Error paths are not covered: they call the microblock logger, which is null until
* MicroblockProxy.preInit runs, so they cannot execute headless.
*/
class MicroMaterialRegistryCharacterizationTest {
private static final TestMaterial STONE = new TestMaterial(false, 3);
private static final TestMaterial GLASS = new TestMaterial(true, 1);
@BeforeAll
static void registerAndFreezeIdMap() {
// Registration is global and re-registering hits the logger-backed error path, which cannot run headless.
if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) {
MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key());
}
MicroMaterialRegistry.registerMaterial(STONE, "test:stone");
MicroMaterialRegistry.registerMaterial(GLASS, "test:glass");
MicroMaterialRegistry.remapName("test:oldglass", "test:glass");
// setupIDMap is package private in Scala, so it has no static forwarder and must go through the singleton.
MicroMaterialRegistry$.MODULE$.setupIDMap();
}
@Test
void idMapIsSortedByNameAndDrivesBothLookups() {
String[] names = new String[MicroMaterialRegistry.getIdMap().length];
for (int i = 0; i < names.length; i++) {
names[i] = MicroMaterialRegistry.getIdMap()[i]._1();
}
String[] sorted = names.clone();
java.util.Arrays.sort(sorted);
assertArrayEquals(sorted, names);
for (int i = 0; i < names.length; i++) {
assertEquals(i, MicroMaterialRegistry.materialID(names[i]));
assertEquals(names[i], MicroMaterialRegistry.materialName(i));
assertSame(MicroMaterialRegistry.getIdMap()[i]._2(), MicroMaterialRegistry.getMaterial(i));
}
}
@Test
void getMaterialByNameResolvesRemapsAndReturnsNullWhenUnknown() {
assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:glass"));
assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:oldglass"));
assertNull(MicroMaterialRegistry.getMaterial("test:nosuchmaterial"));
}
@Test
void materialIdResolvesRemappedNames() {
assertEquals(MicroMaterialRegistry.materialID("test:glass"), MicroMaterialRegistry.materialID("test:oldglass"));
}
@Test
void missingIdPointsAtTheMissingMaterialRatherThanZero() {
assertEquals(
MicroMaterialRegistry.materialID(MissingMicroMaterial.key()),
MicroMaterialRegistry.getMissingId());
assertSame(
MissingMicroMaterial$.MODULE$,
MicroMaterialRegistry.getMaterial(MicroMaterialRegistry.getMissingId()));
}
@Test
void materialIdsRoundTripThroughTheIdWriter() throws Exception {
for (int id = 0; id < MicroMaterialRegistry.getIdMap().length; id++) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes));
MicroMaterialRegistry.writeMaterialID(output, id);
byte[] encoded = bytes.toByteArray();
assertEquals(1, encoded.length, "A small id map must still use the byte carrier");
assertEquals(id, MicroMaterialRegistry.readMaterialID(input(encoded)));
}
}
@Test
void highlightRendererShortCircuitsTheDefaultRenderer() {
RecordingRenderer accepting = new RecordingRenderer(true);
MicroMaterialRegistry.registerHighlightRenderer(accepting);
// A renderer that claims the highlight stops the default client renderer being reached at all.
assertTrue(MicroMaterialRegistry.renderHighlight(null, null, null, 2, 0));
assertEquals(1, accepting.calls);
}
/**
* MissingMicroMaterial overrides only isTransparent and loadIcons, so it inherits canRenderInPass and isSolid from
* the trait. A Java class cannot inherit them today, so this is the only way to observe the supplied values.
*/
@Test
void materialInterfaceSuppliesRenderPassAndSolidity() {
IMicroMaterial missing = MissingMicroMaterial$.MODULE$;
assertTrue(missing.canRenderInPass(0));
assertFalse(missing.canRenderInPass(1));
assertFalse(missing.canRenderInPass(-1));
assertFalse(missing.isTransparent());
assertTrue(missing.isSolid());
}
private static MCDataInput input(byte[] bytes) {
DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes));
return (MCDataInput) Proxy.newProxyInstance(
MCDataInput.class.getClassLoader(),
new Class<?>[] { MCDataInput.class },
(proxy, method, arguments) -> {
switch (method.getName()) {
case "readUByte":
return (short) data.readUnsignedByte();
case "readUShort":
return data.readUnsignedShort();
case "readInt":
return data.readInt();
default:
throw new AssertionError("Unexpected read method: " + method.getName());
}
});
}
private static final class RecordingRenderer implements IMicroHighlightRenderer {
private final boolean handled;
private int calls;
private RecordingRenderer(boolean handled) {
this.handled = handled;
}
@Override
public boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass,
int size, int material) {
calls++;
return handled;
}
}
/** Mirrors the forwarders Scala emits, so this file compiles against the trait and against the Java interface. */
private static final class TestMaterial implements IMicroMaterial {
private final boolean transparent;
private final int cutterStrength;
private TestMaterial(boolean transparent, int cutterStrength) {
this.transparent = transparent;
this.cutterStrength = cutterStrength;
}
@Override
public IIcon getBreakingIcon(int side) {
return null;
}
@Override
public void loadIcons() {}
@Override
public void renderMicroFace(Vector3 pos, int pass, Cuboid6 bounds) {}
@Override
public boolean canRenderInPass(int pass) {
return pass == 0;
}
@Override
public boolean isTransparent() {
return transparent;
}
@Override
public int getLightValue() {
return 0;
}
@Override
public float getStrength(EntityPlayer player) {
return 1f;
}
@Override
public String getLocalizedName() {
return "Test Material";
}
@Override
public ItemStack getItem() {
return null;
}
@Override
public int getCutterStrength() {
return cutterStrength;
}
@Override
public SoundType getSound() {
return null;
}
@Override
public boolean isSolid() {
return !isTransparent();
}
@Override
public float explosionResistance(Entity entity) {
return 1f;
}
}
}