Extract terminal stuff to abstract base classes

This commit is contained in:
Shadowfacts 2021-03-27 10:22:18 -04:00
parent 9418ff8917
commit d18b8a8a08
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
23 changed files with 1034 additions and 911 deletions

View File

@ -7,17 +7,17 @@ import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.slot.Slot;
import net.minecraft.text.Text;
import net.shadowfacts.phycon.block.terminal.TerminalScreen;
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.spongepowered.asm.mixin.Mixin;
/**
* @author shadowfacts
*/
@Mixin(TerminalScreen.class)
public abstract class MixinTerminalScreen extends HandledScreen<TerminalScreenHandler> implements ISpecialScrollableScreen, IContainerScreen {
@Mixin(AbstractTerminalScreen.class)
public abstract class MixinTerminalScreen extends HandledScreen<AbstractTerminalScreenHandler> implements ISpecialScrollableScreen, IContainerScreen {
private MixinTerminalScreen(TerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) {
private MixinTerminalScreen(AbstractTerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) {
super(screenHandler, playerInventory, text);
}

View File

@ -8,7 +8,7 @@ import me.shedaniel.rei.api.plugins.REIPluginV0
import net.fabricmc.api.ClientModInitializer
import net.minecraft.client.MinecraftClient
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.block.terminal.TerminalScreen
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen
/**
* @author shadowfacts
@ -17,7 +17,7 @@ object PhyConPlugin: ClientModInitializer, REIPluginV0 {
const val MODID = "phycon_rei"
override fun onInitializeClient() {
TerminalScreen.registerClickHandler { mouseX, mouseY, button ->
AbstractTerminalScreen.registerClickHandler { mouseX, mouseY, button ->
REIHelper.getInstance().searchTextField?.also {
if (it.bounds.contains(mouseX, mouseY)) {
this.terminalVC.searchField.resignFirstResponder()
@ -32,8 +32,8 @@ object PhyConPlugin: ClientModInitializer, REIPluginV0 {
override fun getPluginIdentifier() = Identifier(MODID, "rei_plugin")
override fun registerBounds(helper: DisplayHelper) {
BaseBoundsHandler.getInstance().registerExclusionZones(TerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as TerminalScreen
BaseBoundsHandler.getInstance().registerExclusionZones(AbstractTerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as AbstractTerminalScreen<*, *>
val view = screen.terminalVC.settingsView
val rect = view.convert(view.bounds, to = null)
listOf(

View File

@ -6,8 +6,8 @@ import net.minecraft.client.render.item.ItemRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.shadowfacts.phycon.block.terminal.TerminalScreen;
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -26,8 +26,8 @@ public class MixinHandledScreen {
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableDepthTest()V")
)
private void drawSlotUnderlay(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;
if ((Object)this instanceof AbstractTerminalScreen) {
AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
self.drawSlotUnderlay(matrixStack, slot);
}
}
@ -37,9 +37,9 @@ public class MixinHandledScreen {
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/ItemRenderer;renderGuiItemOverlay(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V")
)
private void drawSlotAmount(ItemRenderer itemRenderer, TextRenderer textRenderer, ItemStack stack, int x, int y, @Nullable String countLabel, MatrixStack matrixStack, Slot slot) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;
TerminalScreenHandler handler = self.getScreenHandler();
if ((Object)this instanceof AbstractTerminalScreen) {
AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
AbstractTerminalScreenHandler<?> handler = self.getScreenHandler();
if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) {
self.drawNetworkSlotAmount(stack, x, y, slot, matrixStack);
return;

View File

@ -16,7 +16,7 @@ import net.minecraft.world.World
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode
@ -39,7 +39,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
private val invProxy = MinerInvProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT
override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this)
override var providerPriority = 0

View File

@ -0,0 +1,75 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import net.minecraft.util.ItemScatterer
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/**
* @author shadowfacts
*/
abstract class AbstractTerminalBlock<T: AbstractTerminalBlockEntity>: DeviceBlock<T>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
companion object {
val FACING = Properties.FACING
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING])
return set
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return defaultState.with(FACING, context.playerLookDirection.opposite)
}
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult {
getBlockEntity(world, pos)!!.onActivate(player)
return ActionResult.SUCCESS
}
override fun onStateReplaced(state: BlockState, world: World, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.isOf(newState.block)) {
val be = getBlockEntity(world, pos)!!
be.dropItems()
super.onStateReplaced(state, world, pos, newState, moved)
}
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos))
}
}

