Compare commits

..

7 Commits

Author SHA1 Message Date
Shadowfacts f4f4c7ff03
Terminal: Rudimentary stack extraction 2019-10-29 21:58:25 -04:00
Shadowfacts 5d62e60bb2
Terminal: sync internal buffer inventory to client 2019-10-29 21:00:15 -04:00
Shadowfacts 68e612c63c
Terminal GUI: add read only fake slot 2019-10-29 20:59:58 -04:00
Shadowfacts de81412630
Only recalculate and sync net items when a player has the terminal open 2019-10-29 17:45:58 -04:00
Shadowfacts 7bc859eaf6
Sync terminal cached item counts to client 2019-10-29 17:33:33 -04:00
Shadowfacts 64c18e9eae
Start adding terminal container and GUI 2019-10-29 15:15:27 -04:00
Shadowfacts 0c5e353525
Change terminal to read inventories instead of stacks directly
This is slightly more efficient, and removes the need for interfaces to
observe their inventories for changes (something that's not possible in
Vanilla).
2019-10-29 11:25:51 -04:00
18 changed files with 534 additions and 51 deletions

View File

@ -1,9 +1,17 @@
package net.shadowfacts.phycon
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.container.ContainerFactory
import net.fabricmc.fabric.api.container.ContainerProviderRegistry
import net.minecraft.container.Container
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.PacketByteBuf
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyItems
import net.shadowfacts.phycon.network.block.terminal.TerminalBlock
import net.shadowfacts.phycon.network.block.terminal.TerminalContainer
/**
* @author shadowfacts
@ -16,5 +24,13 @@ object PhysicalConnectivity: ModInitializer {
PhyBlocks.init()
PhyBlockEntities.init()
PhyItems.init()
ContainerProviderRegistry.INSTANCE.registerFactory(TerminalContainer.ID, ContainerFactory(::createTerminalContainer))
}
private fun createTerminalContainer(syncId: Int, identifier: Identifier, player: PlayerEntity, buf: PacketByteBuf): Container {
val pos = buf.readBlockPos()
val terminalEntity = PhyBlocks.TERMINAL.getBlockEntity(player.world, pos)!!
return TerminalContainer(syncId, player.inventory, terminalEntity)
}
}

View File

@ -0,0 +1,22 @@
package net.shadowfacts.phycon
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.screen.ContainerScreenFactory
import net.fabricmc.fabric.api.client.screen.ScreenProviderRegistry
import net.minecraft.client.MinecraftClient
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.network.block.terminal.TerminalContainer
import net.shadowfacts.phycon.network.block.terminal.TerminalScreen
/**
* @author shadowfacts
*/
object PhysicalConnectivityClient: ClientModInitializer {
override fun onInitializeClient() {
ScreenProviderRegistry.INSTANCE.registerFactory(TerminalContainer.ID, ContainerScreenFactory(::createTerminalScreen))
}
fun createTerminalScreen(container: TerminalContainer): TerminalScreen {
return TerminalScreen(container, MinecraftClient.getInstance().player.inventory)
}
}

View File

