Commit Graph
158 Commits
Author SHA1 Message Date
Algent 57b314dd4e test: freeze multipart NBT and description packets 2026-08-27 18:11:28 +03:00
Algent 6410cc0028 test: characterize multipart ordering and move lifecycle 2026-08-27 18:02:41 +03:00
Algent 4616b5834c test: guard source-only consumer reflection contracts 2026-08-27 14:11:53 +03:00
Algent 49ed6fd751 fix: restore Schematica registry reflection compatibility 2026-08-27 14:02:47 +03:00
Algent fa976c2002 refactor: port MultipartRenderer to Java
Kept in the TickScheduler$ shape: the companion holds the implementation
because Forge registers that instance itself, and the plain class holds static
forwarders. Both halves are load-bearing for different reasons -- guidenh
resolves renderWorldBlock as a static first and falls back to MODULE$ -- and
neither has a bytecode consumer, only reflective names.

The emitted class list is unchanged. All five rendering callbacks keep their
signatures, MODULE$ keeps its type and flags, and both classes keep class-level
@SideOnly(CLIENT). Only three Scala forwarders for inherited
TileEntitySpecialRenderer members and the companion clinit's ACC_PUBLIC differ.

The render id is still claimed in the singleton's initialiser, matching the
reference's first-access timing. The breaking-overlay branch moved to a private
helper that checks the erased Tuple2 and the boxed index before unboxing, which
is what the reference's pattern match compiled to.

125 plain-JVM tests and 22 Forge server tests pass, none changed by the port.
This class needs a client, so it adds nothing to either suite and everything to
the manual checklist.
2026-08-15 20:13:20 +02:00
Algent da472f1fd3 test: characterize MultipartRenderer
Everything this class does needs a client, so nothing here can run it. What is
pinned is the contract two other parties depend on and the ABI diff cannot see.

guidenh reflects on both MultipartRenderer and MultipartRenderer$ by name,
calling renderWorldBlock as a static first and falling back to the companion's
MODULE$, so both the static's exact signature and the singleton field are
pinned. Forge needs that singleton to be both a TileEntitySpecialRenderer and
an ISimpleBlockRenderingHandler or it rejects the registration. Both classes
carry class-level @SideOnly(CLIENT) and are stripped on a dedicated server.

Every lookup uses initialize = false: initialising the companion runs its
constructor, which claims a render id from Forge's RenderingRegistry.

125 plain-JVM tests, 22 Forge server tests.
2026-08-15 20:09:49 +02:00
Algent d224ce116d refactor: remove the package objects instead of porting them
A Scala package object compiles to a class literally named 'package', and that
is a Java keyword, so no Java source can declare or even name it. The
characterization had to reach both through Class.forName, which is the argument
in one line. There is no Java form to port to.

Each held exactly one member, a logger alias for its proxy, so the indirection
is inlined at its eight call sites: ConfigContent (7) and MultipartGenerator
(1). The functional characterization asserted beforehand that each alias
returned the very instance its proxy holds, so the substitution is an identity;
both classes are descriptor-identical afterwards.

Exactly four classes leave the jar and nothing else changes. No consumer
referenced them, and no Java consumer could have.

120 plain-JVM tests and 22 Forge server tests pass. PackageObjectCharacterization
went with its subject; ProxyLoggerFunctionalTest keeps the assertion worth
having, that both proxies publish a logger during preInit.
2026-08-15 20:04:55 +02:00
Algent d6bba7ff46 test: characterize the package objects
Each holds one member, a logger alias for its proxy's logger. Everything goes
through reflection because 'package' is a Java keyword and cannot appear as a
type name in Java source, which is also why these two classes have no Java
form at all.

The plain-JVM half pins the shape and that the alias reads the proxy's var
rather than holding anything, so it reports null before preInit. The Forge
server half pins the part that matters for what comes next: after preInit each
alias returns the very instance its proxy holds, adding nothing.

123 plain-JVM tests, 23 Forge server tests.
2026-08-15 20:02:42 +02:00
Algent a2797c986b refactor: port ControlKeyModifer and ControlKeyHandler to Java
isControlDown(EntityPlayer) and isClientPressing() keep their descriptors.
map() became a java.util.Map and isClientPressing_$eq became setClientPressing.

Dropped playerControlValue and ControlKeyValue. They existed only to give Scala
player.isControlDown sugar through an implicit conversion, and the reference
already shipped the explicit static 'for Java users'. MicroblockPlacement now
calls that static directly.

ControlKeyHandler was a Scala object extending KeyBinding, so the instance both
the key registry and the event bus needed was ControlKeyHandler$.MODULE$. It is
now an ordinary KeyBinding subclass with an INSTANCE constant, which also drops
the twelve static forwarders Scala generated for inherited KeyBinding members.