View File

@ -0,0 +1,263 @@
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.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.block.entity.BlockEntityType
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.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText
import net.minecraft.util.ItemScatterer
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.block.DeviceBlockEntity
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBlockEntity(type),
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
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)
private 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 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
updateAndSync()
return remaining
}
protected fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
sync()
// 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) { 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))
}
private 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) {
markDirty()
sync()
}
}
open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer)
}
override fun toCommonTag(tag: CompoundTag) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
}
override fun fromCommonTag(tag: CompoundTag) {
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>()
}
data 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 >= AbstractTerminalBlockEntity.LOCATE_REQUEST_TIMEOUT
}
}
}

View File

@ -0,0 +1,200 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.Element
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.LinkedList
import kotlin.math.ceil
import kotlin.math.min
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreen<BE: AbstractTerminalBlockEntity, T: AbstractTerminalScreenHandler<BE>>(
handler: T,
playerInv: PlayerInventory,
title: Text,
val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int,
): CacaoHandledScreen<T>(handler, playerInv, title) {
companion object {
private val clickHandlers = LinkedList<AbstractTerminalScreen<*, *>.(Double, Double, Int) -> Boolean?>()
fun registerClickHandler(handler: AbstractTerminalScreen<*, *>.(Double, Double, Int) -> Boolean?) {
clickHandlers.add(handler)
}
}
abstract val backgroundTexture: Identifier
val terminalVC: AbstractTerminalViewController<*, *, *>
var amountVC: TerminalRequestAmountViewController? = null
var searchQuery = ""
var scrollPosition = 0.0
init {
backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight
terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int, slot: Slot, matrixStack: MatrixStack) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
matrixStack.push()
matrixStack.translate(x.toDouble(), y.toDouble(), itemRenderer.zOffset + 200.0)
val scale = 2 / 3f
matrixStack.scale(scale, scale, 1.0f)
val immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().buffer)
val textX = (1 / scale * 18) - textRenderer.getWidth(s).toFloat() - 3
val textY = (1 / scale * 18) - 11
textRenderer.draw(s, textX, textY, 0xffffff, true, matrixStack.peek().model, immediate, false, 0, 0xF000F0)
immediate.draw()
matrixStack.pop()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
drawBackgroundTexture(matrixStack)
}
open fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(backgroundTexture)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun tick() {
super.tick()
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
for (handler in clickHandlers) {
val res = handler(mouseX, mouseY, button)
if (res != null) {
return res
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
val stack = slot.stack
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 {
showRequestAmountDialog(stack)
}
}
}
}
private val fakeFocusedElement = TextFieldWidget(textRenderer, 0, 0, 0, 0, LiteralText(""))
override fun getFocused(): Element? {
return if (windows.last().firstResponder != null) {
fakeFocusedElement
} else {
null
}
}
}

View File

@ -0,0 +1,250 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.ScreenHandlerType
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.DefaultPlugin
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreenHandler<T: AbstractTerminalBlockEntity>(
handlerType: ScreenHandlerType<*>,
syncId: Int,
val playerInv: PlayerInventory,
val terminal: T,
): ScreenHandler(handlerType, syncId),
AbstractTerminalBlockEntity.NetItemObserver {
private val rowsDisplayed = 6
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, 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 netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
it.key.name.string.contains(searchQuery, true)
}
totalEntries = filtered.size
val sorted =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
// itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries))
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val result = slot.stack.copy()
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) }
if (slot.stack.isEmpty) {
return ItemStack.EMPTY
}
}
return result
}
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
slot.markDirty()
slotsInsertedInto.add(index)
} else if (canStacksCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
}

View File

