Compare commits

..

5 Commits

Author SHA1 Message Date
Shadowfacts 700817919a
Move face device block logic to separate base class
The extractor currently isn't considered a "face block" because its
network connection is always in the opposite direction of its facing,
unlike the interace.
2021-02-21 15:35:20 -05:00
Shadowfacts 7abdb69b87
Move terminal stack requesting logic to TerminalScreen 2021-02-21 15:18:48 -05:00
Shadowfacts f9befe9549
Calculate terminal displayed items on the server
Avoids sending giant S2C packets containing all items in the network
when the network contains a great variety of different items
2021-02-21 12:02:38 -05:00
Shadowfacts 3ebafc062f
Add Terminal GUI sort mode 2021-02-21 11:14:15 -05:00
Shadowfacts 404eb27e67
Fix shift-clicking stacks from buffer not matching vanilla slot
insertion order
2021-02-20 15:28:55 -05:00
14 changed files with 436 additions and 186 deletions

View File

@ -7,6 +7,7 @@ import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyItems import net.shadowfacts.phycon.init.PhyItems
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.networking.ServerReceiver import net.shadowfacts.phycon.networking.ServerReceiver
/** /**
@ -23,6 +24,7 @@ object PhysicalConnectivity: ModInitializer {
PhyScreens.init() PhyScreens.init()
registerGlobalReceiver(C2STerminalRequestItem) registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
} }
private fun registerGlobalReceiver(receiver: ServerReceiver) { private fun registerGlobalReceiver(receiver: ServerReceiver) {

View File

@ -2,19 +2,30 @@ package net.shadowfacts.phycon
import net.fabricmc.api.ClientModInitializer import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry
import net.minecraft.client.render.RenderLayer import net.minecraft.client.render.RenderLayer
import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.network.block.terminal.TerminalScreen import net.shadowfacts.phycon.network.block.terminal.TerminalScreen
import net.shadowfacts.phycon.networking.ClientReceiver
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhysicalConnectivityClient: ClientModInitializer { object PhysicalConnectivityClient: ClientModInitializer {
override fun onInitializeClient() { override fun onInitializeClient() {
BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent()) BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent())
ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen)
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
} }
private fun registerGlobalReceiver(receiver: ClientReceiver) {
ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
}
} }

View File

@ -0,0 +1,102 @@
package net.shadowfacts.phycon.network
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.ShapeContext
import net.minecraft.item.ItemPlacementContext
import net.minecraft.state.StateManager
import net.minecraft.state.property.EnumProperty
import net.minecraft.state.property.Properties
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.network.block.cable.CableBlock
import java.util.*
/**
* @author shadowfacts
*/
abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): DeviceBlock<T>(settings) {
companion object {
val FACING = Properties.FACING
val CABLE_CONNECTION = EnumProperty.of("cable_connection", Direction::class.java)
}
protected abstract val faceThickness: Double
protected abstract val faceShapes: Map<Direction, VoxelShape>
private val centerShapes: Map<Direction, VoxelShape> by lazy {
mapOf(
Direction.DOWN to createCuboidShape(6.0, faceThickness, 6.0, 10.0, 10.0, 10.0),
Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 16.0 - faceThickness, 10.0),
Direction.NORTH to createCuboidShape(6.0, 6.0, faceThickness, 10.0, 10.0, 10.0),
Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 16.0 - faceThickness),
Direction.WEST to createCuboidShape(faceThickness, 6.0, 6.0, 10.0, 10.0, 10.0),
Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 16.0 - faceThickness, 10.0, 10.0)
)
}
private val shapeCache = mutableMapOf<Pair<Direction, Direction>, VoxelShape>()
fun getShape(facing: Direction, cableConnection: Direction): VoxelShape {
return shapeCache.getOrPut(facing to cableConnection) {
VoxelShapes.union(
faceShapes[facing],
centerShapes[facing],
CableBlock.SIDE_SHAPES[cableConnection]
)
}
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.of(state[CABLE_CONNECTION])
}
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? {
return if (side == state[FACING]) {
null
} else {
getBlockEntity(world, pos)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
builder.add(CABLE_CONNECTION)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState? {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite
val cableConnection = getCableConnectedSide(context.world, context.blockPos) ?: facing.opposite
return defaultState
.with(FACING, facing)
.with(CABLE_CONNECTION, cableConnection)
}
protected fun getCableConnectedSide(world: World, pos: BlockPos): Direction? {
for (side in Direction.values()) {
val offsetPos = pos.offset(side)
if (world.getBlockState(offsetPos) is NetworkComponentBlock) {
return side
}
}
return null
}
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState {
if (neighborState.block is NetworkComponentBlock && world.getBlockState(pos.offset(state[CABLE_CONNECTION])).block !is NetworkComponentBlock) {
return state.with(CABLE_CONNECTION, side)
}
return state
}
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape {
return getShape(state[FACING], state[CABLE_CONNECTION])
}
}

View File

@ -4,103 +4,39 @@ import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.* import net.minecraft.block.*
import net.minecraft.entity.LivingEntity import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.state.StateManager
import net.minecraft.state.property.EnumProperty
import net.minecraft.state.property.Properties
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView import net.minecraft.world.BlockView
import net.minecraft.world.World import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.api.Interface import net.shadowfacts.phycon.network.FaceDeviceBlock
import net.shadowfacts.phycon.network.DeviceBlock
import net.shadowfacts.phycon.network.block.cable.CableBlock
import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class InterfaceBlock: DeviceBlock<InterfaceBlockEntity>(Settings.of(Material.METAL)), class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(Settings.of(Material.METAL)),
NetworkComponentBlock, NetworkComponentBlock,
AttributeProvider { AttributeProvider {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "network_interface") val ID = Identifier(PhysicalConnectivity.MODID, "network_interface")
val FACING = Properties.FACING
val CABLE_CONNECTION = EnumProperty.of("cable_connection", Direction::class.java)
private val SIDE_SHAPES = mapOf<Direction, VoxelShape>(
Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0),
Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0),
Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0),
Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0),
Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0)
)
private val CENTER_SHAPES = mapOf<Direction, VoxelShape>(
Direction.DOWN to createCuboidShape(6.0, 2.0, 6.0, 10.0, 10.0, 10.0),
Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 14.0, 10.0),
Direction.NORTH to createCuboidShape(6.0, 6.0, 2.0, 10.0, 10.0, 10.0),
Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 14.0),
Direction.WEST to createCuboidShape(2.0, 6.0, 6.0, 10.0, 10.0, 10.0),
Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 14.0, 10.0, 10.0)
)
private val shapeCache = mutableMapOf<Pair<Direction, Direction>, VoxelShape>()
fun getShape(facing: Direction, cableConnection: Direction): VoxelShape {
return shapeCache.getOrPut(facing to cableConnection) {
VoxelShapes.union(
VoxelShapes.union(SIDE_SHAPES[facing], CENTER_SHAPES[facing]),
CableBlock.SIDE_SHAPES[cableConnection]
)
}
}
} }
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override val faceThickness = 2.0
val set = EnumSet.of(state[CABLE_CONNECTION]) override val faceShapes = mapOf(
set.remove(state[FACING]) Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0),
return set Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0),
} Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0),
Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0),
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0),
return if (side == state[FACING]) { Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0)
null )
} else {
getBlockEntity(world, pos)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
builder.add(CABLE_CONNECTION)
}
override fun createBlockEntity(world: BlockView) = InterfaceBlockEntity() override fun createBlockEntity(world: BlockView) = InterfaceBlockEntity()
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite
val cableConnection = getCableConnectionSide(context.world, context.blockPos) ?: facing.opposite
return defaultState.with(FACING, facing).with(CABLE_CONNECTION, cableConnection)
}
private fun getCableConnectionSide(world: World, pos: BlockPos): Direction? {
for (side in Direction.values()) {
val offsetPos = pos.offset(side)
if (world.getBlockState(offsetPos).block is NetworkComponentBlock) {
return side
}
}
return null
}
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) { override fun onPlaced(world: World, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
if (!world.isClient) { if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory() getBlockEntity(world, pos)!!.updateInventory()
@ -113,19 +49,8 @@ class InterfaceBlock: DeviceBlock<InterfaceBlockEntity>(Settings.of(Material.MET
} }
} }
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState {
if (neighborState.block is NetworkComponentBlock && world.getBlockState(pos.offset(state[CABLE_CONNECTION])).block !is NetworkComponentBlock) {
return state.with(CABLE_CONNECTION, side)
}
return state
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) { override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos)) to.offer(getBlockEntity(world, pos))
} }
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape {
return getShape(state[FACING], state[CABLE_CONNECTION])
}
} }

