feat: add Java collection occlusion query

This commit is contained in:
Algent committed 2026-09-05 00:48:38 +02:00
1 parent 6744f5b04c
commit d70f69e79f
11 files changed
+317 -25

No files matched your search

+5 -2
View File
@@ -531,7 +531,8 @@ Each row keeps its existing descriptor for binary compatibility and gains a Java
| `MultiPartRegistry.registerParts(IPartFactory2, scala.collection.Seq)` | `registerParts(IPartFactory2, String...)` | Sibling already exists |
| `MicroMaterialRegistry.getIdMap(): scala.Tuple2[]` | `materialCount(): int` plus existing `materialName(int)` and `getMaterial(int)` | Implemented with static/companion deprecation; [guide and compiling example](docs/api/MATERIAL_ENUMERATION.md). Consumer release/adoption remains pending |
| `TileMultipart.operate(scala.Function1<TMultiPart, BoxedUnit>)` | `forEachPart(java.util.function.Consumer<TMultiPart>)` | Implemented through the legacy virtual hook; preserves captured traversal, detached-part filtering and callback failures. Lifecycle still calls `operate` |
| `TileMultipart.occlusionTest(scala.collection.Seq, TMultiPart)` | `occlusionTest(Collection<TMultiPart>, TMultiPart)` | |
| `TileMultipart.occlusionTest(scala.collection.Seq, TMultiPart)` | `testOcclusion(Collection<? extends TMultiPart>, TMultiPart)` | Implemented with an input snapshot and legacy virtual dispatch, including generated partial occlusion; [guide](docs/api/OCCLUSION.md). Distinct Java name avoids Scala overload resolution |
| `NormalOcclusionTest.apply(scala.collection.Traversable<Cuboid6>, scala.collection.Traversable<Cuboid6>)` and companion entry | Java box-versus-box entry, with a distinct name to be chosen after characterization | Still used by ForgeRelocationFMP and OpenComputers. The existing Java `test(Iterable, Iterable)` loop is private; its public part-based sibling does not replace this box-list contract |
| `TileMultipart.loadParts(scala.collection.Iterable)` | `loadPartList(Collection<TMultiPart>)` | Implemented through the legacy virtual hook; binding, world notifications, input iteration and partial failure retained. [Loading guide](docs/api/PART_LOADING.md) |
| `TileMultipart.partList_$eq(scala.collection.Seq)` | `setPartList(List<TMultiPart>)` | Implemented through the legacy setter; Java list copied without binding, null sentinel supported. GuideNH's old reflective name remains supported |
| `TileMultipart.renderID()` / `renderID_$eq(int)` | `getRenderID()` / `setRenderID(int)` | |
@@ -549,11 +550,13 @@ same-name overload there invites a silent wrong-overload bind.
their override dispatch. JVM and generated-tile Forge cases cover the [documented contract](docs/api/PART_TRAVERSAL.md).
- [x] Add Java loading and storage assignment, with legacy override/reflection coverage, explicit ownership and
lifecycle contracts, and generated server/client tile checks; [guide](docs/api/PART_LOADING.md), [API index](docs/API.md).
- [x] Add `testOcclusion(Collection, candidate)` with snapshot ownership, subtype inputs, legacy hook dispatch and
generated partial-occlusion coverage; [guide and compiling example](docs/api/OCCLUSION.md).
- [ ] For remaining siblings, compile external Java examples with Scala excluded from the compile classpath before
finalizing names. The proposed `loadParts(Collection)` overload required `scala.collection.Iterable` during javac
overload resolution; the implemented `loadPartList(Collection)` avoids that dependency. Apply this gate to the
remaining occlusion and registration overloads as well as checking the emitted consumer bytecode.
- [ ] Mark all nine rows `@Deprecated` with javadoc naming the replacement.
- [ ] Mark the remaining legacy entries above `@Deprecated` with javadoc naming a working replacement.
- [ ] Confirm every original descriptor still exists in the ABI fixture after the change.
- [ ] Document the supported API with compiling usage examples and an old-to-new migration guide. Validate Java
subclasses and generated extensions on the actual Forge path, including both sides where relevant.
+13 -1
View File
@@ -491,7 +491,7 @@ behavior:
## Java API adoption ledger
This ledger tracks migration of a specific legacy contract, not completion of an entire consumer's migration.
Material enumeration, tile collection/traversal access and tile loading/storage assignment are implemented on
Material enumeration, tile collection/traversal access, tile loading/storage assignment and collection occlusion queries are implemented on
`algent/java`. The [API index](docs/API.md) links the guides and compiling Java examples. These additions are not yet
tied to a released minimum dependency version. Unlisted contracts remain governed by the inventories above.
@@ -539,6 +539,18 @@ failure behavior. All 546 pre-change compiled JVM tests pass with their recorded
server slot rebuilding/notifications and client storage/loading/render-cache queries. Actual client preview rendering,
consumer release and pack adoption are still separate gates.
Tile collection occlusion is implemented as `testOcclusion(Collection<? extends TMultiPart>, TMultiPart)`, documented
in the [occlusion guide](docs/api/OCCLUSION.md). The supplied Java/Scala source search found no direct consumer calls or
overrides of the old two-argument tile `occlusionTest` and no `testOcclusion` name collisions. The frozen ABI floor
likewise contains no direct reference to that tile descriptor. The legacy hook is nevertheless required by generated
`TPartialOcclusionTile` behavior and by existing tile placement/replacement dispatch; it remains supported.
ProjectRed and ForgeRelocationFMP's `canReplacePart` callers need no rename and must retain outgoing-part exclusion.
The new collection query is not a blanket replacement for placement checks or the part-level `occlusionTest` hook.
`NormalOcclusionTest$.apply(Traversable, Traversable)` remains a separate box-list bridge used by ForgeRelocationFMP
and OpenComputers. Its Java replacement is now explicit in the plan; completion of the tile query does not retire it.
Evidence for this slice is under ignored `run/migration-part-occlusion-reference/`; reference checkouts remain unchanged.
## Practical priority for the current branch
1. Keep the existing `+678` ABI fixture as the exact binary floor and correct failures before source cleanup.
+20 -18
View File
@@ -17,7 +17,7 @@ migration checkout; `codex/tile-compatibility-fixes` was deleted after its fixes
## Current state and next target
**549 plain-JVM tests and 242 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java
**556 plain-JVM tests and 243 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java
files and 9 Scala files / 747 nonblank Scala lines**. The packaged inventory has 443 classes.
Review follow-up: restored packet-scheduler callback mutation behavior with the original Scala hash-map traversal,
@@ -32,34 +32,36 @@ use, but no override triggering these three regressions was found. All 443 class
payloads and 116 generated dumps are retained. The agreed next milestone is the documented consumer-facing Java API,
followed by consumer release/adoption and final Scala removal; see the plan's API migration design and Phases 8–10.
Latest API addition: `TileMultipart.setPartList(List)` copies Java list storage through the legacy setter, and
`loadPartList(Collection)` delegates through the legacy Scala loader. Storage assignment and binding/cache rebuilding
have separate documented contracts. Both old entries are deprecated while retaining their descriptors and bodies.
Four JVM and one Forge baseline cases were committed first as `92273bd`; three further JVM cases, a Java example
and two Forge cases cover the new methods on generated server/client tiles. All 546 pre-change compiled JVM tests
pass with their recorded version. The 443 class APIs retain all existing members, with two methods added; 17
ScalaSignature payloads, 3,748 existing method bodies and 116 generated dumps are unchanged apart from expected
deprecation metadata and build-version literals. Evidence: `run/migration-part-loading-reference/`.
Latest API addition: `TileMultipart.testOcclusion(Collection<? extends TMultiPart>, TMultiPart)` copies the input
before dispatching through the legacy `occlusionTest(Seq, TMultiPart)` hook. Generated partial-occlusion checks remain
in that path; the query is not full placement approval. Three JVM baseline cases and stronger existing Forge
assertions were committed first as `6744f5b`. Four further JVM cases, a compiling shape example and one Forge
generated-partial case cover the new entry. All 552 pre-change compiled JVM tests pass with their recorded version.
All 443 class APIs retain their existing members, with one method added; 17 ScalaSignature payloads, 3,750 existing
method bodies and 116 generated dumps are unchanged apart from expected deprecation metadata and build-version
literals. Evidence: `run/migration-part-occlusion-reference/`; [guide](docs/api/OCCLUSION.md).
Naming correction: the planned `loadParts(Collection)` overload made javac require `scala.collection.Iterable` even
for Java arguments. The distinct `loadPartList` name allows compilation without Scala on the example's classpath.
Keep this compilation gate for remaining occlusion/registration APIs; checking only imports or bytecode is insufficient.
Internal storage writes and reconstruction still dispatch through the old setter/loader hooks, not the Java siblings.
The [API index](docs/API.md) now orients consumers to entry points, compiling examples and migration status. Material
enumeration and tile read/traversal access are also complete; use their guides rather than treating them as missing
APIs. GuideNH and Schematica have Java loading replacements, but their source changes, other reflection/extension
contracts, releases and pack adoption remain pending. The supplied checkouts remain reference-only. The installed
`+719` pack scan retains the `+678` floor: 27 consumers, 35 inherited types, 255 members, 76 other types and 20 strings.
The [API index](docs/API.md) orients consumers to entry points, compiling examples and migration status. Material
enumeration and tile read/traversal/loading/storage APIs are also complete. No supplied consumer directly calls the
two-argument tile occlusion hook; existing `canAddPart`/`canReplacePart` calls need no rename. GuideNH and Schematica
have Java loading replacements, but consumer source changes, other reflection/extension contracts, releases and pack
adoption remain pending. Checkouts remain reference-only. The installed `+719` pack scan retains the `+678` floor:
27 consumers, 35 inherited types, 255 members, 76 other types and 20 strings.
**Next priority: close the Java API gaps in Phases 2 and 9 and map consumer migrations in Phase 10.** Reuse the existing
Java surface; supply missing capabilities, precise contracts, migration guidance and compiling examples. Cover
subclass/override behavior and generated extensions as well as ordinary calls. ProjectRed's illuminated microblocks
are the representative external extension case. Consumer mods may remain Scala internally while adopting this API.
The next bounded API candidate is Java collection-based occlusion testing. Verify legacy override dispatch, both
directions of occlusion checks, ordering/failures, and compilation without Scala before choosing the Java entry name.
The tentative same-name overload may have the same compiler constraint as loading. Registry registration guidance
and bridges remain another pending API slice. FMP-side completion does not establish consumer adoption.
The next bounded API candidate is the direct box-versus-box `NormalOcclusionTest` contract. Its public box-list entry
still takes Scala `Traversable` on both the static class and companion, while the Java `test(Iterable, Iterable)` loop
is private. ForgeRelocationFMP and OpenComputers use the Scala descriptor. Characterize eager input collection,
ordering/failures and geometric boundaries before adding a distinctly named Java entry and deprecating the bridges.
Verify compilation without Scala. Registry registration guidance and bridges remain another pending slice.
Pause mechanical extraction of retained Scala shells unless it enables that API, fixes a demonstrated issue or has
a measured benefit. `ScalaSignature.ClassSymbolRef.info` remains an optional bounded extraction, not the default next
+1 -1
View File
@@ -16,4 +16,4 @@ If you only want to use the API and not modify it:
Implementation of all parts of the API should be clean and easy in Java. The minecraft multipart mod is implemented in java as a test case.
See the [Java API index](docs/API.md) for supported entry points, compiling examples, ownership/lifecycle contracts
and migration status. It links to the material enumeration, part traversal and tile loading guides.
and migration status. It links to material enumeration, part traversal, tile loading and occlusion guides.
+3 -2
View File
@@ -17,6 +17,7 @@ without converting the rest of its code to Java.
| Run callbacks while skipping detached parts | `forEachPart(Consumer)` — [callback and override behavior](api/PART_TRAVERSAL.md#callback-behavior) |
| Rebuild parts on an already prepared composite tile | `loadPartList(Collection)` — [part loading](api/PART_LOADING.md) |
| Assign stored parts during reconstruction, without binding or notifications | `setPartList(List)` — [storage assignment](api/PART_LOADING.md#storage-assignment) |
| Test a candidate against a selected collection of parts | `testOcclusion(Collection, candidate)` — [occlusion queries and generated hooks](api/OCCLUSION.md) |
Each guide explains ownership, lifecycle, legacy replacements and limitations, and links to a compiling Java example
exercised by the test suite. The loading/setter APIs are advanced reconstruction operations; ordinary placement and
@@ -46,8 +47,8 @@ Deprecated Scala-facing entry points remain callable, with their descriptors and
Follow the method-specific guide: a Java sibling is not automatically a replacement override hook, and reflection
must select the intended parameter types when a method is overloaded.
Java collection occlusion access, remaining registry bridges/documentation, generator/reflection replacements and
complete external microblock extension guidance are still pending. In particular, ProjectRed's Scala microblock traits
Direct box-versus-box Java occlusion access, remaining registry bridges/documentation, generator/reflection replacements
and complete external microblock extension guidance are still pending. In particular, ProjectRed's Scala microblock traits
remain a supported dependency; registration signatures alone do not prove a complete Java replacement.
FMP-side implementation is separate from consumer releases and target-pack adoption. Retiring legacy bridges or the
+78
View File
@@ -0,0 +1,78 @@
# Testing part occlusion from Java
[API index](../API.md)
The `algent/java` branch adds `TileMultipart.testOcclusion(Collection<? extends TMultiPart>, TMultiPart)`.
It tests a supplied collection of parts through the tile's existing occlusion hook, including generated
partial-occlusion checks. This addition is not yet tied to a minimum published dependency version.
## Example
Use a `NormallyOccludedPart` to test a normal bounding shape against a supplied group of parts:
```java
public static boolean fitsShape(TileMultipart tile, Collection<? extends TMultiPart> parts, Cuboid6 bounds) {
return tile.testOcclusion(parts, new NormallyOccludedPart(bounds));
}
```
The [complete Java example](../../src/test/java/codechicken/multipart/examples/OcclusionExample.java) compiles and runs
in the test suite. It also compiles with Scala excluded from its classpath and has no Scala bytecode references.
FMP still retains its Scala runtime internally. Collections of part subclasses, such as `List<NormallyOccludedPart>`,
are accepted without a cast.
Pass `tile.jPartList()` to test against the tile's currently stored parts, or pass a separate collection for a preview
or selected subset. To exercise a real candidate's custom occlusion behavior, pass that part instead of the shape
wrapper. The wrapper represents normal bounding geometry, not all the capabilities of a custom part.
## What the query checks
The base tile tests each supplied part against the candidate, then tests the candidate against that same part. It
proceeds in input iteration order and stops immediately when either direction rejects. Unbound/detached parts and
duplicate entries participate; the candidate is not automatically excluded if it is also in the collection.
Generated partial-occlusion tiles retain their extra aggregate check. When the candidate implements `JPartialOcclusion`,
the existing trait checks the group plus the candidate before the ordinary pair callbacks. A plain `TileMultipart`
does not gain that capability just because the supplied parts need it. Use the appropriate composite tile; this method
does not generate or select tile traits.
This is an occlusion query, not full placement approval. It does not check placement permissions, duplicate membership,
slot occupancy or world replaceability, and does not bind/store parts or send notifications. For ordinary placement
and replacement, continue using `canPlacePart`, `canAddPart` and `canReplacePart` as appropriate. In particular,
`canReplacePart(outgoing, candidate)` performs its existing outgoing-part exclusion; this collection query does not.
## Ownership, callbacks and failures
The Java entry copies the input into an immutable sequence before calling the legacy hook. Order, duplicates, null
entries and part identities are preserved. Callback edits to the caller's collection do not change the current
query's sequence. Part objects are shared, so mutations of their state can affect later callbacks. A nested query
takes its own copy.
Copying failures, including a null collection, propagate before the legacy hook is called. There is no eager
candidate/null-entry validation: with the base implementation, an empty collection accepts even a null candidate,
and an early rejection can avoid a later null dereference. Callback exceptions propagate unchanged and stop the query;
callback side effects already performed are not undone. Overrides may add checks or customize these boundaries.
Use the game thread when querying live parts. A geometric query may invoke arbitrary part callbacks, including ones
that read or modify world/part state; it is not a thread-safety guarantee.
## Overrides and migration
Dispatch is `testOcclusion(Collection, candidate)` → virtual `occlusionTest(scala.collection.Seq, candidate)`.
The old method keeps its descriptor and body, and is deprecated for callers. Existing subclasses and generated traits
continue to override it. `canAddPart` and `canReplacePart` still call that hook directly; overriding only the new
Java method does not intercept their checks. The distinct Java name avoids introducing another Scala/Java overload
pair, following the [loader compilation finding](PART_LOADING.md#overrides-and-reflection).
No direct consumer calls or overrides of the two-argument tile hook were found in the supplied source/ABI audit.
Consumers such as ProjectRed and ForgeRelocationFMP already use the Java-typed `canReplacePart` API and need no rename
for that contract. Part-level `occlusionTest(TMultiPart)` remains supported as-is. The Scala `NormalOcclusionTest.apply`
box-versus-box entry is a separate, still-used contract whose Java replacement remains pending.
## Validation
JVM tests cover both directions, early rejection, original exceptions, null boundaries, snapshot ownership, duplicates,
subtype/non-list collections, override routing and the shape example. A Forge test demonstrates that the generated
partial trait rejects overlapping partial parts that plain pair tests accept. Existing descriptors, generated output,
and frozen pre-change callers are checked separately. Actual client previews and full-pack adoption remain covered
by the [manual release checks](../../JAVA_MIGRATION_MANUAL_CHECKS.md).
+22
View File
@@ -1784,3 +1784,25 @@ differences belong in the [divergence ledger](../../JAVA_MIGRATION_DIVERGENCES.m
- Added `docs/API.md` as the consumer entry point, a loading/storage guide, compiling examples, and reciprocal guide
links. Updated the plan, ABI notes and GuideNH/Schematica adoption ledger; reference checkouts remain unchanged and
consumer releases/pack adoption are pending. Evidence: `run/migration-part-loading-reference/`.
### 2026-09-05 — Java collection occlusion query
- Committed three JVM baseline cases and stronger generated-trait assertions first as `6744f5b`. They pin pair order,
both rejection directions, lazy null behavior, original exceptions and replacement-hook dispatch. Saved the pre-API
jar, source, reports, 116 generated dumps and all 552 compiled JVM tests before implementation.
- Added `TileMultipart.testOcclusion(Collection<? extends TMultiPart>, TMultiPart)` as an adapter through the old
virtual `occlusionTest(Seq, TMultiPart)`. It takes a shallow immutable snapshot, retains order/duplicates/identities,
and accepts subtype collections. The distinct name avoids a Scala/Java overload pair. The legacy hook is deprecated
for callers, with its descriptor and body unchanged; placement/replacement still dispatch directly through it.
- Four new JVM cases and a compiling Java shape example cover snapshot/callback behavior, non-list/subtype inputs,
failures, override routing and touching/overlapping bounds. One new Forge case proves aggregate partial-occlusion
rejection through the generated trait when plain pair tests would accept the same parts.
- Normal/clean build/style checks pass: 556 JVM tests, 243 Java 8 Forge tests and all 552 frozen pre-change JVM tests.
The Java example compiles with Scala excluded from the classpath and contains no Scala bytecode references.
All 443 existing class APIs, 17 ScalaSignature payloads, 3,750 existing method bodies and 116 generated dumps are
retained, with exactly one added public method and one deprecation. Only expected deprecation metadata and build
versions differ; no production class or dependency is added.
- Added the occlusion guide and API index entry. No direct consumer use of the two-argument tile hook or collision
with the new name was found. The separately load-bearing Scala box-list `NormalOcclusionTest.apply` contract now
has an explicit plan row and is the next candidate. Reference checkouts remain unchanged; client previews and
consumer release/adoption remain separate gates. Evidence: `run/migration-part-occlusion-reference/`.
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -587,6 +588,24 @@ class ForgeEnvironmentSmokeTest {
assertFalse(tile.occlusionTest(tile.partList(), new PartialPart(voxel(0))));
}
@Test
void javaOcclusionQueryDispatchesThroughTheGeneratedPartialTrait() {
PartialPart existing = new PartialPart(voxel(0));
PartialPart overlapping = new PartialPart(voxel(0));
PartialPart disjoint = new PartialPart(voxel(1));
TileMultipart tile = MultipartHelper.createTileFromParts(Collections.singletonList(existing));
assertTrue(
new TileMultipart().testOcclusion(Collections.singletonList(existing), overlapping),
"Pair tests alone allow these parts; the generated aggregate check must reject them");
assertFalse(tile.testOcclusion(Collections.singletonList(existing), overlapping));
assertTrue(tile.testOcclusion(Collections.singletonList(existing), disjoint));
assertEquals(Collections.singletonList(existing), tile.jPartList());
assertSame(tile, existing.tile());
assertNull(overlapping.tile());
assertNull(disjoint.tile());
}
@Test
void generatedServerPassThroughDelegatesCopiesAndAllowsOnlyOneImplementor() throws Exception {
PassThroughPart part = new PassThroughPart();
@@ -415,7 +415,40 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile {
return occlusionTest(others, npart);
}
/** Returns true if parts do not occlude npart. */
/**
* Tests a candidate against a shallow snapshot of the supplied parts through the legacy
* {@link #occlusionTest(Seq, TMultiPart)} hook, including generated partial-occlusion overrides. Preserves
* collection iteration order, duplicates and part identities; changes to the caller's collection during callbacks
* do not change this query's sequence.
*
* <p>
* The default hook calls each existing part's occlusion test first, then the candidate's reverse test, stopping
* immediately on rejection or an exception. It includes detached parts and does not exclude the candidate itself.
* Part objects remain shared, so callback changes to their state are visible. Existing overrides may add checks.
*
* <p>
* This tests the supplied geometry, not full placement validity: it does not select tile capabilities, check
* placement permissions or slot occupancy, bind parts or change stored parts. Use the appropriate composite tile
* for the candidate's capabilities; use {@link #canAddPart(TMultiPart)} /
* {@link #canReplacePart(TMultiPart, TMultiPart)} for normal tile checks. Those methods still dispatch directly
* through the legacy hook.
*
* @param parts parts to copy before dispatch; null fails before calling the legacy hook
* @param candidate part to test; no eager null check, so the default hook accepts null against an empty collection
* @return true when all applicable occlusion checks allow the candidate
*/
public boolean testOcclusion(Collection<? extends TMultiPart> parts, TMultiPart candidate) {
return occlusionTest(toSeq(new ArrayList<>(parts)), candidate);
}
/**
* Returns true if parts do not occlude npart.
*
* @deprecated Call {@link #testOcclusion(Collection, TMultiPart)} from Java. Retained as the virtual hook for
* existing subclasses, generated traits and tile placement/replacement checks; its sequence behavior is
* unchanged.
*/
@Deprecated
public boolean occlusionTest(Seq<TMultiPart> parts, TMultiPart npart) {
for (TMultiPart part : JavaConversions.seqAsJavaList(parts)) {
if (!part.occlusionTest(npart) || !npart.occlusionTest(part)) {
@@ -9,10 +9,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import org.junit.jupiter.api.Test;
import codechicken.lib.vec.Cuboid6;
import codechicken.multipart.examples.OcclusionExample;
import scala.collection.JavaConversions;
import scala.collection.Seq;
@@ -85,6 +90,105 @@ class TileMultipartOcclusionTest {
assertEquals(Arrays.asList(outgoing, retained), tile.jPartList());
}
@Test
void javaQuerySnapshotsTheSuppliedCollectionAndRetainsDuplicatesAndUnboundParts() {
TileMultipart tile = new TileMultipart();
List<String> calls = new ArrayList<>();
Part stored = new Part("stored", calls);
stored.accepts = false;
tile.setPartList(Arrays.asList(stored));
Part first = new Part("first", calls);
Part second = new Part("second", calls);
Part candidate = new Part("candidate", calls);
List<Part> input = new ArrayList<>(Arrays.asList(first, second, first));
first.onTest = input::clear;
assertTrue(tile.testOcclusion(input, candidate));
assertEquals(
Arrays.asList(
"first:candidate",
"candidate:first",
"second:candidate",
"candidate:second",
"first:candidate",
"candidate:first"),
calls);
assertTrue(input.isEmpty());
assertEquals(
Arrays.asList(stored),
tile.jPartList(),
"The query uses the supplied collection, not tile storage");
assertNull(first.tile());
assertNull(candidate.tile());
}
@Test
void javaQueryPreservesTheLegacyOverrideAndDoesNotReplaceTheInternalHook() {
List<String> calls = new ArrayList<>();
List<Seq<TMultiPart>> inputs = new ArrayList<>();
TileMultipart tile = new TileMultipart() {
@Override
public boolean occlusionTest(Seq<TMultiPart> parts, TMultiPart candidate) {
calls.add("legacy");
inputs.add(parts);
return false;
}
@Override
public boolean testOcclusion(Collection<? extends TMultiPart> parts, TMultiPart candidate) {
calls.add("java");
return super.testOcclusion(parts, candidate);
}
};
Part first = new Part("first", calls);
Part second = new Part("second", calls);
Part candidate = new Part("candidate", calls);
assertFalse(tile.testOcclusion(new LinkedHashSet<>(Arrays.asList(first, second)), candidate));
tile.setPartList(Arrays.asList(first, second));
assertFalse(tile.canReplacePart(first, candidate));
assertEquals(Arrays.asList("java", "legacy", "legacy"), calls);
assertEquals(Arrays.asList(seq(first, second), seq(second)), inputs);
}
@Test
void javaQueryKeepsShortCircuitingAndCallbackFailureBoundaries() {
TileMultipart tile = new TileMultipart();
List<String> calls = new ArrayList<>();
Part first = new Part("first", calls);
Part candidate = new Part("candidate", calls);
assertTrue(tile.testOcclusion(Collections.emptyList(), null));
assertThrows(NullPointerException.class, () -> tile.testOcclusion(null, candidate));
first.accepts = false;
assertFalse(tile.testOcclusion(Arrays.asList(first, null), null));
assertEquals(Arrays.asList("first:null"), calls);
first.accepts = true;
candidate.accepts = false;
calls.clear();
assertFalse(tile.testOcclusion(Arrays.asList(first, null), candidate));
assertEquals(Arrays.asList("first:candidate", "candidate:first"), calls);
calls.clear();
IllegalStateException failure = new IllegalStateException("candidate callback");
candidate.failure = failure;
assertSame(
failure,
assertThrows(
IllegalStateException.class,
() -> tile.testOcclusion(Arrays.asList(first, null), candidate)));
assertEquals(Arrays.asList("first:candidate", "candidate:first"), calls);
}
@Test
void javaShapeExampleDistinguishesTouchingFromOverlappingBounds() {
TileMultipart tile = new TileMultipart();
NormallyOccludedPart existing = new NormallyOccludedPart(new Cuboid6(0, 0, 0, 0.5, 1, 1));
List<NormallyOccludedPart> input = Arrays.asList(existing);
assertTrue(OcclusionExample.fitsShape(tile, input, new Cuboid6(0.5, 0, 0, 1, 1, 1)));
assertFalse(OcclusionExample.fitsShape(tile, input, new Cuboid6(0.25, 0, 0, 1, 1, 1)));
assertTrue(tile.jPartList().isEmpty());
assertNull(existing.tile());
}
private static Seq<TMultiPart> seq(TMultiPart... parts) {
return JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList();
}
@@ -0,0 +1,18 @@
package codechicken.multipart.examples;
import java.util.Collection;
import codechicken.lib.vec.Cuboid6;
import codechicken.multipart.NormallyOccludedPart;
import codechicken.multipart.TMultiPart;
import codechicken.multipart.TileMultipart;
/** Compiling example for docs/api/OCCLUSION.md; this checks geometry, not full placement validity. */
public final class OcclusionExample {
private OcclusionExample() {}
public static boolean fitsShape(TileMultipart tile, Collection<? extends TMultiPart> parts, Cuboid6 bounds) {
return tile.testOcclusion(parts, new NormallyOccludedPart(bounds));
}
}