test: characterize micro occlusion

This commit is contained in:
Algent committed 2026-09-02 10:21:12 +02:00
1 parent 33510e74bd
commit f57f52408b
1 file changed
+443
@@ -0,0 +1,443 @@
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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.Type;
import codechicken.lib.vec.Cuboid6;
import codechicken.multipart.TMultiPart;
import codechicken.multipart.TileMultipart;
class MicroOcclusionCharacterizationTest {
private static final int[] VALID_SLOTS = { 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26 };
private static final Set<String> OCCLUSION_METHODS = signatures(
"recalcBounds(Lcodechicken/microblock/JMicroShrinkRender;Lcodechicken/lib/vec/Cuboid6;)I",
"shapePriority(I)I",
"shrink(Lcodechicken/lib/vec/Cuboid6;Lcodechicken/lib/vec/Cuboid6;I)V",
"shrink(Lcodechicken/microblock/JMicroShrinkRender;Lcodechicken/lib/vec/Cuboid6;I)I",
"shrinkFrom(Lcodechicken/microblock/JMicroShrinkRender;Lcodechicken/microblock/JMicroShrinkRender;Lcodechicken/lib/vec/Cuboid6;)I",
"shrinkSide(II)I",
"shrinkTest(Lcodechicken/microblock/JMicroShrinkRender;Lcodechicken/microblock/JMicroShrinkRender;)Z");
@Test
void keepsFacadeCompanionAndGeneratedTraitHelperSurfaces() throws Exception {
assertTrue(Modifier.isFinal(MicroOcclusion.class.getModifiers()));
assertEquals(0, MicroOcclusion.class.getDeclaredFields().length);
assertEquals(OCCLUSION_METHODS, publicDeclaredMethods(MicroOcclusion.class));
for (Method method : MicroOcclusion.class.getDeclaredMethods()) {
assertTrue(Modifier.isStatic(method.getModifiers()), method.toString());
}
assertTrue(Modifier.isFinal(MicroOcclusion$.class.getModifiers()));
assertEquals(OCCLUSION_METHODS, publicDeclaredMethods(MicroOcclusion$.class));
Field module = MicroOcclusion$.class.getField("MODULE$");
assertTrue(Modifier.isStatic(module.getModifiers()));
assertTrue(Modifier.isFinal(module.getModifiers()));
assertEquals(1, MicroOcclusion$.class.getDeclaredFields().length);
assertTrait(
JMicroShrinkRender.class,
new Class<?>[0],
signatures(
"getBounds()Lcodechicken/lib/vec/Cuboid6;",
"getPriorityClass()I",
"getSize()I",
"getSlot()I",
"isTransparent()Z"));
assertThrows(
ClassNotFoundException.class,
() -> Class.forName("codechicken.microblock.JMicroShrinkRender$class"));
assertTrait(
TMicroOcclusion.class,
new Class<?>[0],
signatures(
"codechicken$microblock$TMicroOcclusion$$super$occlusionTest(Lcodechicken/multipart/TMultiPart;)Z",
"edgeCornerOcclusionTest(Lcodechicken/microblock/TMicroOcclusion;Lcodechicken/microblock/TMicroOcclusion;)Z",
"getBounds()Lcodechicken/lib/vec/Cuboid6;",
"getMaterial()I",
"getSize()I",
"getSlot()I",
"occlusionTest(Lcodechicken/multipart/TMultiPart;)Z"));
assertHelper(
"codechicken.microblock.TMicroOcclusion$class",
signatures(
"$init$(Lcodechicken/microblock/TMicroOcclusion;)V",
"edgeCornerOcclusionTest(Lcodechicken/microblock/TMicroOcclusion;Lcodechicken/microblock/TMicroOcclusion;Lcodechicken/microblock/TMicroOcclusion;)Z",
"occlusionTest(Lcodechicken/microblock/TMicroOcclusion;Lcodechicken/multipart/TMultiPart;)Z"));
assertTrait(
TMicroOcclusionClient.class,
new Class<?>[] { TMicroOcclusion.class, JMicroShrinkRender.class },
signatures(
"codechicken$microblock$TMicroOcclusionClient$$super$onAdded()V",
"codechicken$microblock$TMicroOcclusionClient$$super$onPartChanged(Lcodechicken/multipart/TMultiPart;)V",
"codechicken$microblock$TMicroOcclusionClient$$super$read(Lcodechicken/lib/data/MCDataInput;)V",
"getPriorityClass()I",
"onAdded()V",
"onPartChanged(Lcodechicken/multipart/TMultiPart;)V",
"read(Lcodechicken/lib/data/MCDataInput;)V",
"recalcBounds()V",
"renderBounds()Lcodechicken/lib/vec/Cuboid6;",
"renderBounds_$eq(Lcodechicken/lib/vec/Cuboid6;)V",
"renderMask()I",
"renderMask_$eq(I)V"));
assertHelper(
"codechicken.microblock.TMicroOcclusionClient$class",
signatures(
"$init$(Lcodechicken/microblock/TMicroOcclusionClient;)V",
"getPriorityClass(Lcodechicken/microblock/TMicroOcclusionClient;)I",
"onAdded(Lcodechicken/microblock/TMicroOcclusionClient;)V",
"onPartChanged(Lcodechicken/microblock/TMicroOcclusionClient;Lcodechicken/multipart/TMultiPart;)V",
"read(Lcodechicken/microblock/TMicroOcclusionClient;Lcodechicken/lib/data/MCDataInput;)V",
"recalcBounds(Lcodechicken/microblock/TMicroOcclusionClient;)V"));
assertTrue(
TMicroOcclusion.class.getDeclaredMethod(
"codechicken$microblock$TMicroOcclusion$$super$occlusionTest",
TMultiPart.class).isSynthetic());
assertTrue(
TMicroOcclusionClient.class
.getDeclaredMethod("codechicken$microblock$TMicroOcclusionClient$$super$onAdded")
.isSynthetic());
assertTrue(
TMicroOcclusionClient.class.getDeclaredMethod(
"codechicken$microblock$TMicroOcclusionClient$$super$onPartChanged",
TMultiPart.class).isSynthetic());
}
@Test
void freezesAllValidSlotPriorityAndShrinkDecisions() throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (int slot : VALID_SLOTS) {
int priority = MicroOcclusion.shapePriority(slot);
assertEquals(priority, MicroOcclusion$.MODULE$.shapePriority(slot));
update(digest, priority);
}
for (int first : VALID_SLOTS) {
for (int second : VALID_SLOTS) {
int side = MicroOcclusion.shrinkSide(first, second);
assertEquals(side, MicroOcclusion$.MODULE$.shrinkSide(first, second));
update(digest, side);
}
}
int[] priorities = { -1, 0, 1 };
int[] sizes = { 1, 4, 7 };
for (int firstSlot : VALID_SLOTS) {
for (int secondSlot : VALID_SLOTS) {
for (int firstPriority : priorities) {
for (int secondPriority : priorities) {
for (int firstSize : sizes) {
for (int secondSize : sizes) {
for (boolean firstTransparent : new boolean[] { false, true }) {
for (boolean secondTransparent : new boolean[] { false, true }) {
TestPart first = new TestPart(
firstSlot,
firstSize,
firstPriority,
firstTransparent,
Cuboid6.full);
TestPart second = new TestPart(
secondSlot,
secondSize,
secondPriority,
secondTransparent,
Cuboid6.full);
boolean shrinks = MicroOcclusion.shrinkTest(first, second);
assertEquals(shrinks, MicroOcclusion$.MODULE$.shrinkTest(first, second));
digest.update((byte) (shrinks ? 1 : 0));
}
}
}
}
}
}
}
}
assertEquals("89add86e47ae62da740d85b76193fbd10e791edcdf53fb3c48737349b2483abe", hex(digest.digest()));
}
@Test
void shrinksEachAxisWithoutExpandingExistingBounds() {
Cuboid6 obstacle = new Cuboid6(0.2, 0.3, 0.4, 0.8, 0.7, 0.6);
Cuboid6[] expected = { Cuboid6.full.copy(), new Cuboid6(0, 0.7, 0, 1, 1, 1), new Cuboid6(0, 0, 0, 1, 0.3, 1),
new Cuboid6(0, 0, 0.6, 1, 1, 1), new Cuboid6(0, 0, 0, 1, 1, 0.4), new Cuboid6(0.8, 0, 0, 1, 1, 1),
new Cuboid6(0, 0, 0, 0.2, 1, 1) };
for (int side = -1; side < 6; side++) {
Cuboid6 actual = Cuboid6.full.copy();
MicroOcclusion.shrink(actual, obstacle, side);
assertCuboid(expected[side + 1], actual);
}
Cuboid6 alreadyInside = new Cuboid6(0.9, 0.8, 0.7, 0.1, 0.2, 0.3);
Cuboid6 original = alreadyInside.copy();
for (int side = 0; side < 6; side++) {
MicroOcclusion.shrink(alreadyInside, obstacle, side);
}
assertCuboid(original, alreadyInside);
}
@Test
void keepsShrinkMaskAndSlotTraversalRules() {
TestPart shrinking = new TestPart(15, 4, 0, false, Cuboid6.full);
TestPart face = new TestPart(0, 4, 1, false, new Cuboid6(0, 0, 0, 1, 0.25, 1));
Cuboid6 bounds = Cuboid6.full.copy();
assertEquals(0, MicroOcclusion.shrinkFrom(shrinking, face, bounds));
assertEquals(0.25, bounds.min.y);
TestPart yielding = new TestPart(15, 4, 1, false, Cuboid6.full);
TestPart opaqueFace = new TestPart(0, 4, 0, false, Cuboid6.full);
assertEquals(1, MicroOcclusion.shrinkFrom(yielding, opaqueFace, Cuboid6.full.copy()));
opaqueFace.transparent = true;
assertEquals(0, MicroOcclusion.shrinkFrom(yielding, opaqueFace, Cuboid6.full.copy()));
opaqueFace.transparent = false;
assertEquals(0, MicroOcclusion.shrinkFrom(yielding, opaqueFace, new Cuboid6(0, 0.1, 0, 1, 1, 1)));
RecordingTile tile = new RecordingTile();
shrinking.bind(tile);
tile.parts[0] = face;
bounds = Cuboid6.full.copy();
assertEquals(0, MicroOcclusion.shrink(shrinking, bounds, 6));
assertEquals(0.25, bounds.min.y);
tile.parts[0] = null;
assertTraversal(shrinking, tile, 0, 5, 5);
assertTraversal(shrinking, tile, 7, 14, 14);
assertTraversal(shrinking, tile, 15, 26, 26);
}
@Test
void freezesCompleteMicroblockOcclusionDecisions() throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
TestOcclusionPart first = new TestOcclusionPart();
TestOcclusionPart second = new TestOcclusionPart();
for (int firstSlot : VALID_SLOTS) {
for (int secondSlot : VALID_SLOTS) {
for (int firstSize = 1; firstSize < 8; firstSize++) {
for (int secondSize = 1; secondSize < 8; secondSize++) {
for (int secondMaterial = 0; secondMaterial < 2; secondMaterial++) {
first.set(firstSlot, firstSize, 0);
second.set(secondSlot, secondSize, secondMaterial);
digest.update((byte) (first.occlusionTest(second) ? 1 : 0));
}
}
}
}
}
for (int edge = 15; edge < 27; edge++) {
for (int corner = 7; corner < 15; corner++) {
first.set(edge, 4, 0);
second.set(corner, 4, 1);
digest.update((byte) (first.edgeCornerOcclusionTest(first, second) ? 1 : 0));
}
}
assertEquals("089ef2772b6f50b07b8c09a1733bc0e2a6e2006c134095b19f797b0b37ceb55b", hex(digest.digest()));
assertTrue(first.occlusionTest(new BarePart()));
first.superResult = false;
assertFalse(first.occlusionTest(second));
}
private static void assertTraversal(TestPart part, RecordingTile tile, int slot, int lastSlot, int calls) {
part.slot = slot;
tile.visited.clear();
assertEquals(0, MicroOcclusion.recalcBounds(part, Cuboid6.full.copy()));
assertEquals(calls, tile.visited.size());
assertEquals(lastSlot, tile.visited.get(tile.visited.size() - 1));
assertFalse(tile.visited.contains(slot));
}
private static void assertTrait(Class<?> type, Class<?>[] interfaces, Set<String> methods) {
assertTrue(type.isInterface());
assertArrayEquals(interfaces, type.getInterfaces());
assertEquals(0, type.getDeclaredFields().length);
assertEquals(methods, publicDeclaredMethods(type));
}
private static void assertHelper(String name, Set<String> methods) throws Exception {
Class<?> type = Class.forName(name, false, MicroOcclusionCharacterizationTest.class.getClassLoader());
assertTrue(Modifier.isPublic(type.getModifiers()));
assertTrue(Modifier.isAbstract(type.getModifiers()));
assertEquals(0, type.getDeclaredFields().length);
assertEquals(methods, publicDeclaredMethods(type));
}
private static Set<String> publicDeclaredMethods(Class<?> type) {
Set<String> methods = new TreeSet<>();
for (Method method : type.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
methods.add(method.getName() + Type.getMethodDescriptor(method));
}
}
return methods;
}
private static Set<String> signatures(String... signatures) {
return new TreeSet<>(Arrays.asList(signatures));
}
private static void update(MessageDigest digest, int value) {
digest.update((byte) (value >>> 24));
digest.update((byte) (value >>> 16));
digest.update((byte) (value >>> 8));
digest.update((byte) value);
}
private static String hex(byte[] bytes) {
StringBuilder result = new StringBuilder(bytes.length * 2);
for (byte value : bytes) {
result.append(Character.forDigit(value >>> 4 & 0xF, 16));
result.append(Character.forDigit(value & 0xF, 16));
}
return result.toString();
}
private static void assertCuboid(Cuboid6 expected, Cuboid6 actual) {
assertEquals(expected.min.x, actual.min.x);
assertEquals(expected.min.y, actual.min.y);
assertEquals(expected.min.z, actual.min.z);
assertEquals(expected.max.x, actual.max.x);
assertEquals(expected.max.y, actual.max.y);
assertEquals(expected.max.z, actual.max.z);
}
private static class TestPart extends TMultiPart implements JMicroShrinkRender {
private int slot;
private final int size;
private final int priority;
private boolean transparent;
private final Cuboid6 bounds;
private TestPart(int slot, int size, int priority, boolean transparent, Cuboid6 bounds) {
this.slot = slot;
this.size = size;
this.priority = priority;
this.transparent = transparent;
this.bounds = bounds;
}
@Override
public String getType() {
return "test_shrink";
}
@Override
public int getPriorityClass() {
return priority;
}
@Override
public int getSlot() {
return slot;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isTransparent() {
return transparent;
}
@Override
public Cuboid6 getBounds() {
return bounds;
}
}
private static final class TestOcclusionPart extends TMultiPart implements TMicroOcclusion {
private int slot;
private int size;
private int material;
private boolean superResult = true;
private void set(int slot, int size, int material) {
this.slot = slot;
this.size = size;
this.material = material;
superResult = true;
}
@Override
public String getType() {
return "test_occlusion";
}
@Override
public int getSlot() {
return slot;
}
@Override
public int getSize() {
return size;
}
@Override
public int getMaterial() {
return material;
}
@Override
public Cuboid6 getBounds() {
return Cuboid6.full;
}
@Override
public boolean occlusionTest(TMultiPart part) {
return TMicroOcclusion$class.occlusionTest(this, part);
}
@Override
public boolean edgeCornerOcclusionTest(TMicroOcclusion edge, TMicroOcclusion corner) {
return TMicroOcclusion$class.edgeCornerOcclusionTest(this, edge, corner);
}
public boolean codechicken$microblock$TMicroOcclusion$$super$occlusionTest(TMultiPart part) {
return superResult;
}
}
private static final class BarePart extends TMultiPart {
@Override
public String getType() {
return "bare";
}
}
private static final class RecordingTile extends TileMultipart {
private final TMultiPart[] parts = new TMultiPart[27];
private final List<Integer> visited = new ArrayList<>();
@Override
public TMultiPart partMap(int slot) {
visited.add(slot);
return parts[slot];
}
}
}