From a69f84677566579892843d484aff2fde65cc0d6a Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 14 Aug 2026 17:38:53 +0200 Subject: [PATCH] test: characterize normal occlusion Freeze contact tolerance and partial-box delegation before the Java port. --- JAVA_MIGRATION.md | 2 + .../NormalOcclusionCharacterizationTest.java | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/test/java/codechicken/multipart/NormalOcclusionCharacterizationTest.java diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 10ad4ef..a62545f 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -417,3 +417,5 @@ These ranges were formed before designing the characterization suite and should - Passed the first two Forge integration checks against the untouched Scala implementation: complete server lifecycle/world startup with all three ForgeMultipart mod IDs loaded, and generation plus constructor-class caching of a `TSlottedTile` composite with its 27-slot map. - Hardened the Gradle wrapper to delete stale reports and fail when no fresh report is produced, no tests run, or any failure/error is reported. This is necessary because Forge 1.7.10 can terminate after a test-mod exception with process exit code 0. - Confirmed the Java 8 server lane on Azul OpenJDK 8u492. The current deobfuscated modern-server tasks are not usable as release evidence: Java 21 needs the legacy Security Manager switch and then `Lwjgl3ifyRelauncherTweaker` rejects the server launch; Java 25 rejects `System.setSecurityManager` before Forge loads. Modern-Java validation therefore remains a packaged-server gate rather than a custom test-runner workaround. +- Added eight plain-JVM characterization cases for `NormalOcclusionTest` and `NormallyOccludedPart`: empty sets, separation on every axis, face contact, tolerance-sized overlap, real overlap, containment, identical boxes, multiple-box all-pairs behavior, normal-box delegation, partial-box delegation, and parts without occlusion interfaces. +- Confirmed that normal occlusion returns `true` when parts may coexist, despite the source comment saying “true if the test fails.” Face contact and overlaps no larger than CodeChickenLib's `1e-5` intersection tolerance are accepted; larger overlaps reject placement. Preserve the behavior and correct the misleading documentation separately when this area is ported. diff --git a/src/test/java/codechicken/multipart/NormalOcclusionCharacterizationTest.java b/src/test/java/codechicken/multipart/NormalOcclusionCharacterizationTest.java new file mode 100644 index 0000000..698c4c4 --- /dev/null +++ b/src/test/java/codechicken/multipart/NormalOcclusionCharacterizationTest.java @@ -0,0 +1,143 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Cuboid6; +import scala.collection.Traversable; +import scala.collection.mutable.ArrayBuffer; + +class NormalOcclusionCharacterizationTest { + + @Test + void emptyInputsAreCompatible() { + Cuboid6 box = box(0, 0, 0, 1, 1, 1); + + assertTrue(NormalOcclusionTest.apply(scalaBoxes(), scalaBoxes())); + assertTrue(NormalOcclusionTest.apply(scalaBoxes(box), scalaBoxes())); + assertTrue(NormalOcclusionTest.apply(scalaBoxes(), scalaBoxes(box))); + } + + @Test + void separationOnAnyAxisIsCompatible() { + Cuboid6 origin = box(0, 0, 0, 0.25, 0.25, 0.25); + Cuboid6[] separated = { box(0.5, 0, 0, 0.75, 0.25, 0.25), box(0, 0.5, 0, 0.25, 0.75, 0.25), + box(0, 0, 0.5, 0.25, 0.25, 0.75) }; + + for (Cuboid6 other : separated) { + assertTrue(compatible(origin, other)); + assertTrue(compatible(other, origin)); + } + } + + @Test + void touchingAndSubToleranceOverlapAreCompatible() { + Cuboid6 origin = box(0, 0, 0, 0.5, 1, 1); + + assertTrue(compatible(origin, box(0.5, 0, 0, 1, 1, 1))); + assertTrue(compatible(origin, box(0.499995, 0, 0, 1, 1, 1))); + assertFalse(compatible(origin, box(0.49998, 0, 0, 1, 1, 1))); + } + + @Test + void partialOverlapContainmentAndIdentityAreIncompatible() { + Cuboid6 origin = box(0, 0, 0, 0.75, 0.75, 0.75); + + assertFalse(compatible(origin, box(0.5, 0.5, 0.5, 1, 1, 1))); + assertFalse(compatible(origin, box(0.1, 0.1, 0.1, 0.2, 0.2, 0.2))); + assertFalse(compatible(origin, box(0, 0, 0, 0.75, 0.75, 0.75))); + } + + @Test + void everyBoxPairMustBeCompatible() { + Traversable existing = scalaBoxes( + box(0, 0, 0, 0.2, 0.2, 0.2), + box(0.8, 0.8, 0.8, 1, 1, 1)); + + assertTrue(NormalOcclusionTest.apply(existing, scalaBoxes(box(0.3, 0.3, 0.3, 0.4, 0.4, 0.4)))); + assertFalse(NormalOcclusionTest.apply( + existing, + scalaBoxes(box(0.3, 0.3, 0.3, 0.4, 0.4, 0.4), box(0.9, 0.9, 0.9, 0.95, 0.95, 0.95)))); + } + + @Test + void normallyOccludedPartsUseTheSameRule() { + NormallyOccludedPart origin = new NormallyOccludedPart(box(0, 0, 0, 0.5, 1, 1)); + NormallyOccludedPart touching = new NormallyOccludedPart(box(0.5, 0, 0, 1, 1, 1)); + NormallyOccludedPart overlapping = new NormallyOccludedPart(box(0.25, 0, 0, 0.75, 1, 1)); + + assertTrue(origin.occlusionTest(touching)); + assertTrue(touching.occlusionTest(origin)); + assertFalse(origin.occlusionTest(overlapping)); + assertFalse(overlapping.occlusionTest(origin)); + } + + @Test + void partHelperIncludesPartialOcclusionBoxes() { + NormallyOccludedPart origin = new NormallyOccludedPart(box(0, 0, 0, 0.5, 1, 1)); + PartialPart touching = new PartialPart(box(0.5, 0, 0, 1, 1, 1)); + PartialPart overlapping = new PartialPart(box(0.25, 0, 0, 0.75, 1, 1)); + + assertTrue(NormalOcclusionTest.apply(origin, touching)); + assertFalse(NormalOcclusionTest.apply(origin, overlapping)); + } + + @Test + void partWithoutOcclusionInterfacesIsCompatible() { + NormallyOccludedPart origin = new NormallyOccludedPart(box(0, 0, 0, 1, 1, 1)); + TMultiPart barePart = new TMultiPart() { + + @Override + public String getType() { + return "test:bare"; + } + }; + + assertTrue(NormalOcclusionTest.apply(origin, barePart)); + } + + private static boolean compatible(Cuboid6 first, Cuboid6 second) { + return NormalOcclusionTest.apply(scalaBoxes(first), scalaBoxes(second)); + } + + private static Traversable scalaBoxes(Cuboid6... boxes) { + ArrayBuffer result = new ArrayBuffer<>(); + for (Cuboid6 box : boxes) { + result.$plus$eq(box); + } + return result; + } + + private static Cuboid6 box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return new Cuboid6(minX, minY, minZ, maxX, maxY, maxZ); + } + + private static final class PartialPart extends TMultiPart implements JPartialOcclusion { + + private final Iterable boxes; + + private PartialPart(Cuboid6... boxes) { + this.boxes = Collections.unmodifiableList(Arrays.asList(boxes)); + } + + @Override + public String getType() { + return "test:partial"; + } + + @Override + public Iterable getPartialOcclusionBoxes() { + return boxes; + } + + @Override + public boolean allowCompleteOcclusion() { + return false; + } + } +}