@ -0,0 +1,181 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.MathHelper
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.LayoutGuide
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.client.screen.ScrollTrackView
import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
abstract class AbstractTerminalViewController<BE: AbstractTerminalBlockEntity, S: AbstractTerminalScreen<BE, H>, H: AbstractTerminalScreenHandler<BE>>(
val screen: S,
val handler: H,
val terminal: BE = handler.terminal,
): ViewController() {
private lateinit var scrollTrack: ScrollTrackView
lateinit var settingsView: View
private set
lateinit var searchField: TextField
private set
lateinit var pane: LayoutGuide
private set
lateinit var buffer: LayoutGuide
private set
lateinit var network: LayoutGuide
private set
lateinit var playerInv: LayoutGuide
private set
lateinit var networkLabel: View
private set
lateinit var playerInvLabel: View
private set
lateinit var bufferLabel: View
private set
override fun loadView() {
view = ScrollHandlingView(this)
}
override fun viewDidLoad() {
super.viewDidLoad()
pane = view.addLayoutGuide()
view.solver.dsl {
pane.centerXAnchor equalTo view.centerXAnchor
pane.centerYAnchor equalTo view.centerYAnchor
pane.widthAnchor equalTo screen.terminalBackgroundWidth
pane.heightAnchor equalTo screen.terminalBackgroundHeight
}
buffer = view.addLayoutGuide()
view.solver.dsl {
buffer.leftAnchor equalTo (pane.leftAnchor + 7 + handler.xOffset)
buffer.topAnchor equalTo (pane.topAnchor + 17)
buffer.widthAnchor equalTo (18 * 3)
buffer.heightAnchor equalTo (18 * 6)
}
network = view.addLayoutGuide()
view.solver.dsl {
network.leftAnchor equalTo (pane.leftAnchor + 65 + handler.xOffset)
network.topAnchor equalTo buffer.topAnchor
network.widthAnchor equalTo (18 * 9)
network.heightAnchor equalTo (18 * 6)
}
playerInv = view.addLayoutGuide()
view.solver.dsl {
playerInv.leftAnchor equalTo network.leftAnchor
playerInv.topAnchor equalTo (pane.topAnchor + 139)
playerInv.widthAnchor equalTo (18 * 9)
playerInv.heightAnchor equalTo 76
}
networkLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_network"))).apply {
textColor = Color.TEXT
}
playerInvLabel = view.addSubview(Label(handler.playerInv.displayName)).apply {
textColor = Color.TEXT
}
bufferLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_buffer"))).apply {
textColor = Color.TEXT
}
searchField = view.addSubview(TerminalSearchField()).apply {
handler = ::searchFieldChanged
drawBackground = false
}
searchField.becomeFirstResponder()
scrollTrack = view.addSubview(ScrollTrackView(::scrollPositionChanged))
val settingsStack = view.addSubview(StackView(Axis.VERTICAL, spacing = 2.0))
settingsView = settingsStack
TerminalSettings.allKeys.forEach { key ->
val button = SettingButton(key)
button.handler = { settingsChanged() }
settingsStack.addArrangedSubview(button)
}
view.solver.dsl {
networkLabel.leftAnchor equalTo network.leftAnchor
networkLabel.topAnchor equalTo (pane.topAnchor + 6)
bufferLabel.leftAnchor equalTo buffer.leftAnchor
bufferLabel.topAnchor equalTo networkLabel.topAnchor
playerInvLabel.leftAnchor equalTo playerInv.leftAnchor
playerInvLabel.topAnchor equalTo (pane.topAnchor + 128)
searchField.leftAnchor equalTo (pane.leftAnchor + 138 + handler.xOffset)
searchField.topAnchor equalTo (pane.topAnchor + 5)
searchField.widthAnchor equalTo 80
searchField.heightAnchor equalTo 9
scrollTrack.leftAnchor equalTo (pane.leftAnchor + 232 + handler.xOffset)
scrollTrack.topAnchor equalTo (network.topAnchor + 1)
scrollTrack.bottomAnchor equalTo (network.bottomAnchor - 1)
scrollTrack.widthAnchor equalTo 12
settingsStack.leftAnchor equalTo (pane.rightAnchor + 4)
settingsStack.topAnchor equalTo pane.topAnchor
}
}
private fun searchFieldChanged(field: TextField) {
screen.searchQuery = field.text
screen.requestUpdatedItems()
}
private fun scrollPositionChanged(track: ScrollTrackView) {
val oldOffset = handler.currentScrollOffsetInRows()
handler.scrollPosition = track.scrollPosition.toFloat()
screen.scrollPosition = track.scrollPosition
if (handler.currentScrollOffsetInRows() != oldOffset) {
screen.requestUpdatedItems()
}
}
private fun settingsChanged() {
screen.requestUpdatedItems()
}
class TerminalSearchField: TextField("") {
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
// no-op
}
}
class ScrollHandlingView(val vc: AbstractTerminalViewController<*, *, *>): View() {
override fun mouseScrolled(point: Point, amount: Double): Boolean {
var newOffsetInRows = vc.handler.currentScrollOffsetInRows() - amount.toInt()
newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, vc.handler.maxScrollOffsetInRows())
if (newOffsetInRows != vc.handler.currentScrollOffsetInRows()) {
val newScrollPosition = newOffsetInRows / vc.handler.maxScrollOffsetInRows().toDouble()
vc.screen.scrollPosition = newScrollPosition
vc.scrollTrack.scrollPosition = newScrollPosition
vc.screen.requestUpdatedItems()
}
return true
}
}
}