The misspelling in ControlKeyModifer is the published type name and is kept;
only the file name, which said ControlKeyModifier.scala, is corrected.

120 plain-JVM tests and 21 Forge server tests pass, none of them changed by the
port.
2026-08-15 19:45:53 +02:00
Algent 8f73a693cc test: characterize ControlKeyModifer
Plain-JVM coverage is structural: the Java-facing isControlDown(EntityPlayer)
static that the reference scaladoc calls out as existing for Java callers, and
the typo in the class name, which is the published name.

Forge server coverage for the behavior. isControlDown branches on whether the
player's world is remote, so it needs a real player; a Forge FakePlayer on a
dedicated server world takes the server branch and reads the map the client
keeps in sync through packet 1. Unknown players default to not holding, the map
reflects the last recorded value, and clearing resets it.

The client branch reads isClientPressing instead and no harness has a client.

120 plain-JVM tests, 21 Forge server tests.
2026-08-15 19:42:38 +02:00
Algent 40bb1b71b7 refactor: port PacketScheduler to Java
IScheduledPacketPart is descriptor-identical and schedulePacket is unchanged.
TScheduledPacketPart.read stays abstract: TMultiPart declares read, so a
default would be shadowed and a part would silently read a description instead
of a mask. The dispatch that lived in the $class helper is now the interface
static readMask, with the one-line forwarder documented on read.

writeScheduled and readScheduled became defaults. TMultiPart declares neither,
and empty bodies are what mixing the Scala trait in already gave a Scala
implementor -- only Java implementors ever had to write them out.

sendScheduled is public because the reference's private[multipart] reaches the
handler package and Java has no equivalent scope. An unsupported mask width now
throws IllegalArgumentException rather than scala.MatchError; both are
RuntimeException and the map is still left uncleared on that path, as before.

Dropped PacketScheduler$, TScheduledPacketPart$class and two closure classes,
none referenced in bytecode or by reflective name.

118 plain-JVM tests and 18 Forge server tests pass.
2026-08-15 17:04:41 +02:00
Algent 3ad25f56cb test: characterize PacketScheduler
Plain-JVM coverage is structural: the shapes of IScheduledPacketPart and
TScheduledPacketPart, and the fact that read must stay abstract. TMultiPart
declares read, so a superclass method would beat any default the interface
tried to supply, and a part that does not declare it resolves to TMultiPart's,
which reads a description rather than a mask.

Forge server coverage for the behavior, which needs a part bound to a tile in a
non-remote world and a real write stream: masks OR-ing into one write, the map
clearing after a send, a part that lost its tile being skipped, all four
documented mask widths being accepted, and an undocumented one being rejected
rather than silently writing nothing.

117 plain-JVM tests, 18 Forge server tests.
2026-08-15 17:01:18 +02:00
Algent 31ec83f91b refactor: port TileCache to Java
The first port free to change shape rather than preserve it: TileCache has zero
downstream references in bytecode and does not appear in guidenh's reflective
names. map() became a java.util.Map, add/remove became void, and apply returns
a nullable FlaggedTile instead of a scala.Option. No internal caller used any
of the discarded return values.

Dropped TileCache$, TileCache$FlaggedTile$ and FlaggedTile's case-class
machinery. It is only ever a map value, never compared, hashed, printed or
destructured.

findTile is unchanged, including the branch the reference obscures: its
trailing 'case _ => null' is unreachable because Some(FlaggedTile(t, rem))
matches whatever the flag holds, so a tile flagged removed is returned like any
other and only the warning is suppressed. That is pinned by a functional test
written before the port.

114 plain-JVM tests and 13 Forge server tests pass; the only test change is the
two-line adapter that stopped unwrapping a scala.Option.
2026-08-15 16:23:47 +02:00
Algent b92d4b7af4 test: characterize TileCache
Plain-JVM coverage for the map half: add stores the tile unflagged, remove
replaces the entry rather than dropping it, entries are keyed by coordinate
value rather than identity, and clear empties everything.

Forge server coverage for findTile, which needs a world. It pins the branch
that is easy to misread: the removed flag only suppresses the warning, and a
tile flagged removed is still returned, because the Some pattern matches
regardless of the flag and the reference's trailing null case is unreachable.
The empty-cache path throws with a message starting 'DC: Client multipart @'.

114 plain-JVM tests, 13 Forge server tests.
2026-08-15 16:21:14 +02:00
Algent c3d5b08ff7 refactor: port MultipartHelper to Java
MultipartHelper, MultipartHelper$ and MultipartHelper$IPartTileConverter are
all public-member- and descriptor-identical to the reference, and the full
emitted class list is unchanged -- this port neither adds nor removes a class.
MultipartHelper gains a private constructor and its companion's class
initializer loses ACC_PUBLIC, neither of which is nameable by a caller.

