- drop per-guide Validation sections; manual gates stay in MANUAL_CHECKS - drop audit/provenance lines (reference checkouts, dated refreshes, commit pins) - state version status and Scala-free compile check once in API.md - replace API.md status prose with link to migration status - fix RELEASE_NOTES dangling sentence - rewrite README for GTNH fork and Java port
5.5 KiB
Registering block converters from Java
API index · Factory registration · Conversion results
Use the existing MultiPartRegistry.registerConverter(IPartConverter) on both sides during common mod initialization,
after registering the source blocks. It already accepts ordinary Java types; no new wrapper, reflection or Scala
companion lookup is needed. Register the converted part's persistent factory separately with registerPartFactory.
Compiling example
BlockConversionExample.java registers one converter and its NBT/packet factory:
BlockConversionExample.register(yourSourceBlock);
Replace yourmod:converted and the sample part with your own stable type and implementation. The example copies
block metadata into a fresh part, preserves it through NBT and description packets, and leaves the source untouched.
It intentionally covers registration and state only: it supplies no custom geometry, rendering, drops or gameplay.
Existing published IDs, field names and packet formats must remain unchanged when migrating real parts.
blockTypes() returns a Java Iterable<Block>; Collections.singletonList or Arrays.asList covers ordinary cases.
These are block instances, not numeric IDs or metadata values. convert(world, pos) checks metadata or tile type as
needed and returns null to decline. The example checks the source block explicitly because it can also be called
directly. Read the source before returning: the registry does not subsequently call load or readDesc on this part.
The independent factory creates blank instances; normal reconstruction supplies their payload afterward.
Registration and dispatch
- Register once on the initialization thread. FMP retains the converter, enumerates its block iterable immediately, and appends it to each block's list. Mutating the iterable afterward does not change existing entries.
- Order matters: the first non-null result wins. Returning null continues; throwing stops dispatch and propagates. There is no priority field, fallback after an exception, result validation, or rollback.
- Duplicate blocks and repeated registration produce duplicate entries. There is no unregister operation. Avoid registration on world/server load, which can repeat within the same process.
- Unlike factory registration, converter registration does not enforce a mod-container or postInit state gate. Common preInit/init is the supported integration timing, not a promise of a runtime registration error afterward. Supply valid non-null blocks and a non-null iterable. An iterator failure leaves earlier registrations present.
convertBlock(world, pos, block)selects by its explicit block argument, passes the same world/position references to callbacks and returns the result unchanged. It does not check the world's block, call a factory, bind a part, replace the block or invoke lifecycle hooks. Treat the position as borrowed and do not mutate it.
Probes and committed conversion
Conversion can run repeatedly: a preview, getOrConvertTileResult, a rejected or accepted canPlacePart, then
addPart may each create a different part. Return a fresh, unbound instance each time. Do not drain inventories,
disconnect networks, remove the source block or transfer ownership inside convert. A rejected candidate receives
no compensating cleanup callback. Copy state without changing the original; use world.isRemote if construction
needs different client/server classes. The sample uses the same class on both paths.
An accepted lookup creates a bound, uninstalled placeholder. Normal server placement performs conversion again and then follows this sequence:
- The converted part is bound to a placeholder with world and position, while the original block/tile still exists.
invalidateConvertedTile()runs before replacement. Clear resources already copied into the part here when necessary to prevent the old block's removal from dropping them again. The base implementation does nothing.- FMP replaces the block, installs the placeholder, sends the block-change notification, then calls
onConverted(). The base implementation callsonAdded(). Preserve that lifecycle if overriding, unless deliberately replacing it. - FMP writes the converted part's add-part description, promotes the tile if the requested new part needs more capabilities, then adds that new part through its normal placement lifecycle.
Keep the tile returned by TileMultipart.addPart; even the tile observed inside onConverted() may later be replaced
by capability promotion. A callback failure propagates at its current stage; committed conversion is not transactional.
These hooks run on the server placement path, not on probes. Clients reconstruct from packets instead. For ordinary
placement, check canPlacePart first and call addPart only on the server. Neither a successful check nor a saved
placeholder reserves the location. See conversion results for lookup ownership.
Consumer migration
Chisel copies torch variant/orientation, ForgeRelocationFMP constructs a frame, OpenComputers inspects cable/print
tiles, and AE2 copies cable-bus state from the original tile. Preserve each consumer's eligibility, state-copy and
cleanup rules. Existing Java converter registrations need no rename; Scala consumers can retain Scala implementation
code while returning Java collections and calling the static entry. Their separate factory migration still uses
registerPartFactory.