View File

@ -9,6 +9,7 @@ import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.network.DeviceBlockEntity import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.FaceDeviceBlock
import net.shadowfacts.phycon.network.component.ItemStackPacketHandler import net.shadowfacts.phycon.network.component.ItemStackPacketHandler
import net.shadowfacts.phycon.network.component.NetworkStackProvider import net.shadowfacts.phycon.network.component.NetworkStackProvider
import net.shadowfacts.phycon.network.component.NetworkStackReceiver import net.shadowfacts.phycon.network.component.NetworkStackReceiver
@ -25,7 +26,7 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
NetworkStackReceiver { NetworkStackReceiver {
private val facing: Direction private val facing: Direction
get() = cachedState[InterfaceBlock.FACING] get() = cachedState[FaceDeviceBlock.FACING]
// todo: should this be a weak ref? // todo: should this be a weak ref?
private var inventory: GroupedItemInv? = null private var inventory: GroupedItemInv? = null

View File

@ -56,7 +56,6 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
private var observers = 0 private var observers = 0
val cachedNetItems = ItemStackCollections.intMap() val cachedNetItems = ItemStackCollections.intMap()
var cachedSortedNetItems = listOf<ItemStack>()
var netItemObserver: WeakReference<NetItemObserver>? = null var netItemObserver: WeakReference<NetItemObserver>? = null
@ -86,14 +85,12 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
private fun handleReadInventory(packet: ReadInventoryPacket) { private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory inventoryCache[packet.source] = packet.inventory
updateNetItems() updateAndSync()
sync()
} }
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source) inventoryCache.remove(packet.source)
updateNetItems() updateAndSync()
sync()
} }
private fun handleStackLocation(packet: StackLocationPacket) { private fun handleStackLocation(packet: StackLocationPacket) {
@ -113,12 +110,17 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
// this happens outside the normal update loop because by receiving the item stack packet // 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 // we "know" how much the count in the source inventory has changed
updateNetItems() updateAndSync()
sync()
return remaining return remaining
} }
private fun updateAndSync() {
updateNetItems()
sync()
netItemObserver?.get()?.netItemsChanged()
}
private fun updateNetItems() { private fun updateNetItems() {
cachedNetItems.clear() cachedNetItems.clear()
for (inventory in inventoryCache.values) { for (inventory in inventoryCache.values) {
@ -127,12 +129,6 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b } 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() { private fun beginInsertions() {
@ -175,21 +171,15 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
finishTimedOutPendingInsertions() finishTimedOutPendingInsertions()
} }
if (observers > 0) { if (observers > 0 && !world!!.isClient) {
if (world!!.isClient) { updateAndSync()
println(cachedNetItems)
} else {
updateNetItems()
sync()
}
} }
} }
} }
fun onActivate(player: PlayerEntity) { fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) { if (!world!!.isClient) {
updateNetItems() updateAndSync()
sync()
inventoryCache.clear() inventoryCache.clear()
sendPacket(RequestInventoryPacket(ipAddress)) sendPacket(RequestInventoryPacket(ipAddress))
@ -238,8 +228,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
internalBuffer.setStack(insertion.bufferSlot, remaining) internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateNetItems() updateAndSync()
sync()
return remaining return remaining
} }
@ -263,31 +252,11 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
override fun toClientTag(tag: CompoundTag): CompoundTag { override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.put("InternalBuffer", internalBuffer.toTag()) 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 return tag
} }
override fun fromClientTag(tag: CompoundTag) { override fun fromClientTag(tag: CompoundTag) {
internalBuffer.fromTag(tag.getCompound("InternalBuffer")) 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
}
netItemObserver?.get()?.netItemsChanged()
cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map {
val stack = it.key.copy()
stack.count = it.intValue
stack
}
} }
interface NetItemObserver { interface NetItemObserver {

View File

@ -5,6 +5,7 @@ import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.gui.widget.AbstractButtonWidget import net.minecraft.client.gui.widget.AbstractButtonWidget
import net.minecraft.client.gui.widget.AbstractPressableButtonWidget
import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.util.math.MatrixStack import net.minecraft.client.util.math.MatrixStack
@ -16,10 +17,14 @@ import net.minecraft.text.LiteralText
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW
import java.lang.NumberFormatException import java.lang.NumberFormatException
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
@ -32,6 +37,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
} }
private lateinit var searchBox: TextFieldWidget private lateinit var searchBox: TextFieldWidget
private lateinit var sortButton: SortButton
private lateinit var amountBox: TextFieldWidget private lateinit var amountBox: TextFieldWidget
private var dialogStack = ItemStack.EMPTY private var dialogStack = ItemStack.EMPTY
private var showingAmountDialog = false private var showingAmountDialog = false
@ -73,7 +79,12 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
searchBox.isVisible = true searchBox.isVisible = true
searchBox.setSelected(true) searchBox.setSelected(true)
searchBox.setEditableColor(0xffffff) searchBox.setEditableColor(0xffffff)
children.add(searchBox) addChild(searchBox)
sortButton = SortButton(x + 256, y + 0, handler.sortMode, {
requestUpdatedItems()
}, ::renderTooltip)
addButton(sortButton)
val dialogMinX = width / 2 - dialogWidth / 2 val dialogMinX = width / 2 - dialogWidth / 2
val dialogMinY = height / 2 - dialogHeight / 2 val dialogMinY = height / 2 - dialogHeight / 2
@ -130,6 +141,13 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
doDialogRequest() doDialogRequest()
} }
dialogChildren.add(request) dialogChildren.add(request)
requestUpdatedItems()
}
private fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode))
} }
override fun tick() { override fun tick() {
@ -213,10 +231,22 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
// don't unfocus the search box on mouse click // don't unfocus the search box on mouse click
searchBox.setSelected(true) searchBox.setSelected(true)
if (type == SlotActionType.PICKUP && clickData == 0 && slot != null && handler.isNetworkSlot(slot.id) && !slot.stack.isEmpty) { if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
dialogStack = slot.stack val stack = slot.stack
showingAmountDialog = true
searchBox.setSelected(false) if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
dialogStack = stack
showingAmountDialog = true
searchBox.setSelected(false)
}
}
} }
} }
@ -271,7 +301,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
val oldText = searchBox.text val oldText = searchBox.text
if (searchBox.charTyped(c, i)) { if (searchBox.charTyped(c, i)) {
if (searchBox.text != oldText) { if (searchBox.text != oldText) {
search() requestUpdatedItems()
} }
return true return true
} }
@ -299,7 +329,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
val oldText = searchBox.text val oldText = searchBox.text
if (searchBox.keyPressed(key, j, k)) { if (searchBox.keyPressed(key, j, k)) {
if (searchBox.text != oldText) { if (searchBox.text != oldText) {
search() requestUpdatedItems()
} }
return true return true
} }
@ -311,13 +341,15 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
} }
} }
private fun search() {
screenHandler.search(searchBox.text)
}
private fun doDialogRequest() { private fun doDialogRequest() {
showingAmountDialog = false showingAmountDialog = false
handler.requestItem(client!!.player!!, dialogStack, amountBox.intValue) requestItem(dialogStack, amountBox.intValue)
}
private fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
} }
private var TextFieldWidget.intValue: Int private var TextFieldWidget.intValue: Int
@ -343,4 +375,58 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
} }
} }
class SortButton(
x: Int,
y: Int,
var mode: SortMode,
val onChange: (SortMode) -> Unit,
val doRenderTooltip: (MatrixStack, Text, Int, Int) -> Unit
): AbstractPressableButtonWidget(x, y, 20, 20, LiteralText("")) {
override fun onPress() {}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if ((button == 0 || button == 1) && clicked(mouseX, mouseY)) {
val newVal = if (button == 0) mode.next else mode.prev
mode = newVal
onChange(mode)
playDownSound(MinecraftClient.getInstance().soundManager)
return true
}
return false
}
override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val client = MinecraftClient.getInstance()
RenderSystem.color4f(1f, 1f, 1f, 1f)
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.enableDepthTest()
client.textureManager.bindTexture(WIDGETS_LOCATION)
val k = getYImage(isHovered)
drawTexture(matrixStack, x, y, 0, 46 + k * 20, width / 2, height)
drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, 46 + k * 20, width / 2, height)
client.textureManager.bindTexture(BACKGROUND)
val u: Int = when (mode) {
SortMode.COUNT_HIGH_FIRST -> 0
SortMode.COUNT_LOW_FIRST -> 16
SortMode.ALPHABETICAL -> 32
}
drawTexture(matrixStack, x + 2, y + 2, u, 230, 16, 16)
if (isHovered) {
renderToolTip(matrixStack, mouseX, mouseY)
}
}
override fun renderToolTip(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
val text = LiteralText("")
text.append("Sort by: ")
text.append(mode.tooltip)
doRenderTooltip(matrixStack, text, mouseX, mouseY)
}
}
} }