MultipartHelper$ was kept deliberately. It is not among the 17 MODULE$
singletons read from bytecode and no jar links against it, but guidenh names it
as a reflective string, and removing a companion breaks that invisibly.

createTileFromParts now builds one JavaConversions wrapper for both calls
instead of one per call site, and convertMulti returns Collections.emptyList or
singletonList instead of a Scala Seq wrapper. The declared type was always
java.lang.Iterable.

107 plain-JVM tests and 10 Forge server tests pass unchanged.
2026-08-15 15:47:17 +02:00
Algent 0fdf9e5e75 test: characterize MultipartHelper
Plain-JVM coverage for the half that needs no world: the id guard on
createTileFromNBT and the whole of IPartTileConverter. MultipartSaveLoad
cannot class-initialize headless, which makes it a usable probe -- returning
null rather than raising a LinkageError is what proves the guard short-circuits
before the loadingWorld assignment.

Forge server coverage for the rest, none of which is reachable headless:
createTileFromParts runs the ASM generator, the NBT round trip needs the
save/load hooks, registerTileConverter appends to a Scala MutableList, and
sendDescPacket needs a loaded chunk.

Also pins the two static descriptors other jars link against and the
MultipartHelper/MultipartHelper$ names guidenh reflects on, which the ABI diff
cannot see.

107 plain-JVM tests, 10 Forge server tests.
2026-08-15 15:43:18 +02:00
Algent 9db07f8c7c refactor: port the marker interfaces to Java
TSlottedPart, IRandomDisplayTick, INeighborTileChange, TRandomUpdateTick,
ISidedHollowConnect and IMicroMaterialRender.

All six are member- and descriptor-identical to the reference. The seven
implementors and mixin tiles were diffed too and are unchanged member for
member. Five were already pure abstract interfaces in bytecode, so their
conversion has no bytecode consequence; exactly one class leaves the jar,
TRandomUpdateTick$class.

onWorldJoin cannot become a default: TMultiPart declares it, and a superclass
method beats an interface default, so the auto-registration would silently
never run. It stays abstract and implementors call TickScheduler.loadRandomTick
themselves, as RedstoneTorchPart already did.

No bridge was written for TRandomUpdateTick$class. No jar in the pack
references TRandomUpdateTick in any form, and the frozen baseline holds exactly
the eight helpers the inventory lists.

99 plain-JVM tests and 5 Forge server tests pass.
2026-08-15 15:32:45 +02:00
Algent 3e68e67599 test: characterize the marker interfaces
TSlottedPart, IRandomDisplayTick, INeighborTileChange, TRandomUpdateTick,
ISidedHollowConnect and IMicroMaterialRender carry no implementation, so their
characterization is their shape: every member abstract and public, no
superinterface, and the exact member names.

Two of them extend TMultiPart in Scala source but emit a bare interface, and
IMicroMaterialRender is satisfied entirely by TMultiPart's Scala-style
world/x/y/z accessors, so the names are load-bearing.

TRandomUpdateTick's onWorldJoin auto-registration lives in its $class helper.
A part that does not declare onWorldJoin resolves it to TMultiPart's no-op;
that is pinned so the port cannot quietly turn it into an interface default,
which the superclass would shadow.
2026-08-15 15:28:17 +02:00
Algent cd5b801878 refactor: port BlockMultipart to Java
Converts the Block face of the API and its companion object.
BlockMultipart$ is descriptor-identical to the reference including
reduceMOP's Tuple2 return, and the class differs by exactly one
descriptor: hasTileEntity$default$1, the accessor Scala emits for a
default argument, which Java has no equivalent for. No jar references any
member of this class.

addCollisionBoxesToList keeps its descriptor; only its generic signature
changes from Scala's List<?> to the raw List that vanilla's Block actually
declares, which is the more faithful override.

getClientTile returns TileMultipartClient, a Scala trait extending
TileMultipart, so in bytecode it is a bare interface carrying none of the
class's members; addHitEffects casts to reach the part list. Third
occurrence of that pattern after JIconHitEffects and TRandomUpdateTick.

Four anonfun classes removed, including two nested inner closures.

Validated with 90 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite. Most of this class needs a player, a ray trace or
a renderer, so breaking, selection, collision, pick block, activation and
particles stay on the manual checklist.
2026-08-15 11:45:56 +02:00
Algent 213ff0c8ea refactor: port TickScheduler to Java
Converts the scheduler, its two extension classes and PartTickEntry. No
jar in the pack references this class, so the ABI constraint was minimal,
but the reference shape was kept anyway: TickScheduler$ and PartTickEntry
are descriptor-identical including the Scala-style accessor pairs, and the
statics class adds only a private constructor.

TickScheduler$ is the one companion singleton in this migration that is
not a pure forwarder. The object extends WorldExtensionInstantiator and
WorldExtensionManager is handed the instance itself, so the two
instantiator hooks stay on the singleton.