@ -3,13 +3,11 @@ package net.shadowfacts.phycon.network.block.netinterface
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import net.minecraft.item.ItemStack
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.packet.ReadAllPacket
import net.shadowfacts.phycon.network.packet.RequestReadAllPacket
import net.shadowfacts.phycon.network.packet.*
/**
* @author shadowfacts
@ -28,24 +26,39 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE) {
inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option)
}
override fun handlePacket(packet: Packet) {
when (packet) {
is RequestReadAllPacket -> handleReadAll(packet)
}
}
fun handleReadAll(packet: RequestReadAllPacket) {
enqueueToSingle(ReadAllPacket(readAll(), macAddress, packet.source))
}
fun readAll(): Map<ItemStack, Int> {
private fun getInventory(): GroupedItemInv? {
// if we don't have an inventory, try to get one
// this happens when readAll is called before a neighbor state changes, such as immediately after world load
if (inventory == null) updateInventory()
return inventory
}
return inventory?.let {
it.storedStacks.associateWith(it::getAmount)
} ?: mapOf()
override fun handlePacket(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet)
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
getInventory()?.also { inv ->
enqueueToSingle(ReadInventoryPacket(inv, macAddress, packet.source))
}
}
private fun handleLocateStack(packet: LocateStackPacket) {
getInventory()?.also { inv ->
val amount = inv.getAmount(packet.stack)
enqueueToSingle(StackLocationPacket(packet.stack, amount, this, macAddress, packet.source))
}
}
private fun handleExtractStack(packet: ExtractStackPacket) {
getInventory()?.also { inv ->
val extracted = inv.extract(packet.stack, packet.amount)
enqueueToSingle(ItemStackPacket(extracted, macAddress, packet.source))
}
}
}

View File

@ -31,8 +31,8 @@ class TerminalBlock: BlockWithEntity<TerminalBlockEntity>(Settings.of(Material.M
override fun createBlockEntity(world: BlockView) = TerminalBlockEntity()
override fun activate(blockState_1: BlockState?, world_1: World?, blockPos_1: BlockPos?, playerEntity_1: PlayerEntity?, hand_1: Hand?, blockHitResult_1: BlockHitResult?): Boolean {
getBlockEntity(world_1!!, blockPos_1!!)!!.onActivate()
override fun activate(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): Boolean {
getBlockEntity(world, pos)!!.onActivate(player)
return true
}

View File

@ -1,57 +1,227 @@
package net.shadowfacts.phycon.network.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemStackCollections
import it.unimi.dsi.fastutil.objects.Object2IntMap
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.container.ContainerProviderRegistry
import net.minecraft.entity.ItemEntity
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.inventory.BasicInventory
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.MACAddress
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity
import net.shadowfacts.phycon.network.packet.DeviceRemovedPacket
import net.shadowfacts.phycon.network.packet.ReadAllPacket
import net.shadowfacts.phycon.network.packet.RequestReadAllPacket
import net.shadowfacts.phycon.network.packet.*
import net.shadowfacts.phycon.util.fromTag
import net.shadowfacts.phycon.util.toTag
import java.util.*
import kotlin.math.min
/**
* @author shadowfacts
*/
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL) {
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), InventoryListener, BlockEntityClientSerializable {
private val inventoryCache = mutableMapOf<MACAddress, Map<ItemStack, Int>>()
companion object {
val LOCATE_REQUEST_TIMEOUT = 40 // ticks
}
private val inventoryCache = mutableMapOf<MACAddress, GroupedItemInv>()
val internalBuffer = BasicInventory(18)
private val pendingRequests = LinkedList<StackLocateRequest>()
private var observers = 0
val cachedNetItems = ItemStackCollections.intMap()
var cachedSortedNetItems = listOf<ItemStack>()
var counter = 0
init {
internalBuffer.addListener(this)
}
override fun handlePacket(packet: Packet) {
when (packet) {
is ReadAllPacket -> handleReadAll(packet)
is ReadInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
private fun handleReadAll(packet: ReadAllPacket) {
inventoryCache[packet.source] = packet.items
println("new items: ${computeNetItems()}")
private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateNetItems()
sync()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
updateNetItems()
sync()
}
fun computeNetItems(): Map<ItemStack, Int> {
val net = ItemStackCollections.intMap()
for (map in inventoryCache.values) {
for ((stack, amount) in map) {
net.mergeInt(stack, amount) { a, b -> a + b }
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) {
pendingRequests.remove(request)
stackLocateRequestCompleted(request)
}
}
return net
}
fun onActivate() {
private fun handleItemStack(packet: ItemStackPacket) {
// todo: handle merging stacks into the buffer better?
for (i in 0 until internalBuffer.invSize) {
if (internalBuffer.getInvStack(i).isEmpty) {
internalBuffer.setInvStack(i, packet.stack)
return
}
}
// todo: calculate entity spawn point by finding non-obstructed location
val entity = ItemEntity(world!!, pos.x.toDouble(), pos.y + 1.0, pos.z.toDouble(), packet.stack)
world!!.spawnEntity(entity)
}
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
}
}
fun addObserver() {
observers++
}
fun removeObserver() {
observers--
}
override fun tick() {
super.tick()
if (observers > 0 && (++counter % 20) == 0) {
if (world!!.isClient) {
println(cachedNetItems)
} else {
updateNetItems()
sync()
}
}
}
fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) {
updateNetItems()
sync()
inventoryCache.clear()
enqueueToSingle(RequestReadAllPacket(macAddress))
enqueueToSingle(RequestInventoryPacket(macAddress))
ContainerProviderRegistry.INSTANCE.openContainer(TerminalContainer.ID, player) { buf ->
buf.writeBlockPos(pos)
}
}
addObserver()
}
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
pendingRequests.add(StackLocateRequest(stack, amount, counter))
enqueueToSingle(LocateStackPacket(stack, macAddress))
}
private fun stackLocateRequestCompleted(request: StackLocateRequest) {
// 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
enqueueToSingle(ExtractStackPacket(request.stack, amountToRequest, macAddress, sourceInterface.macAddress))
}
}
override fun onInvChange(inv: Inventory) {
if (inv == internalBuffer && !world!!.isClient) {
markDirty()
sync()
}
}
override fun toTag(tag: CompoundTag): CompoundTag {
tag.put("InternalBuffer", internalBuffer.toTag())
return super.toTag(tag)
}
override fun fromTag(tag: CompoundTag) {
super.fromTag(tag)
internalBuffer.fromTag(tag.getList("InternalBuffer", 10))
}
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) {
tag.getList("InternalBuffer", 10)?.also { list ->
if (list.isNotEmpty()) {
internalBuffer.fromTag(list)
} else {
// todo: should this clear or just do nothing?
internalBuffer.clear()
}
}
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: Int,
var results: MutableSet<Pair<Int, InterfaceBlockEntity>> = mutableSetOf()
) {
val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
}