View File

@ -8,12 +8,15 @@ import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
@ -21,7 +24,7 @@ import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId), class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId),
TerminalBlockEntity.NetItemObserver { TerminalBlockEntity.NetItemObserver {
companion object { companion object {
@ -30,6 +33,19 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
private val fakeInv = FakeInventory(this) private val fakeInv = FakeInventory(this)
private var searchQuery: String = "" private var searchQuery: String = ""
var sortMode = SortMode.COUNT_HIGH_FIRST
private set
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
val stack = it.stack.copy()
stack.count = it.amount
stack
}
}
}
var itemsForDisplay = listOf<ItemStack>() var itemsForDisplay = listOf<ItemStack>()
private set private set
@ -37,8 +53,10 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!) this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!)
init { init {
terminal.netItemObserver = WeakReference(this) if (!terminal.world!!.isClient) {
netItemsChanged() terminal.netItemObserver = WeakReference(this)
netItemsChanged()
}
// network // network
for (y in 0 until 6) { for (y in 0 until 6) {
@ -67,7 +85,10 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
} }
override fun netItemsChanged() { override fun netItemsChanged() {
itemsForDisplay = terminal.cachedNetItems.object2IntEntrySet().filter { val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) { if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1) val unprefixed = searchQuery.drop(1)
@ -77,20 +98,34 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
} }
} }
it.key.name.string.contains(searchQuery, true) it.key.name.string.contains(searchQuery, true)
}.sortedByDescending {
it.intValue
}.map {
val stack = it.key.copy()
stack.count = it.intValue
stack
} }
val sorted =
when (sortMode) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, sortMode))
} }
fun search(query: String) { fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, sortMode: SortMode) {
searchQuery = query this.searchQuery = query
this.sortMode = sortMode
netItemsChanged() netItemsChanged()
} }
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, sortMode: SortMode) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.sortMode = sortMode
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean { override fun canUse(player: PlayerEntity): Boolean {
return true return true
} }
@ -102,28 +137,7 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
} }
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack { override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (isNetworkSlot(slotId)) { if (isBufferSlot(slotId)) {
// the slot clicked was one of the network stacks
if (actionType == SlotActionType.QUICK_MOVE) {
val stack = slots[slotId].stack
if (!stack.isEmpty && player.world.isClient) {
requestItem(player, stack, min(stack.count, stack.maxCount))
}
} else if (actionType == SlotActionType.PICKUP && clickData == 1) {
if (clickData == 1) {
// right click, request half stack
val stack = slots[slotId].stack
if (!stack.isEmpty && player.world.isClient) {
requestItem(player, stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
}
} else {
// todo: left click, show amount dialog
}
}
return ItemStack.EMPTY
} else if (isBufferSlot(slotId)) {
// internal buffer
// todo: why does this think it's quick_craft sometimes? // todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) { if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) {
// placing cursor stack into buffer // placing cursor stack into buffer
@ -134,13 +148,6 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
return super.onSlotClick(slotId, clickData, actionType, player) return super.onSlotClick(slotId, clickData, actionType, player)
} }
fun requestItem(player: PlayerEntity, stack: ItemStack, amount: Int) {
if (!player.world.isClient) return
val handler = (player as ClientPlayerEntity).networkHandler
val packet = C2STerminalRequestItem(terminal, stack, amount)
handler.sendPacket(packet)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) { if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY; return ItemStack.EMPTY;
@ -154,7 +161,8 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
val result = slot.stack.copy() val result = slot.stack.copy()
if (isBufferSlot(slotId)) { if (isBufferSlot(slotId)) {
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, false)) { // last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY return ItemStack.EMPTY
} }
if (slot.stack.isEmpty) { if (slot.stack.isEmpty) {
@ -204,4 +212,6 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
} }

View File

@ -0,0 +1,50 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.PacketSender
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.minecraft.server.MinecraftServer
import net.minecraft.server.network.ServerPlayNetworkHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object C2STerminalUpdateDisplayedItems: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed")
operator fun invoke(terminal: TerminalBlockEntity, query: String, sortMode: SortMode): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
buf.writeString(query)
buf.writeVarInt(sortMode.ordinal)
return ClientPlayNetworking.createC2SPacket(CHANNEL, buf)
}
override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val query = buf.readString()
val sortMode = SortMode.values()[buf.readVarInt()]
server.execute {
if (player.world.registryKey.value != dimID) return@execute
val screenHandler = player.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.sendUpdatedItemsToClient(player, query, sortMode)
}
}
}

