From a9006280ef0635d64893eea35a748a6c3b0969c7 Mon Sep 17 00:00:00 2001 From: Armisael <193531144+Armisael5@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:12:36 -0400 Subject: [PATCH 1/3] Add AE2 Cell Workbench API --- .../li/cil/oc/integration/appeng/AEUtil.scala | 13 ++ .../appeng/DriverCellWorkbench.scala | 127 ++++++++++++++++++ .../cil/oc/integration/appeng/ModAppEng.scala | 2 + 3 files changed, 142 insertions(+) create mode 100644 src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala diff --git a/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala b/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala index bc87b94333..5c7b4c1591 100644 --- a/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala +++ b/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala @@ -140,6 +140,19 @@ object AEUtil { AEApi.instance.definitions.parts.storageBus.isSameAs(stack) } + def isCellWorkbench(stack: ItemStack): Boolean = stack != null && AEApi.instance != null && { + if (useNewItemDefinitionAPI) isCellWorkbenchNew(stack) + else isCellWorkbenchOld(stack) + } + + private def isCellWorkbenchNew(stack: ItemStack): Boolean = + AEApi.instance.definitions.blocks.cellWorkbench.isSameAs(stack) + + private def isCellWorkbenchOld(stack: ItemStack): Boolean = + AEApi.instance.blocks != null && + AEApi.instance.blocks.blockCellWorkbench != null && + AEApi.instance.blocks.blockCellWorkbench.sameAsStack(stack) + def isRobot(stack: ItemStack): Boolean = api.Items.get(stack) == api.Items.get("robot") diff --git a/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala new file mode 100644 index 0000000000..6ba2c93e71 --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala @@ -0,0 +1,127 @@ +package li.cil.oc.integration.appeng + +import appeng.api.storage.StorageName +import appeng.helpers.ICellRestriction.CellRestrictionData +import appeng.tile.misc.TileCellWorkbench +import li.cil.oc.api.driver.{EnvironmentProvider, NamedBlock} +import li.cil.oc.api.machine.{Arguments, Callback, Context} +import li.cil.oc.api.network.ManagedEnvironment +import li.cil.oc.api.prefab.DriverSidedTileEntity +import li.cil.oc.integration.ManagedTileEntityEnvironment +import li.cil.oc.util.ResultWrapper.result +import net.minecraft.item.ItemStack +import net.minecraft.world.World +import net.minecraftforge.common.util.ForgeDirection + +object DriverCellWorkbench extends DriverSidedTileEntity { + def getTileEntityClass = classOf[TileCellWorkbench] + + def createEnvironment(world: World, x: Int, y: Int, z: Int, side: ForgeDirection): ManagedEnvironment = + new Environment(world.getTileEntity(x, y, z).asInstanceOf[TileCellWorkbench]) + + final class Environment(val tile: TileCellWorkbench) + extends ManagedTileEntityEnvironment[TileCellWorkbench](tile, "me_cellworkbench") with NamedBlock { + override def preferredName = "me_cellworkbench" + + override def priority = 5 + + private def config = tile.getAEInventoryByName(StorageName.CONFIG) + + private def checkSlot(slot: Int): Int = { + val c = config + if (c == null || slot < 0 || slot >= c.getSizeInventory) { + throw new IllegalArgumentException("invalid slot") + } + slot + } + + // tile.getStackType() is cached and only updates on cell insert/remove/swap + // Server restart resets to null even with cell inserted + private def liveStackTypeId: Option[String] = { + val cell = tile.getCell + if (cell == null) None + else { + val stackType = cell.getStackType + if (stackType == null) None else Some(stackType.getId) + } + } + + @Callback(doc = "function():boolean -- Returns whether a storage cell is currently inserted.") + def hasCell(context: Context, args: Arguments): Array[AnyRef] = result(tile.getCell != null) + + @Callback(doc = "function():string -- Returns the inserted cell's type (\"item\", \"fluid\", \"essentia\", ...). Returns nil if no cell is inserted.") + def getCellType(context: Context, args: Arguments): Array[AnyRef] = result(liveStackTypeId.orNull) + + @Callback(doc = "function():table -- Returns every partition slot as a table keyed by slot number (1-based). Empty slots are omitted.") + def getPartition(context: Context, args: Arguments): Array[AnyRef] = { + val c = config + val out = new java.util.HashMap[AnyRef, AnyRef]() + if (c != null) { + for (i <- 0 until c.getSizeInventory) { + val stack = c.getAEStackInSlot(i) + if (stack != null) { + val entry = new java.util.HashMap[AnyRef, AnyRef]() + AEStackFactory.convert(stack, entry) + out.put(Int.box(i + 1), entry) + } + } + } + result(out) + } + + @Callback(doc = "function(slot:number[, item:string OR table]):boolean -- Sets the partition in the given slot (1-based). Accepts a name, or a table for more detail (e.g. item damage). Omit the item to clear the slot.") + def setPartition(context: Context, args: Arguments): Array[AnyRef] = { + val slot = checkSlot(args.checkInteger(0) - 1) + val stack = + if (args.count > 1 && (args.isTable(1) || args.isString(1))) { + val stackTypeId = liveStackTypeId.getOrElse(throw new IllegalArgumentException("no cell inserted")) + val descriptor = + if (args.isTable(1)) args.checkTable(1) + else java.util.Collections.singletonMap("name", args.checkString(1)) + AEStackFactory.parse(stackTypeId, descriptor) + } + else null + config.putAEStackInSlot(slot, stack) + tile.saveAEStackInv() + result(true) + } + + @Callback(doc = "function():boolean -- Clears every partition slot on the inserted cell.") + def clearPartitions(context: Context, args: Arguments): Array[AnyRef] = { + val c = config + if (c != null) { + for (i <- 0 until c.getSizeInventory) c.putAEStackInSlot(i, null) + tile.saveAEStackInv() + } + result(true) + } + + @Callback(doc = "function():number, number -- Returns the cell's restriction as (types, amount). (0, 0) means unrestricted.") + def getRestriction(context: Context, args: Arguments): Array[AnyRef] = { + val r = tile.getCellRestrictionData(null) + if (r == null) result(0, 0) else result(r.restrictionTypes, r.restrictionAmount) + } + + @Callback(doc = "function(types:number, amount:number):boolean -- Sets the cell's restriction. (0, 0) means unrestricted.") + def setRestriction(context: Context, args: Arguments): Array[AnyRef] = { + val types = args.checkInteger(0) + val amount = args.checkLong(1) + tile.setCellRestriction(null, new CellRestrictionData(types.toByte, amount)) + result(true) + } + + @Callback(doc = "function():string -- Returns the inserted cell's ore filter string. Returns empty string (not nil) if unset.") + def getOreFilter(context: Context, args: Arguments): Array[AnyRef] = result(tile.getFilter) + + @Callback(doc = "function(filter:string):boolean -- Sets the inserted cell's ore filter string.") + def setOreFilter(context: Context, args: Arguments): Array[AnyRef] = { + tile.setFilter(args.checkString(0)) + result(true) + } + } + + object Provider extends EnvironmentProvider { + override def getEnvironment(stack: ItemStack): Class[_] = + if (AEUtil.isCellWorkbench(stack)) classOf[Environment] else null + } +} diff --git a/src/main/scala/li/cil/oc/integration/appeng/ModAppEng.scala b/src/main/scala/li/cil/oc/integration/appeng/ModAppEng.scala index bbcebbc6d2..76eee22180 100644 --- a/src/main/scala/li/cil/oc/integration/appeng/ModAppEng.scala +++ b/src/main/scala/li/cil/oc/integration/appeng/ModAppEng.scala @@ -32,6 +32,7 @@ object ModAppEng extends ModProxy { AEApi.instance.registries.movable.whiteListTileEntity(classOf[Print]) Driver.add(DriverController) + Driver.add(DriverCellWorkbench) Driver.add(DriverExportBus) Driver.add(DriverImportBus) Driver.add(DriverStorageBus) @@ -46,6 +47,7 @@ object ModAppEng extends ModProxy { Driver.add(ConverterPattern) Driver.add(DriverController.Provider) + Driver.add(DriverCellWorkbench.Provider) Driver.add(DriverExportBus.Provider) Driver.add(DriverImportBus.Provider) Driver.add(DriverStorageBus.Provider) From 1a3c7f229177eb9cc85054e8310bb7701146fcab Mon Sep 17 00:00:00 2001 From: Armisael <193531144+Armisael5@users.noreply.github.com> Date: Tue, 1 Sep 2026 00:47:11 -0400 Subject: [PATCH 2/3] Implementing requested changes and improvements --- .../li/cil/oc/integration/appeng/AEUtil.scala | 13 +--- .../appeng/DriverCellWorkbench.scala | 62 ++++++++++--------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala b/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala index 5c7b4c1591..cf69cd3076 100644 --- a/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala +++ b/src/main/scala/li/cil/oc/integration/appeng/AEUtil.scala @@ -140,18 +140,9 @@ object AEUtil { AEApi.instance.definitions.parts.storageBus.isSameAs(stack) } - def isCellWorkbench(stack: ItemStack): Boolean = stack != null && AEApi.instance != null && { - if (useNewItemDefinitionAPI) isCellWorkbenchNew(stack) - else isCellWorkbenchOld(stack) - } - - private def isCellWorkbenchNew(stack: ItemStack): Boolean = + def isCellWorkbench(stack: ItemStack): Boolean = { AEApi.instance.definitions.blocks.cellWorkbench.isSameAs(stack) - - private def isCellWorkbenchOld(stack: ItemStack): Boolean = - AEApi.instance.blocks != null && - AEApi.instance.blocks.blockCellWorkbench != null && - AEApi.instance.blocks.blockCellWorkbench.sameAsStack(stack) + } def isRobot(stack: ItemStack): Boolean = api.Items.get(stack) == api.Items.get("robot") diff --git a/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala index 6ba2c93e71..051a5bba4b 100644 --- a/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala +++ b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala @@ -1,5 +1,6 @@ package li.cil.oc.integration.appeng +import appeng.api.config.{CopyMode, Settings} import appeng.api.storage.StorageName import appeng.helpers.ICellRestriction.CellRestrictionData import appeng.tile.misc.TileCellWorkbench @@ -28,8 +29,7 @@ object DriverCellWorkbench extends DriverSidedTileEntity { private def config = tile.getAEInventoryByName(StorageName.CONFIG) private def checkSlot(slot: Int): Int = { - val c = config - if (c == null || slot < 0 || slot >= c.getSizeInventory) { + if (slot < 0 || slot >= config.getSizeInventory) { throw new IllegalArgumentException("invalid slot") } slot @@ -52,35 +52,23 @@ object DriverCellWorkbench extends DriverSidedTileEntity { @Callback(doc = "function():string -- Returns the inserted cell's type (\"item\", \"fluid\", \"essentia\", ...). Returns nil if no cell is inserted.") def getCellType(context: Context, args: Arguments): Array[AnyRef] = result(liveStackTypeId.orNull) + @Callback(doc = "function():table -- Returns details about the inserted cell item itself. Returns nil if no cell is inserted. Essentia cells return limited info.") + def getCell(context: Context, args: Arguments): Array[AnyRef] = + result(tile.getInventoryByName("cell").getStackInSlot(0)) + @Callback(doc = "function():table -- Returns every partition slot as a table keyed by slot number (1-based). Empty slots are omitted.") - def getPartition(context: Context, args: Arguments): Array[AnyRef] = { - val c = config - val out = new java.util.HashMap[AnyRef, AnyRef]() - if (c != null) { - for (i <- 0 until c.getSizeInventory) { - val stack = c.getAEStackInSlot(i) - if (stack != null) { - val entry = new java.util.HashMap[AnyRef, AnyRef]() - AEStackFactory.convert(stack, entry) - out.put(Int.box(i + 1), entry) - } - } - } - result(out) - } + def getPartition(context: Context, args: Arguments): Array[AnyRef] = + result(Array.tabulate(config.getSizeInventory)(config.getAEStackInSlot)) - @Callback(doc = "function(slot:number[, item:string OR table]):boolean -- Sets the partition in the given slot (1-based). Accepts a name, or a table for more detail (e.g. item damage). Omit the item to clear the slot.") + @Callback(doc = "function(slot:number[, item:table]):boolean -- Sets the partition in the given slot (1-based). Accepts a table describing the item (e.g. name, damage). Omit the item to clear the slot.") def setPartition(context: Context, args: Arguments): Array[AnyRef] = { val slot = checkSlot(args.checkInteger(0) - 1) val stack = - if (args.count > 1 && (args.isTable(1) || args.isString(1))) { + if (args.count <= 1) null + else { val stackTypeId = liveStackTypeId.getOrElse(throw new IllegalArgumentException("no cell inserted")) - val descriptor = - if (args.isTable(1)) args.checkTable(1) - else java.util.Collections.singletonMap("name", args.checkString(1)) - AEStackFactory.parse(stackTypeId, descriptor) + AEStackFactory.parse(stackTypeId, args.checkTable(1)) } - else null config.putAEStackInSlot(slot, stack) tile.saveAEStackInv() result(true) @@ -88,11 +76,8 @@ object DriverCellWorkbench extends DriverSidedTileEntity { @Callback(doc = "function():boolean -- Clears every partition slot on the inserted cell.") def clearPartitions(context: Context, args: Arguments): Array[AnyRef] = { - val c = config - if (c != null) { - for (i <- 0 until c.getSizeInventory) c.putAEStackInSlot(i, null) - tile.saveAEStackInv() - } + for (i <- 0 until config.getSizeInventory) config.putAEStackInSlot(i, null) + tile.saveAEStackInv() result(true) } @@ -104,6 +89,7 @@ object DriverCellWorkbench extends DriverSidedTileEntity { @Callback(doc = "function(types:number, amount:number):boolean -- Sets the cell's restriction. (0, 0) means unrestricted.") def setRestriction(context: Context, args: Arguments): Array[AnyRef] = { + if (tile.getCell == null) throw new IllegalArgumentException("no cell inserted") val types = args.checkInteger(0) val amount = args.checkLong(1) tile.setCellRestriction(null, new CellRestrictionData(types.toByte, amount)) @@ -115,9 +101,27 @@ object DriverCellWorkbench extends DriverSidedTileEntity { @Callback(doc = "function(filter:string):boolean -- Sets the inserted cell's ore filter string.") def setOreFilter(context: Context, args: Arguments): Array[AnyRef] = { + if (tile.getCell == null) throw new IllegalArgumentException("no cell inserted") tile.setFilter(args.checkString(0)) result(true) } + + @Callback(doc = "function():string -- Returns the workbench's copy mode: \"clear\" or \"keep\" (whether the partition settings are cleared or remains when a cell is removed).") + def getCopyMode(context: Context, args: Arguments): Array[AnyRef] = { + val mode = tile.getConfigManager.getSetting(Settings.COPY_MODE) + result(if (mode == CopyMode.KEEP_ON_REMOVE) "keep" else "clear") + } + + @Callback(doc = "function(mode:string):boolean -- Sets the workbench's copy mode: \"clear\" or \"keep\".") + def setCopyMode(context: Context, args: Arguments): Array[AnyRef] = { + val mode = args.checkString(0) match { + case "clear" => CopyMode.CLEAR_ON_REMOVE + case "keep" => CopyMode.KEEP_ON_REMOVE + case other => throw new IllegalArgumentException(s"invalid mode: $other") + } + tile.getConfigManager.putSetting(Settings.COPY_MODE, mode) + result(true) + } } object Provider extends EnvironmentProvider { From ae135bbbc7565be4aba793ffcde4d4527ab8f5af Mon Sep 17 00:00:00 2001 From: Armisael <193531144+Armisael5@users.noreply.github.com> Date: Tue, 1 Sep 2026 00:53:16 -0400 Subject: [PATCH 3/3] Wording --- .../li/cil/oc/integration/appeng/DriverCellWorkbench.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala index 051a5bba4b..c6cc0b187a 100644 --- a/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala +++ b/src/main/scala/li/cil/oc/integration/appeng/DriverCellWorkbench.scala @@ -106,7 +106,7 @@ object DriverCellWorkbench extends DriverSidedTileEntity { result(true) } - @Callback(doc = "function():string -- Returns the workbench's copy mode: \"clear\" or \"keep\" (whether the partition settings are cleared or remains when a cell is removed).") + @Callback(doc = "function():string -- Returns the workbench's copy mode: \"clear\" or \"keep\" (whether the partitions are cleared or remains when a cell is removed).") def getCopyMode(context: Context, args: Arguments): Array[AnyRef] = { val mode = tile.getConfigManager.getSetting(Settings.COPY_MODE) result(if (mode == CopyMode.KEEP_ON_REMOVE) "keep" else "clear")