From d70f69e79fc939f3863969e8a416fe2dc2e6fa83 Mon Sep 17 00:00:00 2001 From: Algent Date: Sat, 5 Sep 2026 00:48:38 +0200 Subject: [PATCH] feat: add Java collection occlusion query --- JAVA_MIGRATION.md | 7 +- JAVA_MIGRATION_CONSUMER_AUDIT.md | 14 ++- JAVA_MIGRATION_HANDOFF.md | 38 ++++--- README.md | 2 +- docs/API.md | 5 +- docs/api/OCCLUSION.md | 78 +++++++++++++ docs/migration/HISTORY.md | 22 ++++ .../test/ForgeEnvironmentSmokeTest.java | 19 ++++ .../codechicken/multipart/TileMultipart.java | 35 +++++- .../multipart/TileMultipartOcclusionTest.java | 104 ++++++++++++++++++ .../multipart/examples/OcclusionExample.java | 18 +++ 11 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 docs/api/OCCLUSION.md create mode 100644 src/test/java/codechicken/multipart/examples/OcclusionExample.java diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index adfcead..a6dc741 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -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)` | `forEachPart(java.util.function.Consumer)` | 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)` | | +| `TileMultipart.occlusionTest(scala.collection.Seq, TMultiPart)` | `testOcclusion(Collection, 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, scala.collection.Traversable)` 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)` | 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)` | 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. diff --git a/JAVA_MIGRATION_CONSUMER_AUDIT.md b/JAVA_MIGRATION_CONSUMER_AUDIT.md index 9324d8a..6d32637 100644 --- a/JAVA_MIGRATION_CONSUMER_AUDIT.md +++ b/JAVA_MIGRATION_CONSUMER_AUDIT.md @@ -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, 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. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index ff590ef..a39e6d2 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -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, 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 diff --git a/README.md b/README.md index 12aaa8b..054f4e3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/API.md b/docs/API.md index eaaa485..9da3f6f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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 diff --git a/docs/api/OCCLUSION.md b/docs/api/OCCLUSION.md new file mode 100644 index 0000000..1d8e17e --- /dev/null +++ b/docs/api/OCCLUSION.md @@ -0,0 +1,78 @@ +# Testing part occlusion from Java + +[API index](../API.md) + +The `algent/java` branch adds `TileMultipart.testOcclusion(Collection, 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 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`, +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). diff --git a/docs/migration/HISTORY.md b/docs/migration/HISTORY.md index 19d51ff..3348e3d 100644 --- a/docs/migration/HISTORY.md +++ b/docs/migration/HISTORY.md @@ -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, 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/`. diff --git a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java index 1da29eb..bdfade0 100644 --- a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java @@ -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(); diff --git a/src/main/scala/codechicken/multipart/TileMultipart.java b/src/main/scala/codechicken/multipart/TileMultipart.java index b65500b..57d71b0 100644 --- a/src/main/scala/codechicken/multipart/TileMultipart.java +++ b/src/main/scala/codechicken/multipart/TileMultipart.java @@ -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. + * + *

+ * 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. + * + *

+ * 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 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 parts, TMultiPart npart) { for (TMultiPart part : JavaConversions.seqAsJavaList(parts)) { if (!part.occlusionTest(npart) || !npart.occlusionTest(part)) { diff --git a/src/test/java/codechicken/multipart/TileMultipartOcclusionTest.java b/src/test/java/codechicken/multipart/TileMultipartOcclusionTest.java index 61453cb..2775486 100644 --- a/src/test/java/codechicken/multipart/TileMultipartOcclusionTest.java +++ b/src/test/java/codechicken/multipart/TileMultipartOcclusionTest.java @@ -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 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 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 calls = new ArrayList<>(); + List> inputs = new ArrayList<>(); + TileMultipart tile = new TileMultipart() { + + @Override + public boolean occlusionTest(Seq parts, TMultiPart candidate) { + calls.add("legacy"); + inputs.add(parts); + return false; + } + + @Override + public boolean testOcclusion(Collection 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 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 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 seq(TMultiPart... parts) { return JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList(); } diff --git a/src/test/java/codechicken/multipart/examples/OcclusionExample.java b/src/test/java/codechicken/multipart/examples/OcclusionExample.java new file mode 100644 index 0000000..4b77335 --- /dev/null +++ b/src/test/java/codechicken/multipart/examples/OcclusionExample.java @@ -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 parts, Cuboid6 bounds) { + return tile.testOcclusion(parts, new NormallyOccludedPart(bounds)); + } +}