View File

@ -1,80 +1,18 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.server.world.ServerWorld
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import net.minecraft.util.ItemScatterer
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/**
* @author shadowfacts
*/
class TerminalBlock: DeviceBlock<TerminalBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
class TerminalBlock: AbstractTerminalBlock<TerminalBlockEntity>() {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
val FACING = Properties.FACING
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING])
return set
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return defaultState.with(FACING, context.playerLookDirection.opposite)
}
override fun createBlockEntity(world: BlockView) = TerminalBlockEntity()
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult {
getBlockEntity(world, pos)!!.onActivate(player)
return ActionResult.SUCCESS
}
override fun onStateReplaced(state: BlockState, world: World, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.isOf(newState.block)) {
val be = getBlockEntity(world, pos)!!
ItemScatterer.spawn(world, pos, be.internalBuffer)
super.onStateReplaced(state, world, pos, newState, moved)
}
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos))
}
}

View File

@ -1,175 +1,21 @@
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.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
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.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText
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.block.DeviceBlockEntity
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
import net.shadowfacts.phycon.packet.RequestInventoryPacket
/**
* @author shadowfacts
*/
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
ItemStackPacketHandler,
NetworkStackDispatcher<TerminalBlockEntity.PendingInsertion> {
class TerminalBlockEntity: AbstractTerminalBlockEntity(PhyBlockEntities.TERMINAL) {
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
}
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
private 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 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
updateAndSync()
return remaining
}
private fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
sync()
// 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) { 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()
}
}
fun onActivate(player: PlayerEntity) {
override fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) {
updateAndSync()
@ -190,85 +36,4 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
}
}
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))
}
private 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) {
markDirty()
sync()
}
}
override fun toCommonTag(tag: CompoundTag) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
}
override fun fromCommonTag(tag: CompoundTag) {
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>()
}
}
data 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 >= TerminalBlockEntity.LOCATE_REQUEST_TIMEOUT
}
}

View File