View File

@ -0,0 +1,11 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.minecraft.util.Identifier
/**
* @author shadowfacts
*/
interface ClientReceiver: ClientPlayNetworking.PlayChannelHandler {
val CHANNEL: Identifier
}

View File

@ -0,0 +1,57 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.PacketSender
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.minecraft.client.MinecraftClient
import net.minecraft.client.network.ClientPlayNetworkHandler
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object S2CTerminalUpdateDisplayedItems: ClientReceiver {
override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL
operator fun invoke(terminal: TerminalBlockEntity, entries: List<TerminalScreenHandler.Entry>, query: String, sortMode: SortMode): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
buf.writeVarInt(entries.size)
for (e in entries) {
buf.writeItemStack(e.stack)
buf.writeVarInt(e.amount)
}
buf.writeString(query)
buf.writeVarInt(sortMode.ordinal)
return ServerPlayNetworking.createS2CPacket(CHANNEL, buf)
}
override fun receive(client: MinecraftClient, handler: ClientPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val entryCount = buf.readVarInt()
val entries = ArrayList<TerminalScreenHandler.Entry>(entryCount)
for (i in 0 until entryCount) {
entries.add(TerminalScreenHandler.Entry(buf.readItemStack(), buf.readVarInt()))
}
val query = buf.readString()
val sortMode = SortMode.values()[buf.readVarInt()]
client.execute {
if (client.player!!.world.registryKey.value != dimID) return@execute
val screenHandler = client.player!!.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.receivedUpdatedItemsFromServer(entries, query, sortMode)
}
}
}

View File

@ -0,0 +1,27 @@
package net.shadowfacts.phycon.util
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
/**
* @author shadowfacts
*/
enum class SortMode {
COUNT_HIGH_FIRST,
COUNT_LOW_FIRST,
ALPHABETICAL;
val prev: SortMode
get() = values()[(ordinal - 1 + values().size) % values().size]
val next: SortMode
get() = values()[(ordinal + 1) % values().size]
val tooltip: Text
get() = when (this) {
COUNT_HIGH_FIRST -> LiteralText("Count, highest first")
COUNT_LOW_FIRST -> LiteralText("Count, lowest first")
ALPHABETICAL -> LiteralText("Name")
}
}

View File

@ -140,7 +140,6 @@
}, },
{ {
"when": {"cable_connection": "down", "facing": "east"}, "when": {"cable_connection": "down", "facing": "east"},
"apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 270 } "apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 270 }
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB