PhysicalConnectivity/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt

264 lines
8.3 KiB
Kotlin

package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryChangedListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.ItemScatterer
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.NetworkUtil
import java.lang.ref.WeakReference
import java.util.*
import java.util.function.IntBinaryOperator
import kotlin.math.min
import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): DeviceBlockEntity(type, pos, state),
InventoryChangedListener,
ItemStackPacketHandler,
NetworkStackDispatcher<AbstractTerminalBlockEntity.PendingInsertion> {
companion object {
// the locate/insertion timeouts are only 1 tick because that's long enough to hear from every device on the network
// in a degraded state (when there's latency in the network), not handling interface priorities correctly is acceptable
val LOCATE_REQUEST_TIMEOUT: Long = 1 // ticks
val INSERTION_TIMEOUT: Long = 1
}
protected val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
protected val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT
private var observers = 0
val cachedNetItems = ItemStackCollections.intMap()
// todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null
init {
internalBuffer.addListener(this)
}
override fun findDestination(): Interface? {
for (dir in Direction.values()) {
val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir)
if (itf != null) {
return itf
}
}
return null
}
override fun handle(packet: Packet) {
when (packet) {
is ReadInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
is CapacityPacket -> handleCapacity(packet)
}
}
private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateAndSync()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
updateAndSync()
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = pendingRequests.firstOrNull {
ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack)
}
if (request != null) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
stackLocateRequestCompleted(request)
}
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val mode =
if (packet.bounceCount > 0) {
// if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry
TerminalBufferInventory.Mode.TO_NETWORK
} else {
TerminalBufferInventory.Mode.FROM_NETWORK
}
val remaining = internalBuffer.insert(packet.stack, mode)
// this happens outside the normal update loop because by receiving the item stack packet
// we "know" how much the count in the source inventory has changed
updateAndSync()
return remaining
}
protected fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
markUpdate()
// syncs the open container (if any) to the client
netItemObserver?.get()?.netItemsChanged()
}
private fun updateNetItems() {
cachedNetItems.clear()
for (inventory in inventoryCache.values) {
for (stack in inventory.storedStacks) {
val amount = inventory.getAmount(stack)
cachedNetItems.mergeInt(stack, amount, IntBinaryOperator { a, b -> a + b })
}
}
}
private fun beginInsertions() {
if (world!!.isClient) return
for (slot in 0 until internalBuffer.size()) {
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
if (pendingInsertions.any { it.bufferSlot == slot }) continue
val stack = internalBuffer.getStack(slot)
dispatchItemStack(stack) { insertion ->
insertion.bufferSlot = slot
}
}
}
private fun finishPendingRequests() {
if (world!!.isClient) return
if (pendingRequests.isEmpty()) return
val finishable = pendingRequests.filter { it.isFinishable(counter) }
// stackLocateRequestCompleted removes the object from pendingRequests
finishable.forEach(::stackLocateRequestCompleted)
}
override fun tick() {
super.tick()
if (!world!!.isClient) {
finishPendingRequests()
finishTimedOutPendingInsertions()
}
if (counter % 20 == 0L && !world!!.isClient) {
beginInsertions()
}
}
open fun onActivate(player: PlayerEntity) {
}
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
val request = StackLocateRequest(stack, amount, counter)
pendingRequests.add(request)
// locate packets are sent immediately instead of being added to a queue
// otherwise the terminal UI feels sluggish and unresponsive
sendPacket(LocateStackPacket(stack, ipAddress))
}
protected open fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request)
val sortedResults = request.results.toMutableList()
sortedResults.sortWith { a, b ->
// sort results first by provider priority, and then by the count that it can provide
if (a.second.providerPriority == b.second.providerPriority) {
b.first - a.first
} else {
b.second.providerPriority - a.second.providerPriority
}
}
var amountRequested = 0
while (amountRequested < request.amount && sortedResults.isNotEmpty()) {
val (sourceAmount, sourceInterface) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, request.amount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress))
}
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val remaining = super.finishInsertion(insertion)
internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync()
return remaining
}
override fun onInventoryChanged(inv: Inventory) {
if (inv == internalBuffer && world != null && !world!!.isClient) {
markUpdate()
}
}
open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
}
interface NetItemObserver {
fun netItemsChanged()
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
var bufferSlot by Delegates.notNull<Int>()
}
open class StackLocateRequest(
val stack: ItemStack,
val amount: Int,
val timestamp: Long,
) {
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean {
// we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to
// correctly sort by priority
return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT
}
}
}