From e39397e2d842712586d1dd2d34e541cb3614eac9 Mon Sep 17 00:00:00 2001 From: MuXiu1997 Date: Wed, 27 Apr 2022 18:36:45 +0800 Subject: [PATCH] FluidContainerTransfer --- .../cil/oc/server/component/Transposer.scala | 2 +- .../traits/FluidContainerTransfer.scala | 197 ++++++++++++++++ .../li/cil/oc/util/FluidContainerUtils.scala | 211 ++++++++++++++++++ 3 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/li/cil/oc/server/component/traits/FluidContainerTransfer.scala create mode 100644 src/main/scala/li/cil/oc/util/FluidContainerUtils.scala diff --git a/src/main/scala/li/cil/oc/server/component/Transposer.scala b/src/main/scala/li/cil/oc/server/component/Transposer.scala index 52037c398..e2cea4e0d 100644 --- a/src/main/scala/li/cil/oc/server/component/Transposer.scala +++ b/src/main/scala/li/cil/oc/server/component/Transposer.scala @@ -22,7 +22,7 @@ import scala.language.existentials object Transposer { - abstract class Common extends prefab.ManagedEnvironment with traits.WorldInventoryAnalytics with traits.WorldTankAnalytics with traits.WorldFluidContainerAnalytics with traits.InventoryTransfer with DeviceInfo { + abstract class Common extends prefab.ManagedEnvironment with traits.WorldInventoryAnalytics with traits.WorldTankAnalytics with traits.WorldFluidContainerAnalytics with traits.InventoryTransfer with traits.FluidContainerTransfer with DeviceInfo { override val node = api.Network.newNode(this, Visibility.Network). withComponent("transposer"). withConnector(). diff --git a/src/main/scala/li/cil/oc/server/component/traits/FluidContainerTransfer.scala b/src/main/scala/li/cil/oc/server/component/traits/FluidContainerTransfer.scala new file mode 100644 index 000000000..a87d1bd01 --- /dev/null +++ b/src/main/scala/li/cil/oc/server/component/traits/FluidContainerTransfer.scala @@ -0,0 +1,197 @@ +package li.cil.oc.server.component.traits + +import li.cil.oc.api.machine.{Arguments, Callback, Context} +import li.cil.oc.server.component.{result, traits} +import li.cil.oc.util.ExtendedArguments.extendedArguments +import li.cil.oc.util.{FluidContainerUtils, FluidUtils, InventoryUtils} +import net.minecraft.inventory.IInventory +import net.minecraft.item.ItemStack +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.fluids.IFluidHandler + +trait FluidContainerTransfer extends traits.WorldAware with traits.SideRestricted { + // Return None on success, else Some("failure reason") + def onTransferContents(): Option[String] + + @Callback(doc = """function(tankSide:number, inventorySide:number, inventorySlot:number [, count:number [, sourceTank:number [, outputSide:number[, outputSlot:number]]]]):boolean, number -- Transfer some fluid from the tank to the container. Returns operation result and filled amount""") + def transferFluidFromTankToContainer(context: Context, args: Arguments): Array[AnyRef] = { + val tankSide = checkSideForAction(args, 0) + val tankPos = position.offset(tankSide) + val inventorySide = checkSideForAction(args, 1) + val checkInventorySlot: IInventory => Int = inventory => args.checkSlot(inventory, 2) + val count = args.optFluidCount(3) + val sourceTank = args.optInteger(4, -1) + val outputSide = if (args.count > 5) checkSideForAction(args, 5) else inventorySide + val checkOutputSlot: IInventory => Option[Int] = inventory => if (args.count > 6) Some(args.checkSlot(inventory, 6)) else None + + onTransferContents() match { + case Some(reason) => + result(Unit, reason) + case _ => + withInventory(inventorySide, inventory => { + withInventory(outputSide, output => { + withReplayableMove( + FluidUtils.fluidHandlerAt(tankPos), + FluidContainerUtils.fluidHandlerIn(inventory, checkInventorySlot(inventory)), + (replayableTank, replayableContainer) => FluidUtils.transferBetweenFluidHandlers(replayableTank, tankSide, replayableContainer, ForgeDirection.UNKNOWN, count, sourceTank), + (tank, container, tankReplay, containerReplay) => { + containerReplay(container) + val result = FluidContainerUtils.getContainerResult(container) + if (syncResult(inventory, inventorySide, checkInventorySlot(inventory), output, outputSide, checkOutputSlot(output), result)) { + tankReplay(tank) + true + } else { + false + } + } + ) + }) + }) + } + } + + @Callback(doc = """function(inventorySide:number, inventorySlot:number, tankSide:number [, count:number [, outputSide:number[, outputSlot:number]]]):boolean, number -- Transfer some fluid from the container to the tank. Returns operation result and filled amount""") + def transferFluidFromContainerToTank(context: Context, args: Arguments): Array[AnyRef] = { + val inventorySide = checkSideForAction(args, 0) + val checkInventorySlot: IInventory => Int = inventory => args.checkSlot(inventory, 1) + val tankSide = checkSideForAction(args, 2) + val tankPos = position.offset(tankSide) + val count = args.optFluidCount(3) + val outputSide = if (args.count > 4) checkSideForAction(args, 4) else inventorySide + val checkOutputSlot: IInventory => Option[Int] = inventory => if (args.count > 5) Some(args.checkSlot(inventory, 5)) else None + + onTransferContents() match { + case Some(reason) => + result(Unit, reason) + case _ => + withInventory(inventorySide, inventory => { + withInventory(outputSide, output => { + withReplayableMove( + FluidContainerUtils.fluidHandlerIn(inventory, checkInventorySlot(inventory)), + FluidUtils.fluidHandlerAt(tankPos), + (replayableContainer, replayableTank) => FluidUtils.transferBetweenFluidHandlers(replayableContainer, ForgeDirection.UNKNOWN, replayableTank, tankSide, count), + (container, tank, containerReplay, tankReplay) => { + containerReplay(container) + val result = FluidContainerUtils.getContainerResult(container) + if (syncResult(inventory, inventorySide, checkInventorySlot(inventory), output, outputSide, checkOutputSlot(output), result)) { + tankReplay(tank) + true + } else { + false + } + } + ) + }) + }) + } + } + + @Callback(doc = """function(sourceSide:number, sourceSlot:number, sinkSide:number, sinkSlot:number[, count:number [, sourceOutputSide:number[, sinkOutputSide:number[, sourceOutputSlot:number[, sinkOutputSlot:number]]]]]):boolean, number -- Transfer some fluid from a container to another container. Returns operation result and filled amount""") + def transferFluidBetweenContainers(context: Context, args: Arguments): Array[AnyRef] = { + val sourceSide = checkSideForAction(args, 0) + val checkSourceSlot: IInventory => Int = inventory => args.checkSlot(inventory, 1) + val sinkSide = checkSideForAction(args, 2) + val checkSinkSlot: IInventory => Int = inventory => args.checkSlot(inventory, 3) + val count = args.optFluidCount(4) + val sourceOutputSide = if (args.count > 5) checkSideForAction(args, 5) else sourceSide + val sinkOutputSide = if (args.count > 6) checkSideForAction(args, 6) else sinkSide + val checkSourceOutputSlot: IInventory => Option[Int] = inventory => if (args.count > 7) Some(args.checkSlot(inventory, 7)) else None + val checkSinkOutputSlot: IInventory => Option[Int] = inventory => if (args.count > 8) Some(args.checkSlot(inventory, 8)) else None + + onTransferContents() match { + case Some(reason) => + result(Unit, reason) + case _ => + withInventory(sourceSide, source => { + withInventory(sinkSide, sink => { + withInventory(sourceOutputSide, sourceOutput => { + withInventory(sinkOutputSide, sinkOutput => { + withMove( + FluidContainerUtils.fluidHandlerIn(source, checkSourceSlot(source)), + FluidContainerUtils.fluidHandlerIn(sink, checkSinkSlot(sink)), + (sourceContainer, sinkContainer) => FluidUtils.transferBetweenFluidHandlers(sourceContainer, ForgeDirection.UNKNOWN, sinkContainer, ForgeDirection.UNKNOWN, count), + (sourceContainer, sinkContainer) => { + val sourceResult = FluidContainerUtils.getContainerResult(sourceContainer) + val sinkResult = FluidContainerUtils.getContainerResult(sinkContainer) + if ( + syncResult(source, sourceSide, checkSourceSlot(source), sourceOutput, sourceOutputSide, checkSourceOutputSlot(sourceOutput), sourceResult, simulate = true) + && syncResult(sink, sinkSide, checkSinkSlot(sink), sinkOutput, sinkOutputSide, checkSinkOutputSlot(sinkOutput), sinkResult, simulate = true) + ) { + syncResult(source, sourceSide, checkSourceSlot(source), sourceOutput, sourceOutputSide, checkSourceOutputSlot(sourceOutput), sourceResult) + syncResult(sink, sinkSide, checkSinkSlot(sink), sinkOutput, sinkOutputSide, checkSinkOutputSlot(sinkOutput), sinkResult) + true + } else { + false + } + } + ) + }) + }) + }) + }) + } + } + + private def syncResult(inventory: IInventory, inventorySide: ForgeDirection, inventorySlot: Int, output: IInventory, outputSide: ForgeDirection, outputSlot: Option[Int], result: ItemStack, simulate: Boolean = false) = { + val stack = if (simulate) result.copy() else result + def decrStackSizeIfInserted(inserted: Boolean): Boolean = { + if (inserted && !simulate) { + inventory.decrStackSize(inventorySlot, 1) + } + inserted + } + + def replaceOr(f: () => Boolean) = { + if (inventorySide == outputSide && outputSlot.getOrElse(inventorySlot) == inventorySlot && inventory.getStackInSlot(inventorySlot).stackSize == 1) { + if (!simulate) inventory.setInventorySlotContents(inventorySlot, stack) + true + } + else f() + } + + outputSlot match { + case None => + replaceOr( + () => decrStackSizeIfInserted(InventoryUtils.insertIntoInventory(stack, output, Some(outputSide.getOpposite), simulate = simulate)) + ) + case Some(slot) => + replaceOr( + () => decrStackSizeIfInserted(InventoryUtils.insertIntoInventorySlot(stack, output, Some(outputSide.getOpposite), slot, simulate = simulate)) + ) + } + } + + private def withReplayableMove(handlerA: Option[IFluidHandler], handlerB: Option[IFluidHandler], moveFunc: (IFluidHandler, IFluidHandler) => Int, cb: (IFluidHandler, IFluidHandler, IFluidHandler => Unit, IFluidHandler => Unit) => Boolean) = { + val moved = handlerA.fold(0)(a => + handlerB.fold(0)(b => { + val replayableA = FluidContainerUtils.replayableFluidHandler(a) + val replayableB = FluidContainerUtils.replayableFluidHandler(b) + moveFunc(replayableA, replayableB) match { + case 0 => 0 + case moved => + if (cb(a, b, h => FluidContainerUtils.replay(replayableA, h), h => FluidContainerUtils.replay(replayableB, h))) moved else 0 + } + }) + ) + result(moved > 0, moved) + } + + private def withMove(handlerA: Option[IFluidHandler], handlerB: Option[IFluidHandler], moveFunc: (IFluidHandler, IFluidHandler) => Int, cb: (IFluidHandler, IFluidHandler) => Boolean) = { + val moved = handlerA.fold(0)(a => + handlerB.fold(0)(b => { + moveFunc(a, b) match { + case 0 => 0 + case moved => + if (cb(a, b)) moved else 0 + } + }) + ) + result(moved > 0, moved) + } + + private def withInventory(side: ForgeDirection, f: IInventory => Array[AnyRef]) = + InventoryUtils.inventoryAt(position.offset(side)) match { + case Some(inventory) if inventory.isUseableByPlayer(fakePlayer) && mayInteract(position.offset(side), side.getOpposite) => f(inventory) + case _ => result(Unit, "no inventory") + } +} diff --git a/src/main/scala/li/cil/oc/util/FluidContainerUtils.scala b/src/main/scala/li/cil/oc/util/FluidContainerUtils.scala new file mode 100644 index 000000000..3de9b1622 --- /dev/null +++ b/src/main/scala/li/cil/oc/util/FluidContainerUtils.scala @@ -0,0 +1,211 @@ +package li.cil.oc.util + +import net.minecraft.inventory.IInventory +import net.minecraft.item.ItemStack +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.fluids._ + +object FluidContainerUtils { + + /** + * Retrieves an actual fluid handler implementation for a fluid container item. + */ + def fluidHandlerIn(inventory: IInventory, slot: Int): Option[IFluidHandler] = { + inventory.getStackInSlot(slot) match { + case stack: ItemStack if stack != null => + val oneSizedStack = stack.copy() + oneSizedStack.stackSize = 1 + oneSizedStack match { + case _ if FluidContainerRegistry.isFilledContainer(oneSizedStack) => Option(new FilledContainerWrapper(oneSizedStack)) + case _ if FluidContainerRegistry.isEmptyContainer(oneSizedStack) => Option(new EmptyContainerWrapper(oneSizedStack)) + case _ => + stack.getItem match { + case _: IFluidContainerItem => Option(new FluidContainerItemWrapper(oneSizedStack)) + case _ => None + } + } + case _ => None + } + } + + /** + * A fluid handler implementation that records fluid operations and can replay them. + */ + def replayableFluidHandler(handler: IFluidHandler, simulate: Boolean = true): IFluidHandler = { + new ReplayableFluidHandler(handler, simulate) + } + + def getContainerResult(container: IFluidHandler): ItemStack = { + container match { + case w: ContainerWrapper => w.getResult + case _ => null + } + } + + + def replay(replayable: IFluidHandler, handler: IFluidHandler): Unit = { + replayable match { + case r: ReplayableFluidHandler => r.replay(handler) + case _ => () + } + } + + private trait ContainerWrapper extends IFluidHandler { + def getResult: ItemStack + } + + private class FilledContainerWrapper(val stack: ItemStack) extends ContainerWrapper { + private val fluid = FluidContainerRegistry.getFluidForFilledItem(stack) + private val capacity = FluidContainerRegistry.getContainerCapacity(stack) + private var result = null: ItemStack + private var dirty = false + + override def fill(from: ForgeDirection, resource: FluidStack, doFill: Boolean): Int = 0 + + override def drain(from: ForgeDirection, resource: FluidStack, doDrain: Boolean): FluidStack = { + if (dirty) return null + if (resource == null || !resource.isFluidEqual(fluid)) null + else drain(from, resource.amount, doDrain) + } + + override def drain(from: ForgeDirection, maxDrain: Int, doDrain: Boolean): FluidStack = { + if (dirty) return null + if (maxDrain < capacity) null + else { + if (doDrain) { + result = FluidContainerRegistry.drainFluidContainer(stack) + dirty = true + } + fluid + } + } + + override def canFill(from: ForgeDirection, fluid: Fluid): Boolean = false + + override def canDrain(from: ForgeDirection, fluid: Fluid): Boolean = { + if (dirty) return false + fluid != null && this.fluid.getFluid == fluid + } + + override def getTankInfo(from: ForgeDirection): Array[FluidTankInfo] = { + Array(new FluidTankInfo(fluid, capacity)) + } + + override def getResult: ItemStack = result + } + + private class EmptyContainerWrapper(val stack: ItemStack) extends ContainerWrapper { + private var result = null: ItemStack + private var dirty = false + + override def fill(from: ForgeDirection, resource: FluidStack, doFill: Boolean): Int = { + if (dirty) return 0 + val filledContainer = FluidContainerRegistry.fillFluidContainer(resource, stack) + if (filledContainer == null) 0 + else { + if (doFill) { + result = filledContainer + dirty = true + } + FluidContainerRegistry.getFluidForFilledItem(filledContainer).amount + } + } + + override def drain(from: ForgeDirection, resource: FluidStack, doDrain: Boolean): FluidStack = null + + override def drain(from: ForgeDirection, maxDrain: Int, doDrain: Boolean): FluidStack = null + + override def canFill(from: ForgeDirection, fluid: Fluid): Boolean = { + if (dirty) return false + FluidContainerRegistry.fillFluidContainer(new FluidStack(fluid, Int.MaxValue), stack) != null + } + + override def canDrain(from: ForgeDirection, fluid: Fluid): Boolean = false + + override def getTankInfo(from: ForgeDirection): Array[FluidTankInfo] = { + Array(new FluidTankInfo(null, Int.MaxValue)) + } + + override def getResult: ItemStack = result + } + + private class FluidContainerItemWrapper(val stack: ItemStack) extends ContainerWrapper { + private val fluidContainerItem = stack.getItem.asInstanceOf[IFluidContainerItem] + + override def fill(from: ForgeDirection, resource: FluidStack, doFill: Boolean): Int = { + fluidContainerItem.fill(stack, resource, doFill) + } + + override def drain(from: ForgeDirection, resource: FluidStack, doDrain: Boolean): FluidStack = { + if (fluidContainerItem.getFluid(stack) == null || !fluidContainerItem.getFluid(stack).isFluidEqual(resource)) null + else fluidContainerItem.drain(stack, resource.amount, doDrain) + } + + override def drain(from: ForgeDirection, maxDrain: Int, doDrain: Boolean): FluidStack = { + fluidContainerItem.drain(stack, maxDrain, doDrain) + } + + override def canFill(from: ForgeDirection, fluid: Fluid): Boolean = { + if (fluidContainerItem.getFluid(stack) == null) true + else if (fluidContainerItem.getFluid(stack).getFluid == fluid && fluidContainerItem.getFluid(stack).amount < fluidContainerItem.getCapacity(stack)) true + else false + } + + override def canDrain(from: ForgeDirection, fluid: Fluid): Boolean = { + if (fluidContainerItem.getFluid(stack) == null) false + else if (fluidContainerItem.getFluid(stack).getFluid == fluid && 0 < fluidContainerItem.getFluid(stack).amount) true + else false + } + + override def getTankInfo(from: ForgeDirection): Array[FluidTankInfo] = { + Array(new FluidTankInfo(fluidContainerItem.getFluid(stack), fluidContainerItem.getCapacity(stack))) + } + + override def getResult: ItemStack = stack + } + + private class ReplayableFluidHandler(val handler: IFluidHandler, val simulate: Boolean = true) extends IFluidHandler { + var actions: List[IFluidHandler => Unit] = List.empty + + def replay(handler: IFluidHandler): Unit = { + actions.foreach(action => action(handler)) + } + + override def fill(from: ForgeDirection, resource: FluidStack, doFill: Boolean): Int = { + actions.+:=((h: IFluidHandler) => { + h.fill(from, resource, doFill) + () + }) + handler.fill(from, resource, doFill && !simulate) + + } + + override def drain(from: ForgeDirection, resource: FluidStack, doDrain: Boolean): FluidStack = { + actions.+:=((h: IFluidHandler) => { + h.drain(from, resource, doDrain) + () + }) + handler.drain(from, resource, doDrain && !simulate) + } + + override def drain(from: ForgeDirection, maxDrain: Int, doDrain: Boolean): FluidStack = { + actions.+:=((h: IFluidHandler) => { + h.drain(from, maxDrain, doDrain) + () + }) + handler.drain(from, maxDrain, doDrain && !simulate) + } + + override def canFill(from: ForgeDirection, fluid: Fluid): Boolean = { + handler.canFill(from, fluid) + } + + override def canDrain(from: ForgeDirection, fluid: Fluid): Boolean = { + handler.canDrain(from, fluid) + } + + override def getTankInfo(from: ForgeDirection): Array[FluidTankInfo] = { + handler.getTankInfo(from) + } + } +} \ No newline at end of file