processTicks filtered a ListBuffer with a side-effecting predicate that
fires part callbacks and mutates the entry; the Java version iterates and
rebuilds in the same order, so callback order and the reschedule mutation
are unchanged.

One Scala call site changed: the registration now passes
TickScheduler$.MODULE$. TRandomUpdateTick needed casts because a Scala
trait extending a class is a bare interface in bytecode, the same pattern
already recorded for JIconHitEffects.

Five anonfun classes removed.

Validated with 90 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite, which runs this code every tick because the
scheduler is registered as a world extension at startup.
2026-08-15 11:38:07 +02:00
Algent 1b9eb05726 refactor: port TMultiPart to Java
Converts the base class every part extends. The cleanest port so far:
every descriptor is identical to the reference across all members, no
public member is lost or added, @SideOnly sits on the same seven members,
and the same two members stay deprecated.

No in-repo Scala call site needed changing, which is a first for a type
this widely used. Scala resolves the empty-paren Java accessors as it did
the Scala ones, and assignment to tile still desugars to tile_$eq.

The only real implementation is collisionRayTrace. getSubParts was
traversed through JavaConversions purely to get Scala's map, but
rayTraceCuboids already took a java.util.List, so the Java loop builds
that list directly and the conversion disappears along with the single
anonfun class.

Validated with 90 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-15 11:23:42 +02:00
Algent cacc9a30a8 refactor: port TileMultipart to Java
Converts the central tile class and its companion object. TileMultipartClient
moves to its own Scala file unchanged: it is registered with the ASM
generator through registerTrait, so converting it is Phase 5
registerJavaTrait work rather than part of this step.

Every public member of the reference survives except protected$worldObj,
the accessor Scala emitted so the dropItems closure could reach the
protected field; no jar references it, and TileMultipartClient lost
nothing including its generated super accessor for markRender. All five
Scala-typed descriptors are preserved: partList and its setter, operate,
getOrConvertTile2's Tuple2, loadParts, and the Tuple2 rayTraceAll stores
into ExtendedMOP.data.

partList stays an immutable Scala List republished on each mutation, which
matches the reference semantics exactly and takes no performance decision;
that belongs to Phase 4 with profiling. Internal callers still go through
operate rather than a private loop, so a trait overriding it still sees
every call.

Recompiled Scala consumers need four adjustments, all source-only:
partList.apply(i) for indexing, renderID_$eq for assignment, an explicit
cast on getOrConvertTile2._2, and an explicit AbstractFunction1 for
operate since 2.11 will not adapt a lambda to Function1[_, BoxedUnit].
Eleven, one, two and one in-repo call sites respectively.

31 anonfun classes collapse to one Java anonymous class.

Validated with 90 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite, which is the load-bearing check here because the
ASM generator builds composite tiles on top of this class.
2026-08-15 11:15:03 +02:00
Algent beb0995e60 test: characterize TileMultipart part storage and predicates
Gate step before converting the central tile. Covers the surface that does
not need a world: the part list is replaced rather than mutated so a
previously published Seq never changes under a caller, jPartList reflects
the current parts, operate skips parts whose tile has been cleared,
getLightValue is the maximum and zero when empty while
getExplosionResistance is the maximum but throws when empty, occlusionTest
needs agreement in both directions, canReplacePart excludes the outgoing
part but still rejects a part already present, and solidity falls back to
partMap being null on a bare tile.

One assumption was wrong and the test caught it: TMultiPart.doesTick
defaults to true, so a default part starts the tile ticking and a part has
to opt out to leave it idle. The case now records the real behavior.

Anything touching worldObj, the registry or packets stays out of this
layer because it cannot run headless; the Forge server suite covers that
side.

90 plain-JVM tests, all passing against the untouched Scala.
2026-08-15 10:49:57 +02:00
Algent b9d5e6ab52 refactor: port MultiPartRegistry to Java
Replaces MultiPartRegistry.scala with a Java registry, its companion
singleton and the three nested interfaces. All three interfaces are
descriptor-identical to the reference and MODULE$ keeps every instance
method downstream links against. Twelve jars reference this type.

Both Scala-typed registerParts descriptors are preserved, since
ForgeRelocationFMP links against the Function2 form and ProjRed against
the IPartFactory2 Seq form. That leaves TileMultipart.partList as the last
Scala-typed descriptor in the inventory.

The two array overloads are declared varargs. The descriptor is unchanged
and ACC_VARARGS does not affect linkage, but MicroblockClass called the
Scala varargs form with a single string, which no longer resolves against
a plain array parameter.

readIDMap moves to java.util.List and the mangled accessors Scala emitted
for its closures are gone; neither is referenced by any jar. Eight anonfun
classes and one anonymous class are replaced by the two Java adapters the
deprecated overloads build.