View File

@ -0,0 +1,73 @@
package net.shadowfacts.phycon.network.block.terminal
import net.minecraft.container.Container
import net.minecraft.container.Slot
import net.minecraft.container.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import kotlin.math.max
/**
* @author shadowfacts
*/
class TerminalContainer(syncId: Int, playerInv: PlayerInventory, val terminal: TerminalBlockEntity): Container(null, syncId) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
}
init {
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(terminal, y * 9 + x, 66 + x * 18, 18 + y * 18))
}
}
// internal buffer
for (y in 0 until 6) {
for (x in 0 until 3) {
addSlot(Slot(terminal.internalBuffer, y * 3 + x, 8 + x * 18, 18 + y * 18))
}
}
// player inv
for (y in 0 until 3) {
for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, 66 + x * 18, 140 + y * 18))
}
}
// hotbar
for (x in 0 until 9) {
addSlot(Slot(playerInv, x, 66 + x * 18, 198))
}
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
terminal.removeObserver()
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (actionType == SlotActionType.QUICK_MOVE) {
if (slotId < 54) {
// the slot clicked was one of the network stacks
val slot = slotList[slotId]
val stack = slotList[slotId].stack
if (!stack.isEmpty && !player.world.isClient) {
terminal.requestItem(stack, max(stack.count, stack.maxCount))
}
return ItemStack.EMPTY
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
}
}

View File

@ -0,0 +1,61 @@
package net.shadowfacts.phycon.network.block.terminal
import net.minecraft.container.Slot
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.inventory.Inventory
import net.minecraft.item.ItemStack
/**
* @author shadowfacts
*/
class TerminalFakeSlot(val terminal: TerminalBlockEntity, slot: Int, x: Int, y: Int): Slot(FakeInventory(terminal, slot), slot, x, y) {
override fun canInsert(stack: ItemStack): Boolean {
return false
}
override fun setStack(stack: ItemStack) {
}
override fun canTakeItems(player: PlayerEntity): Boolean {
return false
}
}
class FakeInventory(val terminal: TerminalBlockEntity, val slot: Int): Inventory {
override fun getInvStack(_slot: Int): ItemStack {
if (slot >= terminal.cachedSortedNetItems.size) return ItemStack.EMPTY
return terminal.cachedSortedNetItems[slot]
}
override fun markDirty() {
}
override fun clear() {
}
override fun setInvStack(p0: Int, p1: ItemStack?) {
}
override fun removeInvStack(p0: Int): ItemStack {
return ItemStack.EMPTY
}
override fun canPlayerUseInv(p0: PlayerEntity?): Boolean {
return false
}
override fun getInvSize(): Int {
return 1
}
override fun takeInvStack(p0: Int, p1: Int): ItemStack {
return ItemStack.EMPTY
}
override fun isInvEmpty(): Boolean {
return false
}
}

View File

