2026-08-14 22:03:04 +02:00
|
|
|
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;
|
2026-09-04 23:20:29 +02:00
|
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
2026-08-14 22:03:04 +02:00
|
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
|
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
|
import java.io.DataInputStream;
|
|
|
|
|
import java.io.DataOutputStream;
|
2026-09-04 23:20:29 +02:00
|
|
|
import java.lang.reflect.Field;
|
2026-08-14 22:03:04 +02:00
|
|
|
import java.lang.reflect.Proxy;
|
2026-09-04 23:20:29 +02:00
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.Collections;
|
2026-08-14 22:03:04 +02:00
|
|
|
|
|
|
|
|
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;
|
2026-09-04 23:20:29 +02:00
|
|
|
import codechicken.lib.packet.PacketCustom;
|
2026-08-14 22:03:04 +02:00
|
|
|
import codechicken.lib.vec.Cuboid6;
|
|
|
|
|
import codechicken.lib.vec.Vector3;
|
|
|
|
|
import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer;
|
|
|
|
|
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial;
|
2026-09-04 23:30:54 +02:00
|
|
|
import codechicken.microblock.examples.MaterialEnumerationExample;
|
2026-08-14 22:03:04 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the
|
2026-09-04 23:30:54 +02:00
|
|
|
* cases below restore any temporary state changes. Logger-backed error paths require MicroblockProxy.preInit and cannot
|
|
|
|
|
* execute headless.
|
2026-08-14 22:03:04 +02:00
|
|
|
*/
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 23:20:29 +02:00
|
|
|
@Test
|
|
|
|
|
void legacyIdMapIsTheSharedArrayAndNullBeforeInitialization() throws Exception {
|
|
|
|
|
Object original = MicroMaterialRegistry.getIdMap();
|
|
|
|
|
assertSame(original, MicroMaterialRegistry.getIdMap());
|
|
|
|
|
assertSame(original, MicroMaterialRegistry$.MODULE$.getIdMap());
|
|
|
|
|
Field idMap = MicroMaterialRegistry.class.getDeclaredField("idMap");
|
|
|
|
|
idMap.setAccessible(true);
|
|
|
|
|
try {
|
|
|
|
|
idMap.set(null, null);
|
|
|
|
|
assertNull(MicroMaterialRegistry.getIdMap());
|
|
|
|
|
assertNull(MicroMaterialRegistry$.MODULE$.getIdMap());
|
|
|
|
|
assertThrows(NullPointerException.class, () -> MicroMaterialRegistry.materialName(0));
|
|
|
|
|
assertThrows(NullPointerException.class, () -> MicroMaterialRegistry.getMaterial(0));
|
|
|
|
|
} finally {
|
|
|
|
|
idMap.set(null, original);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void serverMapControlsOrderAndRetainsMissingSlotsAndEmptyMaps() {
|
|
|
|
|
Object original = MicroMaterialRegistry.getIdMap();
|
|
|
|
|
try {
|
|
|
|
|
assertEquals(
|
|
|
|
|
Collections.emptyList(),
|
|
|
|
|
MicroMaterialRegistry.readIDMap(incomingMap("test:stone", "test:glass")));
|
|
|
|
|
assertEquals(2, MicroMaterialRegistry.getIdMap().length);
|
|
|
|
|
assertEquals("test:stone", MicroMaterialRegistry.materialName(0));
|
|
|
|
|
assertSame(STONE, MicroMaterialRegistry.getMaterial(0));
|
|
|
|
|
assertEquals("test:glass", MicroMaterialRegistry.materialName(1));
|
|
|
|
|
assertSame(GLASS, MicroMaterialRegistry.getMaterial(1));
|
|
|
|
|
assertEquals(0, MicroMaterialRegistry.materialID("test:stone"));
|
|
|
|
|
assertEquals(1, MicroMaterialRegistry.materialID("test:oldglass"));
|
|
|
|
|
|
|
|
|
|
assertEquals(
|
|
|
|
|
Arrays.asList("server:unavailable"),
|
|
|
|
|
MicroMaterialRegistry.readIDMap(incomingMap("test:stone", "server:unavailable", "test:glass")));
|
|
|
|
|
assertEquals(3, MicroMaterialRegistry.getIdMap().length);
|
|
|
|
|
assertNull(MicroMaterialRegistry.getIdMap()[1]);
|
|
|
|
|
assertEquals("test:glass", MicroMaterialRegistry.materialName(2));
|
|
|
|
|
assertThrows(NullPointerException.class, () -> MicroMaterialRegistry.materialName(1));
|
|
|
|
|
assertThrows(NullPointerException.class, () -> MicroMaterialRegistry.getMaterial(1));
|
|
|
|
|
|
|
|
|
|
assertEquals(Collections.emptyList(), MicroMaterialRegistry.readIDMap(incomingMap()));
|
|
|
|
|
assertEquals(0, MicroMaterialRegistry.getIdMap().length);
|
|
|
|
|
assertThrows(ArrayIndexOutOfBoundsException.class, () -> MicroMaterialRegistry.materialName(0));
|
|
|
|
|
assertThrows(ArrayIndexOutOfBoundsException.class, () -> MicroMaterialRegistry.getMaterial(-1));
|
|
|
|
|
} finally {
|
|
|
|
|
MicroMaterialRegistry$.MODULE$.setupIDMap();
|
|
|
|
|
}
|
|
|
|
|
assertEquals(java.lang.reflect.Array.getLength(original), MicroMaterialRegistry.getIdMap().length);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 23:30:54 +02:00
|
|
|
@Test
|
|
|
|
|
void javaExampleEnumeratesTheSameMaterialsWithoutExposingTheLegacyArray() throws Exception {
|
|
|
|
|
String[] names = Arrays.stream(MicroMaterialRegistry.getIdMap()).map(entry -> entry._1())
|
|
|
|
|
.toArray(String[]::new);
|
|
|
|
|
assertEquals(names.length, MicroMaterialRegistry.materialCount());
|
|
|
|
|
assertEquals(Arrays.asList(names), MaterialEnumerationExample.materialNames());
|
|
|
|
|
assertTrue(MicroMaterialRegistry.class.getMethod("getIdMap").isAnnotationPresent(Deprecated.class));
|
|
|
|
|
assertTrue(MicroMaterialRegistry$.class.getMethod("getIdMap").isAnnotationPresent(Deprecated.class));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void javaEnumerationTracksServerOrderMissingSlotsAndEmptyMaps() {
|
|
|
|
|
try {
|
|
|
|
|
MicroMaterialRegistry.readIDMap(incomingMap("test:stone", "test:glass"));
|
|
|
|
|
assertEquals(2, MicroMaterialRegistry.materialCount());
|
|
|
|
|
assertEquals(Arrays.asList("test:stone", "test:glass"), MaterialEnumerationExample.materialNames());
|
|
|
|
|
|
|
|
|
|
MicroMaterialRegistry.readIDMap(incomingMap("test:stone", "server:unavailable", "test:glass"));
|
|
|
|
|
assertEquals(3, MicroMaterialRegistry.materialCount());
|
|
|
|
|
assertThrows(NullPointerException.class, MaterialEnumerationExample::materialNames);
|
|
|
|
|
|
|
|
|
|
MicroMaterialRegistry.readIDMap(incomingMap());
|
|
|
|
|
assertEquals(0, MicroMaterialRegistry.materialCount());
|
|
|
|
|
assertEquals(Collections.emptyList(), MaterialEnumerationExample.materialNames());
|
|
|
|
|
} finally {
|
|
|
|
|
MicroMaterialRegistry$.MODULE$.setupIDMap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void materialCountRejectsAnUninitializedMapInsteadOfReportingAnEmptyRegistry() throws Exception {
|
|
|
|
|
Field idMap = MicroMaterialRegistry.class.getDeclaredField("idMap");
|
|
|
|
|
idMap.setAccessible(true);
|
|
|
|
|
Object original = idMap.get(null);
|
|
|
|
|
try {
|
|
|
|
|
idMap.set(null, null);
|
|
|
|
|
assertThrows(IllegalStateException.class, MicroMaterialRegistry::materialCount);
|
|
|
|
|
} finally {
|
|
|
|
|
idMap.set(null, original);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 22:03:04 +02:00
|
|
|
@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() {
|
2026-08-14 22:08:30 +02:00
|
|
|
IMicroMaterial missing = MissingMicroMaterial$.MODULE$;
|
2026-08-14 22:03:04 +02:00
|
|
|
|
2026-08-14 22:08:30 +02:00
|
|
|
assertTrue(missing.canRenderInPass(0));
|
|
|
|
|
assertFalse(missing.canRenderInPass(1));
|
|
|
|
|
assertFalse(missing.canRenderInPass(-1));
|
|
|
|
|
|
|
|
|
|
assertFalse(missing.isTransparent());
|
|
|
|
|
assertTrue(missing.isSolid());
|
2026-08-14 22:03:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 23:20:29 +02:00
|
|
|
private static PacketCustom incomingMap(String... names) {
|
|
|
|
|
PacketCustom packet = new PacketCustom("test", 1).writeInt(names.length);
|
|
|
|
|
for (String name : names) {
|
|
|
|
|
packet.writeString(name);
|
|
|
|
|
}
|
|
|
|
|
return new PacketCustom(packet.getByteBuf().copy());
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 22:03:04 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|