2019-10-27 01:36:31 +00:00
|
|
|
package net.shadowfacts.phycon.network.block.terminal
|
2021-02-14 02:37:39 +00:00
|
|
|
|
|
|
|
import alexiil.mc.lib.attributes.item.GroupedItemInv
|
|
|
|
import alexiil.mc.lib.attributes.item.ItemStackCollections
|
|
|
|
import alexiil.mc.lib.attributes.item.ItemStackUtil
|
|
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
|
|
|
|
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
|
|
|
|
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
|
|
|
|
import net.minecraft.block.BlockState
|
|
|
|
import net.minecraft.entity.player.PlayerEntity
|
|
|
|
import net.minecraft.entity.player.PlayerInventory
|
|
|
|
import net.minecraft.inventory.Inventory
|
|
|
|
import net.minecraft.inventory.InventoryChangedListener
|
|
|
|
import net.minecraft.item.ItemStack
|
|
|
|
import net.minecraft.nbt.CompoundTag
|
|
|
|
import net.minecraft.nbt.ListTag
|
|
|
|
import net.minecraft.network.PacketByteBuf
|
|
|
|
import net.minecraft.screen.ScreenHandler
|
|
|
|
import net.minecraft.server.network.ServerPlayerEntity
|
|
|
|
import net.minecraft.text.LiteralText
|
|
|
|
import net.minecraft.util.Tickable
|
|
|
|
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.init.PhyBlockEntities
|
|
|
|
import net.shadowfacts.phycon.network.DeviceBlockEntity
|
|
|
|
import net.shadowfacts.phycon.network.NetworkUtil
|
|
|
|
import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity
|
|
|
|
import net.shadowfacts.phycon.network.component.ItemStackPacketHandler
|
|
|
|
import net.shadowfacts.phycon.network.component.handleItemStack
|
|
|
|
import net.shadowfacts.phycon.network.packet.*
|
|
|
|
import java.util.*
|
|
|
|
import kotlin.math.min
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @author shadowfacts
|
|
|
|
*/
|
|
|
|
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), InventoryChangedListener, BlockEntityClientSerializable, Tickable, ItemStackPacketHandler {
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
val LOCATE_REQUEST_TIMEOUT = 40 // ticks
|
|
|
|
val INSERTION_TIMEOUT = 40
|
|
|
|
}
|
|
|
|
|
|
|
|
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInv>()
|
|
|
|
val internalBuffer = TerminalBufferInventory(18)
|
|
|
|
|
|
|
|
private val locateRequestQueue = LinkedList<StackLocateRequest>()
|
|
|
|
private val pendingRequests = LinkedList<StackLocateRequest>()
|
|
|
|
private val pendingInsertions = Int2ObjectArrayMap<PendingStackInsertion>()
|
|
|
|
|
|
|
|
private var observers = 0
|
|
|
|
val cachedNetItems = ItemStackCollections.intMap()
|
|
|
|
var cachedSortedNetItems = listOf<ItemStack>()
|
|
|
|
|
|
|
|
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
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
|
|
|
|
inventoryCache.remove(packet.source)
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
}
|
|
|
|
|
|
|
|
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.sourceInterface)
|
|
|
|
if (request.totalResultAmount >= request.amount || counter - request.timestamp >= LOCATE_REQUEST_TIMEOUT || request.results.size >= inventoryCache.size) {
|
|
|
|
stackLocateRequestCompleted(request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
|
|
|
|
val remaining = internalBuffer.insertFromNetwork(packet.stack)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
|
|
|
|
return remaining
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleCapacity(packet: CapacityPacket) {
|
|
|
|
val insertion = pendingInsertions.values.firstOrNull {
|
|
|
|
ItemStackUtil.areEqualIgnoreAmounts(packet.stack, it.stack)
|
|
|
|
}
|
|
|
|
if (insertion != null) {
|
|
|
|
insertion.results.add(packet.capacity to packet.receivingInterface)
|
|
|
|
if (insertion.isFinishable(counter)) {
|
|
|
|
finishInsertion(insertion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateNetItems() {
|
|
|
|
cachedNetItems.clear()
|
|
|
|
for (inventory in inventoryCache.values) {
|
|
|
|
for (stack in inventory.storedStacks) {
|
|
|
|
val amount = inventory.getAmount(stack)
|
|
|
|
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// todo: is the map necessary or is just the sorted list enough?
|
|
|
|
cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map {
|
|
|
|
val stack = it.key.copy()
|
|
|
|
stack.count = it.intValue
|
|
|
|
stack
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun beginInsertions() {
|
|
|
|
if (world!!.isClient) return
|
|
|
|
|
|
|
|
for (slot in 0 until internalBuffer.size()) {
|
|
|
|
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
|
|
|
|
if (slot in pendingInsertions) continue
|
|
|
|
val stack = internalBuffer.getStack(slot)
|
|
|
|
pendingInsertions[slot] = PendingStackInsertion(slot, stack, counter)
|
|
|
|
sendPacket(CheckCapacityPacket(stack, ipAddress, IPAddress.BROADCAST))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun finishPendingInsertions() {
|
|
|
|
if (world!!.isClient) return
|
|
|
|
|
|
|
|
for (insertion in pendingInsertions.values) {
|
|
|
|
if (!insertion.isFinishable(counter)) continue
|
|
|
|
finishInsertion(insertion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun sendEnqueuedLocateRequests() {
|
|
|
|
if (world!!.isClient) return
|
|
|
|
|
|
|
|
for (request in locateRequestQueue) {
|
|
|
|
pendingRequests.add(request)
|
|
|
|
sendPacket(LocateStackPacket(request.stack, ipAddress))
|
|
|
|
}
|
|
|
|
locateRequestQueue.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun finishPendingRequests() {
|
|
|
|
if (world!!.isClient) return
|
|
|
|
|
|
|
|
for (request in pendingRequests) {
|
|
|
|
if (request.isFinishable(counter)) {
|
|
|
|
stackLocateRequestCompleted(request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun addObserver() {
|
|
|
|
observers++
|
|
|
|
}
|
|
|
|
|
|
|
|
fun removeObserver() {
|
|
|
|
observers--
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun tick() {
|
|
|
|
super.tick()
|
|
|
|
|
|
|
|
if (counter % 20 == 0L) {
|
|
|
|
if (!world!!.isClient) {
|
|
|
|
sendEnqueuedLocateRequests()
|
|
|
|
finishPendingRequests()
|
|
|
|
beginInsertions()
|
|
|
|
finishPendingInsertions()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (observers > 0) {
|
|
|
|
if (world!!.isClient) {
|
|
|
|
println(cachedNetItems)
|
|
|
|
} else {
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onActivate(player: PlayerEntity) {
|
|
|
|
if (!world!!.isClient) {
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
|
|
|
|
inventoryCache.clear()
|
|
|
|
sendPacket(RequestInventoryPacket(ipAddress))
|
|
|
|
val factory = object: ExtendedScreenHandlerFactory {
|
|
|
|
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? {
|
|
|
|
return TerminalScreenHandler(syncId, playerInv, this@TerminalBlockEntity)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getDisplayName() = LiteralText("Terminal")
|
|
|
|
|
|
|
|
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
|
|
|
|
buf.writeBlockPos(this@TerminalBlockEntity.pos)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
player.openHandledScreen(factory)
|
|
|
|
}
|
|
|
|
addObserver()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
|
|
|
|
locateRequestQueue.add(StackLocateRequest(stack, amount, counter))
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun stackLocateRequestCompleted(request: StackLocateRequest) {
|
|
|
|
pendingRequests.remove(request)
|
|
|
|
|
|
|
|
// todo: also sort results by interface priority
|
|
|
|
val sortedResults = request.results.sortedByDescending { it.first }.toMutableList()
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun finishInsertion(insertion: PendingStackInsertion) {
|
|
|
|
pendingInsertions.remove(insertion.bufferSlot)
|
|
|
|
|
|
|
|
// todo: also sort results by interface priority
|
|
|
|
val sortedResults = insertion.results.sortedBy { it.first }.toMutableList()
|
|
|
|
val remaining = insertion.stack
|
|
|
|
while (!remaining.isEmpty && sortedResults.isNotEmpty()) {
|
|
|
|
val (capacity, receivingInterface) = sortedResults.removeAt(0)
|
|
|
|
if (capacity <= 0) continue
|
|
|
|
sendPacket(ItemStackPacket(remaining.copy(), ipAddress, receivingInterface.ipAddress))
|
|
|
|
// todo: the interface should confirm how much was actually inserted, in case of race condition
|
|
|
|
remaining.count -= capacity
|
|
|
|
}
|
|
|
|
internalBuffer.setStack(insertion.bufferSlot, remaining)
|
|
|
|
|
|
|
|
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
|
|
|
|
updateNetItems()
|
|
|
|
sync()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onInventoryChanged(inv: Inventory) {
|
|
|
|
if (inv == internalBuffer && world != null && !world!!.isClient) {
|
|
|
|
markDirty()
|
|
|
|
sync()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun toTag(tag: CompoundTag): CompoundTag {
|
|
|
|
tag.put("InternalBuffer", internalBuffer.toTag())
|
|
|
|
return super.toTag(tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun fromTag(state: BlockState, tag: CompoundTag) {
|
|
|
|
super.fromTag(state, tag)
|
|
|
|
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun toClientTag(tag: CompoundTag): CompoundTag {
|
|
|
|
tag.put("InternalBuffer", internalBuffer.toTag())
|
|
|
|
val list = ListTag()
|
|
|
|
tag.put("CachedNetItems", list)
|
|
|
|
for ((stack, amount) in cachedNetItems) {
|
|
|
|
val entryTag = stack.toTag(CompoundTag())
|
|
|
|
entryTag.putInt("NetAmount", amount)
|
|
|
|
list.add(entryTag)
|
|
|
|
}
|
|
|
|
return tag
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun fromClientTag(tag: CompoundTag) {
|
|
|
|
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
|
|
|
|
val list = tag.getList("CachedNetItems", 10)
|
|
|
|
cachedNetItems.clear()
|
|
|
|
for (entryTag in list) {
|
|
|
|
val stack = ItemStack.fromTag(entryTag as CompoundTag)
|
|
|
|
val netAmount = entryTag.getInt("NetAmount")
|
|
|
|
cachedNetItems[stack] = netAmount
|
|
|
|
}
|
|
|
|
cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map {
|
|
|
|
val stack = it.key.copy()
|
|
|
|
stack.count = it.intValue
|
|
|
|
stack
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
data class StackLocateRequest(
|
|
|
|
val stack: ItemStack,
|
|
|
|
val amount: Int,
|
|
|
|
val timestamp: Long,
|
|
|
|
var results: MutableSet<Pair<Int, InterfaceBlockEntity>> = mutableSetOf()
|
|
|
|
) {
|
|
|
|
val totalResultAmount: Int
|
|
|
|
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
|
|
|
|
|
|
|
|
fun isFinishable(currentTimestamp: Long): Boolean {
|
|
|
|
return totalResultAmount >= amount || currentTimestamp - timestamp >= TerminalBlockEntity.LOCATE_REQUEST_TIMEOUT
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data class PendingStackInsertion(
|
|
|
|
val bufferSlot: Int,
|
|
|
|
val stack: ItemStack,
|
|
|
|
val timestamp: Long,
|
|
|
|
val results: MutableSet<Pair<Int, InterfaceBlockEntity>> = mutableSetOf(),
|
|
|
|
) {
|
|
|
|
val totalCapacity: Int
|
|
|
|
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
|
|
|
|
|
|
|
|
fun isFinishable(currentTimestamp: Long): Boolean {
|
|
|
|
return totalCapacity >= stack.count || currentTimestamp - timestamp >= TerminalBlockEntity.INSERTION_TIMEOUT
|
|
|
|
}
|
|
|
|
}
|