@ -0,0 +1,40 @@
package net.shadowfacts.phycon.network.block.terminal
import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.gui.screen.ingame.AbstractContainerScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.text.LiteralText
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
/**
* @author shadowfacts
*/
// todo: translate title
class TerminalScreen(container: TerminalContainer, playerInv: PlayerInventory): AbstractContainerScreen<TerminalContainer>(container, playerInv, LiteralText("Terminal")) {
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png")
}
init {
containerWidth = 252
containerHeight = 222
}
override fun drawForeground(mouseX: Int, mouseY: Int) {
font.draw(title.asFormattedString(), 65f, 6f, 0x404040)
font.draw(playerInventory.displayName.asFormattedString(), 65f, containerHeight - 94f, 0x404040)
// todo: translate this
font.draw("Buffer", 7f, 6f, 0x404040)
}
override fun drawBackground(delta: Float, mouseX: Int, mouseY: Int) {
GlStateManager.color4f(1f, 1f, 1f, 1f)
minecraft!!.textureManager.bindTexture(BACKGROUND)
val x = (width - containerWidth) / 2
val y = (height - containerHeight) / 2
blit(x, y, 0, 0, containerWidth, containerHeight)
}
}

View File

@ -0,0 +1,10 @@
package net.shadowfacts.phycon.network.packet
import net.minecraft.item.ItemStack
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class ExtractStackPacket(val stack: ItemStack, val amount: Int, source: MACAddress, destination: MACAddress): BasePacket(source, destination) {
}

View File

@ -0,0 +1,10 @@
package net.shadowfacts.phycon.network.packet
import net.minecraft.item.ItemStack
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class ItemStackPacket(val stack: ItemStack, source: MACAddress, destination: MACAddress): BasePacket(source, destination) {
}

View File

@ -0,0 +1,10 @@
package net.shadowfacts.phycon.network.packet
import net.minecraft.item.ItemStack
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class LocateStackPacket(val stack: ItemStack, source: MACAddress, destination: MACAddress = MACAddress.BROADCAST): BasePacket(source, destination) {
}

View File

@ -1,13 +1,13 @@
package net.shadowfacts.phycon.network.packet
import net.minecraft.item.ItemStack
import alexiil.mc.lib.attributes.item.GroupedItemInv
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class ReadAllPacket(
val items: Map<ItemStack, Int>,
class ReadInventoryPacket(
val inventory: GroupedItemInv,
source: MACAddress,
destination: MACAddress
): BasePacket(source, destination)

View File

@ -0,0 +1,8 @@
package net.shadowfacts.phycon.network.packet
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class RequestInventoryPacket(source: MACAddress, destination: MACAddress = MACAddress.BROADCAST): BasePacket(source, destination)

View File

@ -1,8 +0,0 @@
package net.shadowfacts.phycon.network.packet
import net.shadowfacts.phycon.api.util.MACAddress
/**
* @author shadowfacts
*/
class RequestReadAllPacket(source: MACAddress, destination: MACAddress = MACAddress.BROADCAST): BasePacket(source, destination)

View File

@ -0,0 +1,18 @@
package net.shadowfacts.phycon.network.packet
import net.minecraft.item.ItemStack
import net.shadowfacts.phycon.api.util.MACAddress
import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity
/**
* @author shadowfacts
*/
// todo: better name with LocateStackPacket
class StackLocationPacket(
val stack: ItemStack,
val amount: Int,
val sourceInterface: InterfaceBlockEntity,
source: MACAddress,
destination: MACAddress
): BasePacket(source, destination) {
}

View File

@ -0,0 +1,34 @@
package net.shadowfacts.phycon.util
import net.minecraft.inventory.BasicInventory
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import java.lang.RuntimeException
/**
* @author shadowfacts
*/
fun BasicInventory.toTag(): ListTag {
val list = ListTag()
for (slot in 0 until invSize) {
val stack = getInvStack(slot)
if (!stack.isEmpty) {
val stackTag = stack.toTag(CompoundTag())
stackTag.putInt("Slot", slot)
list.add(stackTag)
}
}
return list
}
fun BasicInventory.fromTag(list: ListTag) {
if (list.listType != 10) throw RuntimeException("Can't decode BasicInventory from list tag that does not contain compound tags")
this.clear()
for (tag in list) {
val compound = tag as CompoundTag
val stack = ItemStack.fromTag(compound)
val slot = compound.getInt("Slot")
setInvStack(slot, stack)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -19,6 +19,12 @@
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.PhysicalConnectivity"
}
],
"client": [
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.PhysicalConnectivityClient"
}
]
},
"mixins": [