@ -23,7 +23,7 @@ class TerminalFakeSlot(fakeInv: FakeInventory, slot: Int, x: Int, y: Int): Slot(
}
class FakeInventory(val screenHandler: TerminalScreenHandler): Inventory {
class FakeInventory(val screenHandler: AbstractTerminalScreenHandler<*>): Inventory {
override fun getStack(slot: Int): ItemStack {
if (slot >= screenHandler.itemsForDisplay.size) return ItemStack.EMPTY
return screenHandler.itemsForDisplay[slot]

View File

@ -19,7 +19,7 @@ import kotlin.math.floor
* @author shadowfacts
*/
class TerminalRequestAmountViewController(
val screen: TerminalScreen,
val screen: AbstractTerminalScreen<*, *>,
val stack: ItemStack,
): ViewController() {

View File

@ -1,194 +1,33 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.Element
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.LinkedList
import kotlin.math.ceil
import kotlin.math.min
/**
* @author shadowfacts
*/
class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): CacaoHandledScreen<TerminalScreenHandler>(handler, playerInv, title) {
class TerminalScreen(
handler: TerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<TerminalBlockEntity, TerminalScreenHandler>(
handler,
playerInv,
title,
252,
222
) {
companion object {
private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png")
private val clickHandlers = LinkedList<TerminalScreen.(Double, Double, Int) -> Boolean?>()
fun registerClickHandler(handler: TerminalScreen.(Double, Double, Int) -> Boolean?) {
clickHandlers.add(handler)
}
}
val backgroundWidth: Int
get() = backgroundWidth
val backgroundHeight: Int
get() = backgroundHeight
override val backgroundTexture = BACKGROUND
val terminalVC = TerminalViewController(this, handler, handler.terminal)
var amountVC: TerminalRequestAmountViewController? = null
var searchQuery = ""
var scrollPosition = 0.0
init {
backgroundWidth = 252
backgroundHeight = 222
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int, slot: Slot, matrixStack: MatrixStack) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
matrixStack.push()
matrixStack.translate(x.toDouble(), y.toDouble(), itemRenderer.zOffset + 200.0)
val scale = 2 / 3f
matrixStack.scale(scale, scale, 1.0f)
val immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().buffer)
val textX = (1 / scale * 18) - textRenderer.getWidth(s).toFloat() - 3
val textY = (1 / scale * 18) - 11
textRenderer.draw(s, textX, textY, 0xffffff, true, matrixStack.peek().model, immediate, false, 0, 0xF000F0)
immediate.draw()
matrixStack.pop()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun tick() {
super.tick()
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
for (handler in clickHandlers) {
val res = handler(mouseX, mouseY, button)
if (res != null) {
return res
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
val stack = slot.stack
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 {
showRequestAmountDialog(stack)
}
}
}
}
private val fakeFocusedElement = TextFieldWidget(textRenderer, 0, 0, 0, 0, LiteralText(""))
override fun getFocused(): Element? {
return if (windows.last().firstResponder != null) {
fakeFocusedElement
} else {
null
}
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return TerminalViewController(this, handler)
}
}

View File

@ -1,62 +1,18 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.DefaultPlugin
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
class TerminalScreenHandler(
syncId: Int,
val playerInv: PlayerInventory,
val terminal: TerminalBlockEntity,
): ScreenHandler(PhyScreens.TERMINAL, syncId),
TerminalBlockEntity.NetItemObserver {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
}
private val rowsDisplayed = 6
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
playerInv: PlayerInventory,
terminal: TerminalBlockEntity,
): AbstractTerminalScreenHandler<TerminalBlockEntity>(PhyScreens.TERMINAL, syncId, playerInv, terminal) {
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this(
@ -65,195 +21,4 @@ class TerminalScreenHandler(
PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
)
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, 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 netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
it.key.name.string.contains(searchQuery, true)
}
totalEntries = filtered.size
val sorted =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
// itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries))
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val result = slot.stack.copy()
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) }
if (slot.stack.isEmpty) {
return ItemStack.EMPTY
}
}
return result
}
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
slot.markDirty()
slotsInsertedInto.add(index)
} else if (canStacksCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
}

View File

@ -1,164 +1,13 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.MathHelper
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.client.screen.ScrollTrackView
import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
class TerminalViewController(
val screen: TerminalScreen,
val handler: TerminalScreenHandler,
val terminal: TerminalBlockEntity,
): ViewController() {
private lateinit var scrollTrack: ScrollTrackView
lateinit var settingsView: View
private set
lateinit var searchField: TextField
private set
override fun loadView() {
view = ScrollHandlingView(this)
}
override fun viewDidLoad() {
super.viewDidLoad()
val pane = view.addLayoutGuide()
view.solver.dsl {
pane.centerXAnchor equalTo view.centerXAnchor
pane.centerYAnchor equalTo view.centerYAnchor
pane.widthAnchor equalTo screen.backgroundWidth
pane.heightAnchor equalTo screen.backgroundHeight
}
val buffer = view.addLayoutGuide()
view.solver.dsl {
buffer.leftAnchor equalTo (pane.leftAnchor + 7)
buffer.topAnchor equalTo (pane.topAnchor + 17)
buffer.widthAnchor equalTo (18 * 3)
buffer.heightAnchor equalTo (18 * 6)
}
val network = view.addLayoutGuide()
view.solver.dsl {
network.leftAnchor equalTo (pane.leftAnchor + 65)
network.topAnchor equalTo buffer.topAnchor
network.widthAnchor equalTo (18 * 9)
network.heightAnchor equalTo (18 * 6)
}
val playerInv = view.addLayoutGuide()
view.solver.dsl {
playerInv.leftAnchor equalTo network.leftAnchor
playerInv.topAnchor equalTo (pane.topAnchor + 139)
playerInv.widthAnchor equalTo (18 * 9)
playerInv.heightAnchor equalTo 76
}
val titleLabel = view.addSubview(Label(screen.title)).apply {
textColor = Color.TEXT
}
val playerInvLabel = view.addSubview(Label(handler.playerInv.displayName)).apply {
textColor = Color.TEXT
}
val bufferLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_buffer"))).apply {
textColor = Color.TEXT
}
searchField = view.addSubview(TerminalSearchField()).apply {
handler = ::searchFieldChanged
drawBackground = false
}
searchField.becomeFirstResponder()
scrollTrack = view.addSubview(ScrollTrackView(::scrollPositionChanged))
val settingsStack = view.addSubview(StackView(Axis.VERTICAL, spacing = 2.0))
settingsView = settingsStack
TerminalSettings.allKeys.forEach { key ->
val button = SettingButton(key)
button.handler = { settingsChanged() }
settingsStack.addArrangedSubview(button)
}
view.solver.dsl {
titleLabel.leftAnchor equalTo network.leftAnchor
titleLabel.topAnchor equalTo (pane.topAnchor + 6)
bufferLabel.leftAnchor equalTo buffer.leftAnchor
bufferLabel.topAnchor equalTo titleLabel.topAnchor
playerInvLabel.leftAnchor equalTo playerInv.leftAnchor
playerInvLabel.topAnchor equalTo (pane.bottomAnchor - 94)
searchField.leftAnchor equalTo (pane.leftAnchor + 138)
searchField.topAnchor equalTo (pane.topAnchor + 5)
searchField.widthAnchor equalTo 80
searchField.heightAnchor equalTo 9
scrollTrack.leftAnchor equalTo (pane.leftAnchor + 232)
scrollTrack.topAnchor equalTo (network.topAnchor + 1)
scrollTrack.bottomAnchor equalTo (network.bottomAnchor - 1)
scrollTrack.widthAnchor equalTo 12
settingsStack.leftAnchor equalTo (pane.rightAnchor + 4)
settingsStack.topAnchor equalTo pane.topAnchor
}
}
private fun searchFieldChanged(field: TextField) {
screen.searchQuery = field.text
screen.requestUpdatedItems()
}
private fun scrollPositionChanged(track: ScrollTrackView) {
val oldOffset = handler.currentScrollOffsetInRows()
handler.scrollPosition = track.scrollPosition.toFloat()
screen.scrollPosition = track.scrollPosition
if (handler.currentScrollOffsetInRows() != oldOffset) {
screen.requestUpdatedItems()
}
}
private fun settingsChanged() {
screen.requestUpdatedItems()
}
class TerminalSearchField: TextField("") {
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
// no-op
}
}
class ScrollHandlingView(val vc: TerminalViewController): View() {
override fun mouseScrolled(point: Point, amount: Double): Boolean {
var newOffsetInRows = vc.handler.currentScrollOffsetInRows() - amount.toInt()
newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, vc.handler.maxScrollOffsetInRows())
if (newOffsetInRows != vc.handler.currentScrollOffsetInRows()) {
val newScrollPosition = newOffsetInRows / vc.handler.maxScrollOffsetInRows().toDouble()
vc.screen.scrollPosition = newScrollPosition
vc.scrollTrack.scrollPosition = newScrollPosition
vc.screen.requestUpdatedItems()
}
return true
}
}
screen: TerminalScreen,
handler: TerminalScreenHandler,
): AbstractTerminalViewController<TerminalBlockEntity, TerminalScreen, TerminalScreenHandler>(
screen,
handler,
) {
}

View File

@ -19,7 +19,7 @@ import net.minecraft.util.math.Direction
import net.minecraft.world.BlockRenderView
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlock
import java.util.Random
import java.util.function.Function
import java.util.function.Supplier
@ -112,7 +112,7 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
randomSupplier: Supplier<Random>,
context: RenderContext
) {
val mesh = meshes[state[TerminalBlock.FACING].ordinal]
val mesh = meshes[state[AbstractTerminalBlock.FACING].ordinal]
context.meshConsumer().accept(mesh)
}

View File

