feat: add Java material enumeration

This commit is contained in:
Algent committed 2026-09-04 23:30:54 +02:00
1 parent 9b78099d21
commit 2bb5e7ffc9
11 files changed
+267 -10

No files matched your search

+2 -1
View File
@@ -529,7 +529,7 @@ their sibling and only need the annotation and javadoc.
| --- | --- | --- |
| `TileMultipart.partList(): scala.collection.Seq` | `jPartList(): java.util.List` | Sibling already exists |
| `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)` | The array index **is** the material ID, so no pair or map type is needed |
| `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>)` | Preserve the existing skip-unbound-part behavior |
| `TileMultipart.occlusionTest(scala.collection.Seq, TMultiPart)` | `occlusionTest(Collection<TMultiPart>, TMultiPart)` | |
| `TileMultipart.loadParts(scala.collection.Iterable)` | `loadParts(Collection<TMultiPart>)` | Schematica and GuideNH reflect the Scala descriptor; see Phase 10 |
@@ -544,6 +544,7 @@ same-name overload there invites a silent wrong-overload bind.
- [ ] Add the missing Java-shaped siblings over shared behavior, preserving legacy override dispatch and avoiding
recursive forwarding as required by the API migration design.
- [x] Add and document `materialCount()` with existing indexed lookups; retain and deprecate both `getIdMap()` entries.
- [ ] Mark all nine rows `@Deprecated` with javadoc naming the 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
+20
View File
@@ -488,6 +488,26 @@ behavior:
API, trait, generator, registry, or tile change. Add UtilitiesInExcess as a compile/runtime fixture now even though
it is not yet in the pack.
## Java API adoption ledger
This ledger tracks migration of a specific legacy contract, not completion of an entire consumer's migration.
The FMP material-enumeration addition is implemented on `algent/java`, with the
[documented Java replacement](docs/api/MATERIAL_ENUMERATION.md) and a compiling example. It is not yet a released
minimum dependency version. Unlisted contracts remain governed by the inventories above.
| Legacy contract | Consumer and inspected source | Supported replacement | Consumer migration/release | Target-pack adoption and removal gate |
| --- | --- | --- | --- | --- |
| `MicroMaterialRegistry.getIdMap(): scala.Tuple2[]` | UtilitiesInExcess `3e107a1fe9bc15fcb6a808242ffda1354dac7c3a`: `FMPRecipeLoader.run`, `UEMultipartItem.getSubItems` | `materialCount()` with `materialName(int)` and, when needed, `getMaterial(int)` | Source patch and first released version pending; checkout used as reference only | No migrated pack version verified; retain the bridge |
| `MicroMaterialRegistry.getIdMap(): scala.Tuple2[]` | Extra Utilities 1.2.12 decompiled reference: both NEI microblock handlers and `multipart.microblock.ItemMicroBlock` | Same ID-based Java enumeration | No editable upstream consumer in scope; retirement/replacement remains pending | Verify Extra Utilities is absent and its replacement uses the new API before retiring this dependency |
Evidence for the FMP addition is under ignored `run/migration-material-enumeration-reference/`. The original
reference-compiled Scala consumer still exercises the companion and tuple-array descriptor. The new compiling Java
example has no Scala imports or bytecode references. Runtime checks cover initialized ID lookup and handshake order;
they do not establish that an updated UtilitiesInExcess release has shipped or entered the pack.
FMP's own `ItemMicroPart` and `MicroRecipe$` still use the legacy array internally. Their migration, any additional
retained companion users, and the other Scala-facing contracts remain separate removal gates.
## Practical priority for the current branch
1. Keep the existing `+678` ABI fixture as the exact binary floor and correct failures before source cleanup.
+15 -7
View File
@@ -16,7 +16,7 @@ migration checkout; `codex/tile-compatibility-fixes` was deleted after its fixes
## Current state and next target
**530 plain-JVM tests and 237 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java
**535 plain-JVM tests and 238 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,
@@ -31,17 +31,25 @@ 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 bounded target: `ScalaSignature.MethodSymbol.info` delegates info-ID evaluation to
`ScalaSignatureParser.methodSymbolInfo`. Seven characterization tests were committed first as `9859aa8`. They pin
infoId/eval order, repeated uncached reads, exact and null results, the result cast, failures, unused symbol/type members
and the frozen Scala caller descriptor. All 519 frozen JVM consumers pass; checks preserve 442 retained class/member
APIs, all 17 ScalaSignature payloads, 3,744 unrelated method bodies and all 116 generated dumps. No class is added or
removed; the total stays 443 classes. Evidence: `run/migration-method-symbol-info-reference/`.
Latest API addition: `MicroMaterialRegistry.materialCount()` completes ID-based enumeration with the existing
`materialName(int)` and `getMaterial(int)` methods. Both `getIdMap()` entries are deprecated with unchanged descriptors
and live-array behavior. Two baseline cases were committed first as `9b78099`; three further JVM cases and one Forge
case cover the new API and a compiling Java example. All 532 pre-change compiled tests pass with their recorded
version. The 443 class APIs retain all existing members, with one public static method added; 17 ScalaSignature
payloads, 3,746 existing method bodies and 116 generated dumps are unchanged apart from expected deprecation metadata
and build-version literals. Evidence: `run/migration-material-enumeration-reference/`.
The [material enumeration guide](docs/api/MATERIAL_ENUMERATION.md) documents lifecycle, server ID ordering and failure
boundaries. The consumer audit's adoption ledger records the inspected UtilitiesInExcess revision and Extra Utilities
1.2.12; their source migrations, releases and pack adoption remain pending. The supplied checkouts remain reference-only.
**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 `TileMultipart` collection/traversal access, preserving legacy getter and callback
overrides, captured-sequence mutation behavior and detached-part filtering. Do not repeat material enumeration as an
unimplemented gap or treat its FMP-side completion as consumer adoption.
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
+4
View File
@@ -14,3 +14,7 @@ If you only want to use the API and not modify it:
* Place both mods in your /libs/ folder and link them as libraries in your IDE.
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.
Java API migration guides:
* [Enumerating microblock materials](docs/api/MATERIAL_ENUMERATION.md), including a compiling example and legacy API replacements.
+70
View File
@@ -0,0 +1,70 @@
# Enumerating microblock materials from Java
The `algent/java` branch adds `MicroMaterialRegistry.materialCount()`. Use it with the existing
`materialName(int)` and `getMaterial(int)` methods to enumerate materials without importing Scala types.
This API is not yet tied to a published release; consumers must require an FMP version containing the addition.
## Example
The following method copies the active material names in numeric ID order:
```java
public static List<String> materialNames() {
int count = MicroMaterialRegistry.materialCount();
List<String> names = new ArrayList<>(count);
for (int id = 0; id < count; id++) {
names.add(MicroMaterialRegistry.materialName(id));
}
return names;
}
```
Imports and the enclosing class are in the [compiling example](../../src/test/java/codechicken/microblock/examples/MaterialEnumerationExample.java).
It is compiled and exercised by the normal `test` task. It also compiles with only the FMP dev jar and Java 8 APIs on
the compile classpath; this does not remove FMP's runtime dependencies. Use `MicroMaterialRegistry.getMaterial(id)` in the loop when
you also need the `IMicroMaterial`, for example to obtain its item. That returns the registered instance, not a copy.
The example's list is an independent copy; editing it does not change the registry.
## Timing, order and identity
- Enumerate after ForgeMicroblock has built its ID map during post-initialization. A consumer's own post-init handler
needs suitable mod ordering if it enumerates there; later lifecycle/game callbacks can use the initialized map.
Consumers must not call `setupIDMap()` themselves to force readiness.
- On a multiplayer client, enumeration of the connected server's IDs requires a completed, successful FMP handshake.
FMP also rebuilds the local map before server startup. Reacquire IDs after map replacement.
- Use the game/lifecycle thread. The calls are live lookups, not an atomic snapshot: do not rebuild the map or perform
registration/handshake work from inside an enumeration loop.
- Local setup orders IDs by material name. A multiplayer client adopts server order; do not sort or filter IDs when
preserving a mapping. Store material names in saved data and resolve their numeric IDs for the active map.
- `materialCount()` counts ID slots, including the missing-material placeholder when present. It throws
`IllegalStateException` before initialization and returns zero for an initialized empty map.
- A failed handshake can leave unresolved slots. The count includes them; `materialName(id)` and `getMaterial(id)`
retain their existing `NullPointerException` for such slots. FMP disconnects the client for missing mappings.
The count is not a handshake-success indicator, and callers should not silently skip unresolved IDs.
- The existing indexed lookups throw `ArrayIndexOutOfBoundsException` outside the active ID range and
`NullPointerException` before map initialization.
## Migrating existing consumers
| Legacy expression | Java API |
| --- | --- |
| `getIdMap().length` | `materialCount()` |
| `getIdMap()[id]._1()` | `materialName(id)` |
| `getIdMap()[id]._2()` | `getMaterial(id)` |
| Loop over the tuple array | Loop over IDs from zero to `materialCount()`, exclusive |
For UtilitiesInExcess, apply this to `FMPRecipeLoader.run()` and `UEMultipartItem.getSubItems()`: replace each tuple
loop with an ID loop and use `materialName(id)` wherever the tuple name was used. Preserve its recipe/item ordering,
filters and material-name NBT. A material object, when needed, is available from `getMaterial(id)`.
Extra Utilities' NEI handlers use both tuple fields; both indexed replacements are available.
The static and companion `getIdMap()` methods are deprecated, with their original descriptors and live-array
behavior retained. This change does not authorize removal of either method. The
[consumer adoption ledger](../../JAVA_MIGRATION_CONSUMER_AUDIT.md#java-api-adoption-ledger) records the migrations and
released pack versions still needed.
## Validation
The JVM tests cover the legacy array contract, initialization, server ordering, empty maps, unresolved slots, the
compiled Java example and the frozen Scala consumer. The Forge test checks that the Java enumeration matches the
initialized registry and the material-ID handshake payload. The public addition does not change packet or NBT formats.
+17
View File
@@ -1724,3 +1724,20 @@ differences belong in the [divergence ledger](../../JAVA_MIGRATION_DIVERGENCES.m
- ProjectRed, Chisel and ExtraUtilities extend `BlockMicroMaterial`; no supplied consumer overrides the affected
accessors. These are verified extension-contract regressions, without a demonstrated current-pack gameplay
failure. Evidence is under ignored `run/conversion-review/`; migration-plan changes remain deferred.
### 2026-09-04 — Java material enumeration API
- Committed two baseline cases first as `9b78099`, pinning the shared legacy array, uninitialized access, server ID
ordering, missing slots and empty maps. Saved the pre-API jar and all 532 compiled tests before implementation.
- Added `MicroMaterialRegistry.materialCount()` beside the existing indexed name/material lookups. It reports the
active ID-map length and explicitly rejects an uninitialized map. The static and companion `getIdMap()` methods
keep their original descriptors and live-array behavior, with deprecation documentation pointing at the Java API.
- Added three JVM cases, a compiling Java enumeration example and one Forge handshake/registry case. Normal/clean build/style
checks, 535 JVM tests, 238 Java 8 Forge tests and all 532 frozen pre-change tests pass. The example compiles against
only the dev jar and Java 8 APIs and has no Scala bytecode references; FMP's runtime dependencies remain in place.
- Binary comparison retains all 443 class APIs with exactly one new public static method and two deprecations.
All 17 ScalaSignature payloads, 3,746 existing method bodies and 116 generated dumps are unchanged, allowing only
the expected deprecation metadata and build-version literals. No production class or dependency is added.
- Added the material-enumeration guide and the first consumer adoption ledger rows. UtilitiesInExcess's two source
call sites and Extra Utilities' tuple-array readers have supported replacements; consumer changes, releases and
pack adoption remain pending. Evidence is under ignored `run/migration-material-enumeration-reference/`.
@@ -3,6 +3,8 @@ package codechicken.multipart.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.network.NetHandlerPlayServer;
@@ -22,6 +24,24 @@ import scala.Tuple2;
class MicroblockPacketHandlerFunctionalTest {
@Test
void javaEnumerationUsesTheSameIdsAsTheInitializedRegistryAndHandshake() {
PacketCustom outgoing = new PacketCustom("test", 1);
MicroMaterialRegistry.writeIDMap(outgoing);
PacketCustom incoming = new PacketCustom(outgoing.getByteBuf().copy());
int count = MicroMaterialRegistry.materialCount();
assertTrue(count > 0);
assertEquals(count, incoming.readInt());
for (int id = 0; id < count; id++) {
String name = MicroMaterialRegistry.materialName(id);
assertEquals(name, incoming.readString());
assertEquals(id, MicroMaterialRegistry.materialID(name));
assertNotNull(MicroMaterialRegistry.getMaterial(id));
assertSame(MicroMaterialRegistry.getMaterial(name), MicroMaterialRegistry.getMaterial(id));
}
assertEquals(0, incoming.getByteBuf().readableBytes());
}
@Test
void sendsTheCompleteMaterialIdMapDuringTheServerHandshake() {
MinecraftServer server = MinecraftServer.getServer();
@@ -89,6 +89,13 @@ public final class MicroMaterialRegistry$ {
return MicroMaterialRegistry.getMaterial(id);
}
/**
* Returns the same legacy backing array as {@link MicroMaterialRegistry#getIdMap()}.
*
* @deprecated For enumeration, use {@link MicroMaterialRegistry#materialCount()},
* {@link MicroMaterialRegistry#materialName(int)} and {@link MicroMaterialRegistry#getMaterial(int)}.
*/
@Deprecated
public Tuple2<String, IMicroMaterial>[] getIdMap() {
return MicroMaterialRegistry.getIdMap();
}
@@ -241,6 +241,15 @@ public final class MicroMaterialRegistry {
return idWriter.read(data);
}
/**
* Returns the persistent name at a numeric ID in the active material map. IDs may change when the map is rebuilt or
* synchronized with a server; store material names rather than numeric IDs in persistent data.
*
* @param id an ID from zero (inclusive) to {@link #materialCount()} (exclusive)
* @throws ArrayIndexOutOfBoundsException if the ID is outside the active map
* @throws NullPointerException if the map is uninitialized or the ID is unresolved after a failed
* handshake
*/
public static String materialName(int id) {
return idMap[id]._1();
}
@@ -258,10 +267,47 @@ public final class MicroMaterialRegistry {
return typeMap.get(remapped(name));
}
/**
* Returns the registered material instance at an ID in the active material map; no copy is made.
*
* @param id an ID from zero (inclusive) to {@link #materialCount()} (exclusive)
* @throws ArrayIndexOutOfBoundsException if the ID is outside the active map
* @throws NullPointerException if the map is uninitialized or the ID is unresolved after a failed
* handshake
*/
public static IMicroMaterial getMaterial(int id) {
return idMap[id]._2();
}
/**
* Returns the number of numeric ID slots in the active material map, including the missing-material placeholder
* when present. Enumerate IDs from zero (inclusive) to this count (exclusive) with {@link #materialName(int)} and
* {@link #getMaterial(int)}.
*
* FMP builds the map during post-initialization and before server startup. A multiplayer handshake replaces the
* client map with server ID order. Enumerate on the game/lifecycle thread after initialization; multiplayer clients
* must also finish the handshake successfully. The map must not be rebuilt during enumeration. Do not cache numeric
* IDs across map replacements. A failed handshake can leave unresolved slots, which this count includes; it is not
* a readiness check.
*
* @return the active map length, or zero for an initialized empty map
* @throws IllegalStateException if FMP has not initialized the material ID map
*/
public static int materialCount() {
if (idMap == null) {
throw new IllegalStateException("The material ID map has not been initialized.");
}
return idMap.length;
}
/**
* Returns the legacy live backing array, or null before ID-map initialization. A map replacement leaves earlier
* array references pointing at the old map; unresolved IDs after a failed handshake contain null.
*
* @deprecated For enumeration, use {@link #materialCount()}, {@link #materialName(int)} and
* {@link #getMaterial(int)}. The Scala array descriptor is retained for existing consumers.
*/
@Deprecated
public static Tuple2<String, IMicroMaterial>[] getIdMap() {
return idMap;
}
@@ -35,11 +35,12 @@ import codechicken.lib.vec.Cuboid6;
import codechicken.lib.vec.Vector3;
import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer;
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial;
import codechicken.microblock.examples.MaterialEnumerationExample;
/**
* The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the
* cases below only read it. Error paths are not covered: they call the microblock logger, which is null until
* MicroblockProxy.preInit runs, so they cannot execute headless.
* cases below restore any temporary state changes. Logger-backed error paths require MicroblockProxy.preInit and cannot
* execute headless.
*/
class MicroMaterialRegistryCharacterizationTest {
@@ -129,6 +130,48 @@ class MicroMaterialRegistryCharacterizationTest {
assertEquals(java.lang.reflect.Array.getLength(original), MicroMaterialRegistry.getIdMap().length);
}
@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);
}
}
@Test
void getMaterialByNameResolvesRemapsAndReturnsNullWhenUnknown() {
assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:glass"));
@@ -0,0 +1,21 @@
package codechicken.microblock.examples;
import java.util.ArrayList;
import java.util.List;
import codechicken.microblock.MicroMaterialRegistry;
/** Compiling example for docs/api/MATERIAL_ENUMERATION.md; call after material IDs are ready. */
public final class MaterialEnumerationExample {
private MaterialEnumerationExample() {}
public static List<String> materialNames() {
int count = MicroMaterialRegistry.materialCount();
List<String> names = new ArrayList<>(count);
for (int id = 0; id < count; id++) {
names.add(MicroMaterialRegistry.materialName(id));
}
return names;
}
}