blob: 8dfc38876fc2cda71f3f63a95e3eaf82c98422ae [file] [log] [blame] [raw]
package li.cil.oc.server.component
import java.util.UUID
import li.cil.oc.Settings
import li.cil.oc.api.machine._
import li.cil.oc.api.network.EnvironmentHost
import li.cil.oc.api.prefab.AbstractValue
import li.cil.oc.common.EventHandler
import li.cil.oc.util.InventoryUtils
import net.minecraft.entity.Entity
import net.minecraft.entity.IMerchant
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.tileentity.TileEntity
import net.minecraft.village.MerchantRecipe
import net.minecraftforge.common.DimensionManager
import net.minecraftforge.common.util.ForgeDirection
import scala.collection.convert.WrapAsScala._
import scala.ref.WeakReference
class Trade(val info: TradeInfo) extends AbstractValue {
def this() = this(new TradeInfo())
def this(upgrade: UpgradeTrading, merchant: IMerchant, recipeID: Int) =
this(new TradeInfo(upgrade.host, merchant, recipeID))
def maxRange = Settings.get.tradingRange
def isInRange = (info.merchant.get, info.host) match {
case (Some(merchant: Entity), Some(host)) => merchant.getDistanceSq(host.xPosition, host.yPosition, host.zPosition) < maxRange * maxRange
case _ => false
}
// Queue the load because when load is called we can't access the world yet
// and we need to access it to get the Robot's TileEntity / Drone's Entity.
override def load(nbt: NBTTagCompound) = EventHandler.scheduleServer(() => info.load(nbt))
override def save(nbt: NBTTagCompound) = info.save(nbt)
@Callback(doc = "function():table, table -- Returns the items the merchant wants for this trade.")
def getInput(context: Context, arguments: Arguments): Array[AnyRef] =
result(info.recipe.map(_.getItemToBuy.copy()).orNull,
if (info.recipe.exists(_.hasSecondItemToBuy)) info.recipe.map(_.getSecondItemToBuy.copy()).orNull else null)
@Callback(doc = "function():table -- Returns the item the merchant offers for this trade.")
def getOutput(context: Context, arguments: Arguments): Array[AnyRef] =
result(info.recipe.map(_.getItemToSell.copy()).orNull)
@Callback(doc = "function():boolean -- Returns whether the merchant currently wants to trade this.")
def isEnabled(context: Context, arguments: Arguments): Array[AnyRef] =
result(info.merchant.get.exists(merchant => !info.recipe.exists(_.isRecipeDisabled))) // Make sure merchant is neither dead/gone nor the recipe has been disabled.
@Callback(doc = "function():boolean, string -- Returns true when trade succeeds and nil, error when not.")
def trade(context: Context, arguments: Arguments): Array[AnyRef] = {
// Make sure we can access an inventory.
info.inventory match {
case Some(inventory) =>
// Make sure merchant hasn't died, it somehow gone or moved out of range and still wants to trade this.
info.merchant.get match {
case Some(merchant: Entity) if merchant.isEntityAlive && isInRange =>
if (!merchant.isEntityAlive) {
result(false, "trader died")
} else if (!isInRange) {
result(false, "out of range")
} else {
info.recipe match {
case Some(recipe) =>
if (recipe.isRecipeDisabled) {
result(false, "trade is disabled")
} else {
// Now we'll check if we have enough items to perform the trade, caching first
val firstInputStack = recipe.getItemToBuy
val secondInputStack = if (recipe.hasSecondItemToBuy) Option(recipe.getSecondItemToBuy) else None
def containsAccumulativeItemStack(stack: ItemStack) =
InventoryUtils.extractFromInventory(stack, inventory, ForgeDirection.UNKNOWN, simulate = true).stackSize == 0
def hasRoomForItemStack(stack: ItemStack) = {
val remainder = stack.copy()
InventoryUtils.insertIntoInventory(remainder, inventory, None, remainder.stackSize, simulate = true)
remainder.stackSize == 0
}
// Check if we have enough to perform the trade.
if (containsAccumulativeItemStack(firstInputStack) && secondInputStack.forall(containsAccumulativeItemStack)) {
// Now we need to check if we have enough inventory space to accept the item we get for the trade.
val outputStack = recipe.getItemToSell.copy()
if (hasRoomForItemStack(outputStack)) {
// We established that out inventory allows to perform the trade, now actually do the trade.
InventoryUtils.extractFromInventory(firstInputStack, inventory, ForgeDirection.UNKNOWN)
secondInputStack.map(InventoryUtils.extractFromInventory(_, inventory, ForgeDirection.UNKNOWN))
InventoryUtils.insertIntoInventory(outputStack, inventory, None, outputStack.stackSize)
// Tell the merchant we used the recipe, so MC can disable it and/or enable more recipes.
info.merchant.get.orNull.useRecipe(recipe)
result(true)
} else {
result(false, "not enough inventory space to trade")
}
} else {
result(false, "not enough items to trade")
}
}
case _ => result(false, "trade has become invalid")
}
}
case _ => result(false, "trade has become invalid")
}
case _ => result(false, "trading requires an inventory upgrade to be installed")
}
}
}
class TradeInfo(var host: Option[EnvironmentHost], var merchant: WeakReference[IMerchant], var recipeID: Int) {
def this() = this(None, new WeakReference[IMerchant](null), -1)
def this(host: EnvironmentHost, merchant: IMerchant, recipeID: Int) =
this(Option(host), new WeakReference[IMerchant](merchant), recipeID)
def recipe = merchant.get.map(_.getRecipes(null).get(recipeID).asInstanceOf[MerchantRecipe])
def inventory = host match {
case Some(agent: li.cil.oc.api.internal.Agent) => Option(agent.mainInventory())
case _ => None
}
def load(nbt: NBTTagCompound): Unit = {
val isEntity = nbt.getBoolean("hostIsEntity")
// If drone we find it again by its UUID, if Robot we know the X/Y/Z of the TileEntity.
host = if (isEntity) loadHostEntity(nbt) else loadHostTileEntity(nbt)
merchant = new WeakReference[IMerchant](loadEntity(nbt, new UUID(nbt.getLong("merchantUUIDMost"), nbt.getLong("merchantUUIDLeast"))) match {
case Some(merchant: IMerchant) => merchant
case _ => null
})
recipeID = nbt.getInteger("recipeID")
}
def save(nbt: NBTTagCompound): Unit = {
host match {
case Some(entity: Entity) =>
nbt.setBoolean("hostIsEntity", true)
nbt.setInteger("dimensionID", entity.world.provider.dimensionId)
nbt.setLong("hostUUIDLeast", entity.getPersistentID.getLeastSignificantBits)
nbt.setLong("hostUUIDMost", entity.getPersistentID.getMostSignificantBits)
case Some(tileEntity: TileEntity) =>
nbt.setBoolean("hostIsEntity", false)
nbt.setInteger("dimensionID", tileEntity.getWorldObj.provider.dimensionId)
nbt.setInteger("hostX", tileEntity.xCoord)
nbt.setInteger("hostY", tileEntity.yCoord)
nbt.setInteger("hostZ", tileEntity.zCoord)
case _ => // Welp!
}
merchant.get match {
case Some(entity: Entity) =>
nbt.setLong("merchantUUIDLeast", entity.getPersistentID.getLeastSignificantBits)
nbt.setLong("merchantUUIDMost", entity.getPersistentID.getMostSignificantBits)
case _ =>
}
nbt.setInteger("recipeID", recipeID)
}
private def loadEntity(nbt: NBTTagCompound, uuid: UUID): Option[Entity] = {
val dimension = nbt.getInteger("dimensionID")
val world = DimensionManager.getProvider(dimension).worldObj
world.loadedEntityList.find {
case entity: Entity if entity.getPersistentID == uuid => true
case _ => false
}.map(_.asInstanceOf[Entity])
}
private def loadHostEntity(nbt: NBTTagCompound): Option[EnvironmentHost] = {
loadEntity(nbt, new UUID(nbt.getLong("hostUUIDMost"), nbt.getLong("hostUUIDLeast"))) match {
case Some(entity: Entity with li.cil.oc.api.internal.Agent) => Option(entity: EnvironmentHost)
case _ => None
}
}
private def loadHostTileEntity(nbt: NBTTagCompound): Option[EnvironmentHost] = {
val dimension = nbt.getInteger("dimensionID")
val world = DimensionManager.getProvider(dimension).worldObj
val x = nbt.getInteger("hostX")
val y = nbt.getInteger("hostY")
val z = nbt.getInteger("hostZ")
world.getTileEntity(x, y, z) match {
case robotProxy: li.cil.oc.common.tileentity.RobotProxy => Option(robotProxy.robot)
case agent: li.cil.oc.api.internal.Agent => Option(agent)
case null => None
}
}
}