@ -7,7 +7,6 @@ import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.item.ConsoleItem
import net.shadowfacts.phycon.item.ScrewdriverItem
import net.shadowfacts.phycon.block.cable.CableBlock
import net.shadowfacts.phycon.block.extractor.ExtractorBlock
import net.shadowfacts.phycon.block.inserter.InserterBlock
import net.shadowfacts.phycon.block.miner.MinerBlock

View File

@ -4,6 +4,7 @@ import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry
import net.minecraft.screen.ScreenHandlerType
import net.shadowfacts.phycon.block.inserter.InserterScreenHandler
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreenHandler
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
object PhyScreens {
@ -16,7 +17,7 @@ object PhyScreens {
private set
fun init() {
TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalScreenHandler.ID, ::TerminalScreenHandler)
TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalBlock.ID, ::TerminalScreenHandler)
INSERTER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler)
REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler)
}

View File

@ -13,8 +13,7 @@ import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.minecraft.util.registry.RegistryKey
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.util.copyWithCount
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.util.readItemStackWithoutCount
import net.shadowfacts.phycon.util.writeItemStackWithoutCount
@ -25,7 +24,7 @@ object C2STerminalRequestItem: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_request_item")
operator fun invoke(terminal: TerminalBlockEntity, stack: ItemStack, amount: Int): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, stack: ItemStack, amount: Int): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
@ -48,7 +47,7 @@ object C2STerminalRequestItem: ServerReceiver {
server.execute {
val key = RegistryKey.of(Registry.DIMENSION, dimID)
val world = server.getWorld(key) ?: return@execute
val terminal = world.getBlockEntity(pos) as? TerminalBlockEntity ?: return@execute
val terminal = world.getBlockEntity(pos) as? AbstractTerminalBlockEntity ?: return@execute
terminal.requestItem(stack, amount)
}
}

View File

@ -11,9 +11,8 @@ import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.TerminalSettings
/**
@ -23,7 +22,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed")
operator fun invoke(terminal: TerminalBlockEntity, query: String, scrollPosition: Float): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, query: String, scrollPosition: Float): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -47,7 +46,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
server.execute {
if (player.world.registryKey.value != dimID) return@execute
val screenHandler = player.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.sendUpdatedItemsToClient(player, query, settings, scrollPosition)
}

View File

@ -8,9 +8,8 @@ import net.minecraft.client.network.ClientPlayNetworkHandler
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.readItemStackWithoutCount
import net.shadowfacts.phycon.util.writeItemStackWithoutCount
@ -21,7 +20,7 @@ import net.shadowfacts.phycon.util.writeItemStackWithoutCount
object S2CTerminalUpdateDisplayedItems: ClientReceiver {
override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL
operator fun invoke(terminal: TerminalBlockEntity, entries: List<TerminalScreenHandler.Entry>, query: String, settings: TerminalSettings, scrollPosition: Float, totalEntries: Int): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, entries: List<AbstractTerminalScreenHandler.Entry>, query: String, settings: TerminalSettings, scrollPosition: Float, totalEntries: Int): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -45,9 +44,9 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val entryCount = buf.readVarInt()
val entries = ArrayList<TerminalScreenHandler.Entry>(entryCount)
val entries = ArrayList<AbstractTerminalScreenHandler.Entry>(entryCount)
for (i in 0 until entryCount) {
entries.add(TerminalScreenHandler.Entry(buf.readItemStackWithoutCount(), buf.readVarInt()))
entries.add(AbstractTerminalScreenHandler.Entry(buf.readItemStackWithoutCount(), buf.readVarInt()))
}
val query = buf.readString()
PhysicalConnectivityClient.terminalSettings.fromTag(buf.readCompoundTag()!!)
@ -57,7 +56,7 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
client.execute {
if (client.player!!.world.registryKey.value != dimID) return@execute
val screenHandler = client.player!!.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.receivedUpdatedItemsFromServer(entries, query, scrollPosition, totalEntries)
}

View File

@ -34,6 +34,7 @@
"item.phycon.redstone_processor": "Redstone Processor",
"gui.phycon.terminal_buffer": "Buffer",
"gui.phycon.terminal_network": "Network",
"gui.phycon.console.details": "Device Details",
"gui.phycon.console.details.ip": "IP Address: %s",
"gui.phycon.console.details.mac": "MAC Address: %s",