From 4e8d238b575e7f0754322dc9d540f3cab797bd67 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 30 Aug 2026 13:56:05 +0200 Subject: [PATCH] refactor: port inventory tile trait to java Keep TIInventoryTile as AE2's public rebuild API and JInventoryTile as the registered mixin input, preserving both generated interfaces and flattened sided-slot behavior. --- JAVA_MIGRATION.md | 11 +- JAVA_MIGRATION_DIVERGENCES.md | 53 +++++ JAVA_MIGRATION_HANDOFF.md | 34 ++- .../multipart/scalatraits/JInventoryTile.java | 224 ++++++++++++++++++ .../scalatraits/TIInventoryTile.java | 87 +++++++ .../scalatraits/TIInventoryTile.scala | 126 ---------- 6 files changed, 391 insertions(+), 144 deletions(-) create mode 100644 src/main/scala/codechicken/multipart/scalatraits/JInventoryTile.java create mode 100644 src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.java delete mode 100644 src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.scala diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 46644d3..f2e972a 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -25,10 +25,11 @@ No trustworthy tool will produce a maintainable Java port automatically. A decom The binary and source-level consumer audits, low-coupling Phase 3 queue, audit-derived immediate compatibility gate, focused profile, first traversal optimization, complete redstone interaction helper unit, `MicroRecipe` port, and the -first five Java-trait ports are complete. `TPartialOcclusionTile` proved the no-field path, `TSlottedTile` proved state +first six Java-trait ports are complete. `TPartialOcclusionTile` proved the no-field path, `TSlottedTile` proved state and lifecycle rewriting, `TRedstoneTile` removed the measured redstone allocation, `TTileChangeTile` exercised mutable -state plus inherited access, and `TFluidHandlerTile` preserved ordered Forge fluid distribution. All retain their exact -runtime interfaces. See `JAVA_MIGRATION_PROFILE.md`. +state plus inherited access, `TFluidHandlerTile` preserved ordered Forge fluid distribution, and +`TIInventoryTile`/`JInventoryTile` preserved AE2's direct rebuild linkage plus flattened sided inventory routing. All +retain their exact runtime interfaces. See `JAVA_MIGRATION_PROFILE.md`. That audit found and this branch has now repaired one existing regression: Schematica 1.12.6 reflects the original private `MultiPartRegistry$` field `codechicken$multipart$MultiPartRegistry$$typeMap` and casts it to a Scala mutable map. The @@ -360,7 +361,7 @@ Exit condition: identified steady-state Scala allocation sites are removed with - [ ] Preserve pass-through interface behavior. - [x] Keep the completed OpenComputers `TSlottedTile.v_partMap` mutation/rebinding case green and cover ProjectRed/ Extra Utilities behavior before `TRedstoneTile`. -- [ ] Explicitly cover AE2's `TIInventoryTile.rebuildSlotMap` before the inventory trait. +- [x] Explicitly cover AE2's `TIInventoryTile.rebuildSlotMap` before the inventory trait. Exit condition: built-in behavior is implemented in Java while the established runtime composition mechanism remains stable. @@ -416,7 +417,7 @@ The work should move from low to high risk while keeping a buildable mixed-langu Extensive tests are both possible and recommended here. Their purpose is not to prove that the current Scala behavior is ideal; it is to make its observable behavior explicit before translation, so every later difference is intentional. The repository now has a Java-8-compatible JUnit Jupiter suite and a small Forge integration runner. The current -baseline is 141 plain-JVM tests and 37 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest +baseline is 153 plain-JVM tests and 70 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest framework, so game-dependent coverage continues through the existing narrow Forge harness rather than a second test framework. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index ef731b6..c0c26ca 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -1640,3 +1640,56 @@ multipart fluid distribution. This is a structural removal, not a throughput or - Complete plain-JVM suite: 153 tests, 0 failures, 0 errors. - Java 8 Forge dedicated-server suite: 62 tests, 0 failures, 0 errors. - The frozen consumer inventory and audited source consumers contain no `TFluidHandlerTile` reference. + +## 2026-08-30 — TIInventoryTile/JInventoryTile Java-trait port + +AE2 is the load-bearing consumer for this port. `CableBusPart.notifyNeighbors` checks a generated tile with +`instanceof TIInventoryTile` and directly invokes `rebuildSlotMap()`. The registered trait input is separately named +`JInventoryTile`; preserving only one of those types would break either registration or AE2 linkage. + +### Observable behavior + +No known runtime divergence. Each generated tile starts with distinct empty inventory-list and slot-map instances. +Binding appends only `IInventory` parts in order, removal deletes the matching inventory, and clear empties both the +superclass part list and inventory state. Copying from another inventory tile shares its inventory list by identity and +rebuilds a distinct slot map; copying from a plain tile leaves both current references untouched. + +The slot map still flattens every inventory in list order and routes each global slot to its original inventory and +local index. The fixed name, custom-name flag, stack limit, usability and no-op open/close behavior are unchanged. +Sided access still exposes only `ISidedInventory` slots, while its global base advances across every inventory; +insert/extract checks still delegate to sided inventories and return true for ordinary inventories. + +### Generated ABI and raw input divergence + +At Forge runtime `TIInventoryTile` remains a public interface extending exactly `ISidedInventory`, with no fields and +the exact same 28 methods: 20 behavior/inventory methods, four public state accessors, and four super accessors. +`JInventoryTile` remains a public child interface with no fields and the exact same 36 methods, adding four private- +field accessors and four `JInventoryTile` super bridges. The generated composite still owns the two private fields +`codechicken$multipart$scalatraits$JInventoryTile$$invList` and +`codechicken$multipart$scalatraits$JInventoryTile$$slotMap`, with `LinkedList` and `scala.Tuple2[]` descriptors. + +The raw dev artifact now contains a Java interface `TIInventoryTile`, a concrete Java `JInventoryTile` mixin input, +and package-private `JInventoryTileAccess` instead of the Scala interface, `$class` helper and concrete forwarding +class. No frozen consumer references `TIInventoryTile$class` or names `JInventoryTile`; AE2's load-bearing +`TIInventoryTile.rebuildSlotMap()` descriptor is unchanged. As with the other Java-trait ports, consumers compiling +against the untransformed dev jar should use a transformed compile stub when they name the registered mixin type. + +### Transformer constraint + +The Java-trait stack analyzer has no case for JVM opcode `NEWARRAY` (`188`). Creating the primitive result array in +`getAccessibleSlotsFromSide` initially failed trait registration with `scala.MatchError: 188`. The method now collects +the same ordered boxed slots and delegates only the final `int[]` allocation/copy to package-private +`JInventoryTileAccess`. Reference-array allocation remains inside the trait because its `ANEWARRAY` opcode is already +supported. The helper adds no public surface. + +### Validation + +- `TIInventoryTileFunctionalTest`: 7 Forge tests covering list/map lifecycle and identity, flattened routing, the + direct AE2-shaped rebuild call, fixed metadata, sided global offsets, and insert/extract delegation. +- `ForgeEnvironmentSmokeTest.generatesAndCachesInventoryTileClassWithBothPublicTraitInterfaces`: exact 28- and + 36-method runtime interfaces, inheritance, generated private fields, distinct initialization, setter rebinding, and + generated-class reuse. +- All eight tests passed against the untouched Scala implementation first and pass unchanged after the port. +- Complete plain-JVM suite: 153 tests, 0 failures, 0 errors. +- Java 8 Forge dedicated-server suite: 70 tests, 0 failures, 0 errors. +- Raw `javap` descriptors for both public types match the reference, including `scala.Tuple2[]` state accessors. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index e83c63b..7122248 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -12,8 +12,8 @@ what breaks, and what is left. The other documents hold the reasoning. | `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client | | `JAVA_MIGRATION_PROFILE.md` | The focused baseline, first measured result, findings, and exact rerun command | -Branch: `algent/java`. Base: `master`. 81 commits including separate characterization and port commits for -`TFluidHandlerTile`. +Branch: `algent/java`. Base: `master`. 83 commits including separate characterization and port commits for +`TIInventoryTile`/`JInventoryTile`. ## The one rule that matters @@ -67,7 +67,7 @@ awk -F'"' '/ invList = new LinkedList<>(); + + @SuppressWarnings("unchecked") + private Tuple2[] slotMap = (Tuple2[]) new Tuple2[0]; + + @Override + public LinkedList invList() { + return invList; + } + + @Override + public void invList_$eq(LinkedList inventories) { + invList = inventories; + } + + @Override + public Tuple2[] slotMap() { + return slotMap; + } + + @Override + public void slotMap_$eq(Tuple2[] slots) { + slotMap = slots; + } + + @Override + public void codechicken$multipart$scalatraits$TIInventoryTile$$super$copyFrom(TileMultipart that) { + super.copyFrom(that); + } + + @Override + public void codechicken$multipart$scalatraits$TIInventoryTile$$super$bindPart(TMultiPart part) { + super.bindPart(part); + } + + @Override + public void codechicken$multipart$scalatraits$TIInventoryTile$$super$partRemoved(TMultiPart part, int position) { + super.partRemoved(part, position); + } + + @Override + public void codechicken$multipart$scalatraits$TIInventoryTile$$super$clearParts() { + super.clearParts(); + } + + @Override + public void copyFrom(TileMultipart that) { + codechicken$multipart$scalatraits$TIInventoryTile$$super$copyFrom(that); + if (that instanceof TIInventoryTile) { + invList = ((TIInventoryTile) that).invList(); + rebuildSlotMap(); + } + } + + @Override + public void bindPart(TMultiPart part) { + codechicken$multipart$scalatraits$TIInventoryTile$$super$bindPart(part); + if (part instanceof IInventory) { + invList.add((IInventory) part); + rebuildSlotMap(); + } + } + + @Override + public void partRemoved(TMultiPart part, int position) { + codechicken$multipart$scalatraits$TIInventoryTile$$super$partRemoved(part, position); + if (part instanceof IInventory) { + invList.remove(part); + rebuildSlotMap(); + } + } + + @Override + @SuppressWarnings("unchecked") + public void clearParts() { + codechicken$multipart$scalatraits$TIInventoryTile$$super$clearParts(); + invList.clear(); + slotMap = (Tuple2[]) new Tuple2[0]; + } + + @Override + @SuppressWarnings("unchecked") + public void rebuildSlotMap() { + int totalSlots = 0; + for (IInventory inventory : invList) { + totalSlots += inventory.getSizeInventory(); + } + + Tuple2[] rebuilt = (Tuple2[]) new Tuple2[totalSlots]; + int index = 0; + for (IInventory inventory : invList) { + for (int slot = 0; slot < inventory.getSizeInventory(); slot++) { + rebuilt[index++] = new Tuple2(inventory, Integer.valueOf(slot)); + } + } + slotMap = rebuilt; + } + + @Override + public int getSizeInventory() { + return slotMap.length; + } + + @Override + public ItemStack getStackInSlot(int slot) { + Tuple2 entry = slotMap[slot]; + return entry._1().getStackInSlot(((Number) entry._2()).intValue()); + } + + @Override + public ItemStack decrStackSize(int slot, int amount) { + Tuple2 entry = slotMap[slot]; + return entry._1().decrStackSize(((Number) entry._2()).intValue(), amount); + } + + @Override + public ItemStack getStackInSlotOnClosing(int slot) { + Tuple2 entry = slotMap[slot]; + return entry._1().getStackInSlotOnClosing(((Number) entry._2()).intValue()); + } + + @Override + public void setInventorySlotContents(int slot, ItemStack stack) { + Tuple2 entry = slotMap[slot]; + entry._1().setInventorySlotContents(((Number) entry._2()).intValue(), stack); + } + + @Override + public String getInventoryName() { + return "Multipart Inventory"; + } + + @Override + public boolean hasCustomInventoryName() { + return false; + } + + @Override + public int getInventoryStackLimit() { + return 64; + } + + @Override + public boolean isUseableByPlayer(EntityPlayer player) { + return true; + } + + @Override + public void openInventory() {} + + @Override + public void closeInventory() {} + + @Override + public boolean isItemValidForSlot(int slot, ItemStack stack) { + Tuple2 entry = slotMap[slot]; + return entry._1().isItemValidForSlot(((Number) entry._2()).intValue(), stack); + } + + @Override + public int[] getAccessibleSlotsFromSide(int side) { + List accessibleSlots = new ArrayList<>(); + int base = 0; + for (IInventory inventory : invList) { + if (inventory instanceof ISidedInventory) { + int[] localSlots = ((ISidedInventory) inventory).getAccessibleSlotsFromSide(side); + for (int localSlot : localSlots) { + accessibleSlots.add(Integer.valueOf(localSlot + base)); + } + } + base += inventory.getSizeInventory(); + } + + return JInventoryTileAccess.toIntArray(accessibleSlots); + } + + @Override + public boolean canInsertItem(int slot, ItemStack stack, int side) { + Tuple2 entry = slotMap[slot]; + IInventory inventory = entry._1(); + return !(inventory instanceof ISidedInventory) + || ((ISidedInventory) inventory).canInsertItem(((Number) entry._2()).intValue(), stack, side); + } + + @Override + public boolean canExtractItem(int slot, ItemStack stack, int side) { + Tuple2 entry = slotMap[slot]; + IInventory inventory = entry._1(); + return !(inventory instanceof ISidedInventory) + || ((ISidedInventory) inventory).canExtractItem(((Number) entry._2()).intValue(), stack, side); + } +} + +/** Keeps primitive-array allocation outside the generated trait transformer. */ +final class JInventoryTileAccess { + + private JInventoryTileAccess() {} + + static int[] toIntArray(List values) { + int[] result = new int[values.size()]; + for (int index = 0; index < result.length; index++) { + result[index] = values.get(index).intValue(); + } + return result; + } +} diff --git a/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.java b/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.java new file mode 100644 index 0000000..cac37a7 --- /dev/null +++ b/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.java @@ -0,0 +1,87 @@ +package codechicken.multipart.scalatraits; + +import java.util.LinkedList; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.ISidedInventory; +import net.minecraft.item.ItemStack; + +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import scala.Tuple2; + +/** Consumer-facing inventory contract implemented by the generated multipart tile. */ +public interface TIInventoryTile extends ISidedInventory { + + void codechicken$multipart$scalatraits$TIInventoryTile$$super$copyFrom(TileMultipart that); + + void codechicken$multipart$scalatraits$TIInventoryTile$$super$bindPart(TMultiPart part); + + void codechicken$multipart$scalatraits$TIInventoryTile$$super$partRemoved(TMultiPart part, int position); + + void codechicken$multipart$scalatraits$TIInventoryTile$$super$clearParts(); + + LinkedList invList(); + + void invList_$eq(LinkedList inventories); + + Tuple2[] slotMap(); + + void slotMap_$eq(Tuple2[] slots); + + void copyFrom(TileMultipart that); + + void bindPart(TMultiPart part); + + void partRemoved(TMultiPart part, int position); + + void clearParts(); + + void rebuildSlotMap(); + + @Override + int getSizeInventory(); + + @Override + ItemStack getStackInSlot(int slot); + + @Override + ItemStack decrStackSize(int slot, int amount); + + @Override + ItemStack getStackInSlotOnClosing(int slot); + + @Override + void setInventorySlotContents(int slot, ItemStack stack); + + @Override + String getInventoryName(); + + @Override + boolean hasCustomInventoryName(); + + @Override + int getInventoryStackLimit(); + + @Override + boolean isUseableByPlayer(EntityPlayer player); + + @Override + void openInventory(); + + @Override + void closeInventory(); + + @Override + boolean isItemValidForSlot(int slot, ItemStack stack); + + @Override + int[] getAccessibleSlotsFromSide(int side); + + @Override + boolean canInsertItem(int slot, ItemStack stack, int side); + + @Override + boolean canExtractItem(int slot, ItemStack stack, int side); +} diff --git a/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.scala b/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.scala deleted file mode 100644 index 995f5e4..0000000 --- a/src/main/scala/codechicken/multipart/scalatraits/TIInventoryTile.scala +++ /dev/null @@ -1,126 +0,0 @@ -package codechicken.multipart.scalatraits - -import net.minecraft.inventory.{IInventory, ISidedInventory} -import codechicken.multipart.{TMultiPart, TileMultipart} -import java.util.LinkedList -import net.minecraft.item.ItemStack -import net.minecraft.entity.player.EntityPlayer -import scala.collection.JavaConversions._ -import scala.collection.mutable.ArrayBuffer - -trait TIInventoryTile extends TileMultipart with ISidedInventory { - var invList = new LinkedList[IInventory]() - var slotMap = Array[(IInventory, Int)]() - - override def copyFrom(that: TileMultipart) { - super.copyFrom(that) - if (that.isInstanceOf[TIInventoryTile]) { - invList = that.asInstanceOf[TIInventoryTile].invList - rebuildSlotMap() - } - } - - override def bindPart(part: TMultiPart) { - super.bindPart(part) - if (part.isInstanceOf[IInventory]) { - invList += part.asInstanceOf[IInventory] - rebuildSlotMap() - } - } - - override def partRemoved(part: TMultiPart, p: Int) { - super.partRemoved(part, p) - if (part.isInstanceOf[IInventory]) { - invList -= part.asInstanceOf[IInventory] - rebuildSlotMap() - } - } - - override def clearParts() { - super.clearParts() - invList.clear() - slotMap = Array() - } - - def rebuildSlotMap() { - slotMap = Array.ofDim(invList.map(_.getSizeInventory).sum) - var i = 0 - for (inv <- invList; s <- 0 until inv.getSizeInventory) { - slotMap(i) = (inv, s) - i += 1 - } - } - - def getSizeInventory: Int = slotMap.length - - def getStackInSlot(i: Int) = { - val (inv, slot) = slotMap(i) - inv.getStackInSlot(slot) - } - - def decrStackSize(i: Int, j: Int) = { - val (inv, slot) = slotMap(i) - inv.decrStackSize(slot, j) - } - - def getStackInSlotOnClosing(i: Int) = { - val (inv, slot) = slotMap(i) - inv.getStackInSlotOnClosing(slot) - } - - def setInventorySlotContents(i: Int, itemstack: ItemStack) = { - val (inv, slot) = slotMap(i) - inv.setInventorySlotContents(slot, itemstack) - } - - def getInventoryName = "Multipart Inventory" - - def hasCustomInventoryName = false - - def getInventoryStackLimit = 64 - - def isUseableByPlayer(entityplayer: EntityPlayer) = true - - def openInventory() {} - - def closeInventory() {} - - def isItemValidForSlot(i: Int, itemstack: ItemStack) = { - val (inv, slot) = slotMap(i) - inv.isItemValidForSlot(slot, itemstack) - } - - def getAccessibleSlotsFromSide(side: Int) = { - val buf = new ArrayBuffer[Int]() - var base = 0 - for (inv <- invList) { - inv match { - case isided: ISidedInventory => - buf ++= isided.getAccessibleSlotsFromSide(side).map(_ + base) - case _ => - } - base += inv.getSizeInventory - } - buf.toArray - } - - def canInsertItem(i: Int, itemstack: ItemStack, j: Int) = { - val (inv, slot) = slotMap(i) - inv match { - case isided: ISidedInventory => isided.canInsertItem(slot, itemstack, j) - case _ => true - } - } - - def canExtractItem(i: Int, itemstack: ItemStack, j: Int) = { - val (inv, slot) = slotMap(i) - inv match { - case isided: ISidedInventory => isided.canExtractItem(slot, itemstack, j) - case _ => true - } - } -} - -/** To handle obfuscation issues, this is registered as a java trait. - */ -class JInventoryTile extends TileMultipart with TIInventoryTile {}