Validated with 78 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite, which is the only layer where registration can run
at all since it needs FML's active mod container.
2026-08-15 10:35:15 +02:00
Algent ff380ee3bc test: characterize MultiPartRegistry
Splits the characterization across both layers, because part registration
needs FML's active mod container and throws without one, so it cannot run
headless at all.

Plain JVM covers what is reachable: converters are dispatched per block,
a converter returning null does not stop the scan, converters registered
for another block are never consulted, a converter claiming several blocks
is registered for each, and an unknown mod container throws rather than
returning null.

The Forge server suite covers registration itself, and deliberately
exercises both overloads downstream depends on. Content registers through
the deprecated IPartFactory array form that seven jars use, and each
microblock class registers through the IPartFactory2 varargs form that
ProjRed uses, so resolving mc_torch and mcr_face proves both paths. Part
ids also round trip through the shared IDWriter carrier.

78 plain-JVM tests and 5 server tests, all passing against the untouched
Scala.
2026-08-15 10:31:59 +02:00
Algent edd6136354 refactor: port MicroMaterialRegistry to Java
Replaces MicroMaterialRegistry.scala with a Java registry, its companion
singleton and the two nested interfaces. Both nested interfaces are
descriptor-identical to the reference and MODULE$ keeps every instance
method ProjRed links against. Ten jars reference this type and three more
reach it reflectively, so the surface was checked member by member rather
than by descriptor count.

getIdMap keeps its scala.Tuple2 array because extrautilities links against
it, so the registry still stores tuples internally. readIDMap moves from
scala.collection.Seq to java.util.List, which no jar references; the one
in-repo caller moved from mkString to String.join. That clears one of the
five Scala-typed descriptors the inventory identified.

Three removals, all verified unreferenced: the mangled public accessors
Scala emitted so its closure classes could reach private state,
IMicroMaterial$class, and the MissingMicroMaterial forwarders for the two
members it does not override. The last of those was caught by the
characterization failing to compile, not by inspection.

IMicroMaterial's loadIcons, canRenderInPass and isSolid become real Java
defaults, so implementors can inherit rather than restate them. Eleven
anonfun classes are gone.

Validated with 73 plain-JVM tests passing against both the Scala baseline
and the port, a clean build, and the Java 8 Forge dedicated-server suite,
which exercises setupIDMap and calcMaxCuttingStrength during real startup.
2026-08-14 22:08:30 +02:00
Algent dc758d4310 test: characterize MicroMaterialRegistry
Freezes the registry lifecycle before the port: setupIDMap sorts the type
map by name into the id array, ids are the index into it, materialID and
materialName round trip, remapName resolves on both getMaterial(String)
and materialID, getMissingId points at the registered placeholder rather
than falling back to zero, ids round trip through the shared IDWriter, and
a highlight renderer that claims the highlight short-circuits the default
client renderer.

Also freezes the two members IMicroMaterial supplies. A Java class cannot
inherit them today, so they are observed through MissingMicroMaterial,
which overrides only isTransparent and loadIcons.

Two limits are deliberate and recorded in the test. The registry is global
mutable state, so both test classes guard their registrations and assert
only relative facts. The error paths are not covered at all: they call the
microblock logger, which is null until MicroblockProxy.preInit runs.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 52ff8ff, reading MODULE$ and the raw scala.Tuple2 array.

73 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 22:03:04 +02:00
Algent 52ff8ff460 refactor: port Saw to Java
Extracts the Saw trait out of ItemSaw.scala into a Java interface and
retains the Saw$class bridge for ProjRed. ItemSaw itself stays Scala. The
interface is descriptor-identical to the reference, the bridge adds only a
private constructor, and the emitted class-file set for this area is
unchanged.

Item does not declare getMaxCuttingStrength, so it becomes a real default
method. One consequence is worth noting: ItemSaw previously carried a
Scala trait forwarder for it and now inherits the default instead, so it
no longer declares that method. Three jars reference the ItemSaw type but
none reference its members, and an invokevirtual would still resolve
through the interface default. This is the first port that changed the
declared members of a class it did not touch.

Unlike TFacePart$class and TEdgePart$class, which hold constants and
ignore overrides, this bridge dispatches getCuttingStrength back through
the interface so an implementor's override is honoured. Both behaviors are
preserved as found and asserted separately.

First port outside codechicken.multipart.

Validated with 65 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 21:44:01 +02:00
Algent 844efac5a2 test: characterize Saw
Freezes the one supplied member before the port: getMaxCuttingStrength
asks the saw about a stack of itself, so the stack handed to
getCuttingStrength wraps the saw and has size 1.

Unlike the constant-returning bridges ported so far, this one dispatches
getCuttingStrength through the interface, so a subclass override is
honoured even when the bridge is called directly. That is asserted
explicitly.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 0de7283. It reports its strength only when handed a stack that actually
wraps itself, so the test proves the bridge built the stack rather than
only that it linked. ProjRed is the only consumer.

First characterization outside codechicken.multipart; these tests live in
codechicken.microblock alongside the type under test.

65 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 21:42:03 +02:00
Algent 0de728345a refactor: port TEdgePart to Java
Replaces TEdgePart.scala with a Java interface and retains the
TEdgePart$class bridge for OpenComputers. The interface is
descriptor-identical to the reference, the bridge adds only a private
constructor, and the emitted class-file set for this area is unchanged.

TMultiPart does not declare conductsRedstone and neither does anything in
the microblock superclass chain, so it becomes a real default method and
no in-repo Scala needed changing. The generated microblock classes now
resolve the interface default instead of a Scala trait forwarder, which
the functional generation case added during the TFacePart port covers.

conductsRedstone is kept on the bridge even though OpenComputers only
links against $init$, since the bridge class is being kept regardless.

Validated with 61 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 21:30:14 +02:00
Algent f82b5d2294 test: characterize TEdgePart
Freezes the single trait default before the port: conductsRedstone returns
false, TEdgePart extends TSlottedPart, and the $class static holds the
default rather than dispatching, so an overriding part gets its own value
from a direct call but false from the bridge.

The member is abstract on the reference interface, so a Java class cannot
inherit it today; the test part mirrors the forwarder Scala emits instead,
which keeps the file compiling before and after the port.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at f2f1870. OpenComputers is the only consumer, and it links against
$init$ rather than conductsRedstone.

61 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 21:28:51 +02:00
Algent f2f1870931 refactor: port TItemMultiPart to Java
Replaces ItemMultiPart.scala with Java implementations of JItemMultiPart,
TItemMultiPart and the TItemMultiPart$class bridge. The interface is
descriptor-identical to the reference, JItemMultiPart keeps its
constructor and both instance methods and gains only additive statics, and
the bridge keeps all three public statics for ProjRed, its only consumer.
The emitted class-file set for this area is unchanged.

This is the first converted trait that extends a Minecraft class rather
than TMultiPart, so the linearization check ran against Item. The same
split applied: getHitDepth became a real default because Item does not
declare it, while onItemUse stayed abstract because Item does and a
default would silently lose to the vanilla implementation. The rule
generalises to the implementor's actual superclass.

The bridge's getHitDepth ignores its part argument and calls the static
directly, matching the reference whose trait body never used this. The
onItemUse path still dispatches getHitDepth through the interface, so an
implementor's override is honoured.

Validated with 57 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 21:25:57 +02:00
Algent 6813144d84 test: characterize TItemMultiPart
Freezes the placement logic before the port. getHitDepth projects the hit
vector onto the axis for that side and adds 1 on even sides, giving the
six frozen values, and it copies the vector rather than mutating the
caller's. onItemUse tries the clicked block first only when the depth is
below 1, then always tries the neighbour on that side, reusing and
mutating a single BlockCoord across both attempts.

Placement short-circuits as soon as newPart returns null, before any
world, player or stack access, so all of that is reachable headless with
null arguments. Real placement needs a world and stays on the functional
and manual layers.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 7843f13, whose forwarders call all three statics. ProjRed is the only
consumer of this bridge.

57 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 21:23:45 +02:00
Algent 7843f13792 refactor: port TIconHitEffects to Java
Replaces TIconHitEffects.scala with Java implementations of IconHitEffects,
JIconHitEffects, TIconHitEffects and both $class bridges. The two
interfaces are descriptor-identical to the reference; the statics class
and both bridges add only a private constructor. A frozen Scala consumer
compiled against the reference jar still links through all five statics.

The default-versus-abstract rule discriminates within this one file.
JIconHitEffects.getBreakingIcon becomes a real default because TMultiPart
does not declare it, so Java implementors can now inherit it.
TIconHitEffects.addHitEffects and addDestroyEffects stay abstract because
TMultiPart does declare them and a default would silently lose.
MicroblockClient, the one in-repo case, now declares that pair explicitly.

Removes IconHitEffects$ and its MODULE$ field. No jar in the pack
references it, in bytecode or by name; consumers use either the static
forwarders on IconHitEffects or the two $class bridges. This is the first
public class dropped rather than a method, and it contrasts with
NormalOcclusionTest$, which is referenced and was kept.

Evaluation order inside addDestroyEffects is preserved rather than hoisting
the tile read, since the characterization pins which work happens before a
null tile fails.

Validated with 49 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 21:16:28 +02:00
Algent 07ffc01db1 test: characterize TIconHitEffects
Freezes what is reachable without a client: getBreakingIcon falls back to
getBrokenIcon for the same side and ignores its subPart argument, destroy
effects request sides 0 through 5 in order, and the scaleDensity overload
decides whether the part's own bounds are consulted at all. The particle
calls need a world and an EffectRenderer, so actual appearance stays on
the manual checklist.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 57130a6. It exercises both bridges: getBreakingIcon proves
JIconHitEffects$class delegates to the fixture's getBrokenIcon, and
addDestroyEffects proves TIconHitEffects$class resolves by collecting all
six icons before failing on the null tile.

49 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 21:12:52 +02:00
Algent 57130a6955 refactor: port TFacePart to Java
Replaces TFacePart.scala with a Java interface and retains the
TFacePart$class bridge for OpenComputers, ProjRed and ProjectBlue. The
interface is descriptor-identical to the reference; the bridge adds only a
private constructor.

This is the first converted trait where Java default methods are safe.
TMultiPart declares neither solid nor redstoneConductionMap, and neither
does anything in the microblock superclass chain, so there is no
superclass method for a default to lose to. Both members therefore become
real defaults rather than staying abstract with explicit forwarders, and
no in-repo Scala needed changing. The rule for the remaining traits is to
check that condition before choosing.

A side effect is that Java classes can now inherit these members;
McSidedMetaPart previously had to declare both just to accept the
defaults, and stays descriptor-identical.

Adds the first functional coverage of runtime class generation against a
ported type. Microblock classes are built by ASM at run time, so the
server suite now checks that a generated FaceMicroblock resolves the
inherited default and that a generated HollowMicroblock still returns its
own overrides.

Validated with 43 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite at 3 tests.
2026-08-14 21:00:18 +02:00
Algent d71a4b893d test: characterize TFacePart
Freezes the trait defaults before the port: solid returns true for every
side including out-of-range values, and redstoneConductionMap returns 0.
Also freezes that TFacePart extends TSlottedPart, and that the $class
statics hold the defaults rather than dispatching, so an overriding part
gets its own values from a direct call but the trait values from the
bridge.

Both members are abstract on the reference interface, so a Java class
cannot inherit them today; the test part mirrors the forwarders Scala
emits instead, which keeps the file compiling before and after the port.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at a1bf3c8, whose forwarders call all three statics. OpenComputers,
ProjRed and ProjectBlue depend on them.

43 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 20:56:55 +02:00
Algent a1bf3c8c86 refactor: port TNormalOcclusion to Java
Replaces TNormalOcclusion.scala with Java implementations of the six types
it held: NormalOcclusionTest, NormalOcclusionTest$, JNormalOcclusion,
TNormalOcclusion, TNormalOcclusion$class and NormallyOccludedPart. The
singleton and both interfaces are descriptor-identical to the reference;
the other three add only private members. A frozen Scala consumer compiled
against the reference jar still links through forwarder, $class, singleton
and super accessor.

Scala emits trait super accessors as ACC_SYNTHETIC, which javac hides
entirely, so an abstract synthetic method cannot be implemented by any
Java class or by Scala recompiled against a Java interface.
codechicken$multipart$TNormalOcclusion$$super$occlusionTest is therefore a
non-synthetic default returning true, the identity for the && chain the
bridge builds. Method resolution ignores the flag, so existing call sites
are unaffected.

Both in-repo mixins needed work, in different ways. HollowMicroblock had
no occlusionTest override and now declares one. PostMicroblock already had
one ending in super.occlusionTest, which used to route through the trait
and would now reach Microblock directly, silently dropping the box test;
the same expression was substituted at that call site.

Two further source-only divergences, both recorded: apply sugar on
NormalOcclusionTest no longer compiles for Scala callers, and
NormallyOccludedPart loses the scala.runtime.Null$ getType variant that
Scala inferred for a null literal. No jar in the pack references either.

Validated with 38 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 20:30:11 +02:00
Algent 75900d0602 test: characterize TNormalOcclusion
Extends the occlusion characterization to the parts of the area the port
will touch: the NormalOcclusionTest$ singleton and its MODULE$ field, the
TNormalOcclusion$class bridge, the super accessor the bridge calls back
through, NormallyOccludedPart's single-box constructor that AE2 links
against, and its null getType.

The generated codechicken$multipart$TNormalOcclusion$$super$occlusionTest
accessor is ACC_SYNTHETIC on the reference interface, so javac neither
sees nor allows implementing it. It is reached by reflection here, and the
port has to expose it as an ordinary method for any new implementor to be
able to satisfy it.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at cd6420f. It mixes in the trait without overriding occlusionTest, so
invoking it covers the whole bridge round trip: forwarder, $class,
singleton, and the callback through the accessor. ForgeRelocationFMP,
OpenComputers and ProjRed all depend on that path.

38 plain-JVM tests, all passing against the untouched Scala.
2026-08-14 20:24:28 +02:00
Algent cd6420f7f2 refactor: port TCuboidPart to Java
Replaces TCuboidPart.scala with Java implementations of TCuboidPart,
JCuboidPart and the TCuboidPart$class bridge. The canonical behavior now
lives in three statics on JCuboidPart; the deprecated bridge delegates to
them, since old Scala forwarders call the statics directly and delegating
to the instance methods would recurse.

Every reference descriptor is preserved, verified by diffing javap -s
output against the reference dev jar. ForgeRelocationFMP, OpenComputers,
ProjRed and ProjectBlue all call these four statics, and a frozen Scala
2.11.5 consumer compiled against the reference jar loads and runs against
the port.

Scala trait linearization cannot be reproduced by a Java interface: a
superclass method beats an interface default on the JVM, so a recompiled
Scala class mixing TCuboidPart into a TMultiPart subclass silently gets
TMultiPart's empty implementations. Declaring the methods default would
not help, so they stay abstract, matching the reference bytecode. Existing
binaries are unaffected. Microblock, the one in-repo case, now extends
TMultiPart with TCuboidPart and forwards explicitly. See
JAVA_MIGRATION_DIVERGENCES.md.

The three files live under src/main/scala because compileJava runs before
compileScala and they still reference TMultiPart.

Validated with 33 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
2026-08-14 20:12:22 +02:00
Algent 8294240659 test: characterize TCuboidPart
Freezes the observable behavior of TCuboidPart, JCuboidPart and the
TCuboidPart$class bridge before the Java port, per the test-first gate in
JAVA_MIGRATION.md. All cases pass against the untouched Scala.

Records two asymmetries worth preserving: getSubParts wraps the bounds in
an IndexedCuboid6, whose copy constructor makes it a copy carrying a boxed
Integer 0 as data, while getCollisionBoxes aliases the bounds instance
itself. Cuboid6 has no equals, so geometry is compared component-wise.

Also adds a frozen Scala consumer compiled with 2.11.5 against the
reference dev jar at 246daff. Its forwarders call all four $class statics,
so it detects the linkage break that a freshly recompiled test would miss.
ForgeRelocationFMP, OpenComputers, ProjRed and ProjectBlue all depend on
those descriptors.

One case exists specifically to guard the port: TMultiPart supplies empty
getSubParts and getCollisionBoxes, and on the JVM a superclass method beats
an interface default, so a Java interface cannot reproduce Scala trait
linearization on its own.
2026-08-14 20:03:33 +02:00
Algent 246dafffda refactor: drop unused IDWriter and JPartialOcclusion Scala bridges
The downstream ABI inventory scanned 240 mod jars in GTNH daily
2026-08-14+678 and found no consumer of either bridge. Both were written
speculatively, before the inventory existed.

IDWriter had no downstream reference at all, so its four Scala function
accessors are removed and the class now selects a carrier width directly.
That also removes the Integer boxing and Function1/Function2 allocation
per read and write that the first port had preserved. Calling write or
read before setMax now throws IllegalStateException rather than
NullPointerException; no caller reaches that state.

JPartialOcclusion$class had no downstream reference either. The two
consumers that touch JPartialOcclusion only call getPartialOcclusionBoxes,
and the two jars that implement the interface emit no call to the helper.
The interface and both of its method descriptors are unchanged. The
ReferenceScalaPartialOcclusion fixture is removed with the helper it
existed to verify.

Validated with 27 plain-JVM tests, a clean checkstyleTest build with
Spotless re-enabled, and the Java 8 Forge dedicated-server suite.
2026-08-14 19:52:28 +02:00
Algent 78959e70e9 docs: inventory downstream ForgeMultipart ABI consumers 2026-08-14 19:48:01 +02:00
Algent 5c087c50b5 run spotless 2026-08-14 19:44:56 +02:00
Algent a0428b9994 refactor: port partial occlusion to Java
Keep the Scala trait helper so reference-compiled implementations remain linkable.
2026-08-14 18:24:59 +02:00
Algent 60818853e3 test: freeze Scala partial-occlusion ABI
Load a reference-compiled consumer so the Java bridge must retain the Scala trait helper.
2026-08-14 18:19:18 +02:00
Algent 2c610bdb48 refactor: port IDWriter to Java
Keep Scala function descriptors as deprecated bridges while adding primitive Java entry points.
2026-08-14 18:02:03 +02:00
Algent 07530d93bf test: characterize partial occlusion
Freeze voxel ownership and visibility rules before the Java port.
2026-08-14 17:46:30 +02:00
Algent a69f846775 test: characterize normal occlusion
Freeze contact tolerance and partial-box delegation before the Java port.
2026-08-14 17:38:53 +02:00
Algent 4ae2a6d0fa test: add Forge server characterization
Run JUnit after FML server startup and gate the task on fresh XML because Forge can exit zero after a test-mod exception.
2026-08-14 17:27:18 +02:00