From c15700bf5d090175190ab65641f86c16d05f3d44 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 27 Mar 2021 10:22:18 -0400 Subject: [PATCH] Extract terminal stuff to abstract base classes --- .../mixin/MixinTerminalScreen.java | 10 +- .../phycon/plugin/rei/PhyConPlugin.kt | 8 +- .../mixin/client/MixinHandledScreen.java | 14 +- .../phycon/block/miner/MinerBlockEntity.kt | 4 +- .../block/terminal/AbstractTerminalBlock.kt | 75 +++++ .../terminal/AbstractTerminalBlockEntity.kt | 263 ++++++++++++++++++ .../block/terminal/AbstractTerminalScreen.kt | 200 +++++++++++++ .../terminal/AbstractTerminalScreenHandler.kt | 250 +++++++++++++++++ .../AbstractTerminalViewController.kt | 181 ++++++++++++ .../phycon/block/terminal/TerminalBlock.kt | 64 +---- .../block/terminal/TerminalBlockEntity.kt | 241 +--------------- .../phycon/block/terminal/TerminalFakeSlot.kt | 2 +- .../TerminalRequestAmountViewController.kt | 2 +- .../phycon/block/terminal/TerminalScreen.kt | 189 +------------ .../block/terminal/TerminalScreenHandler.kt | 241 +--------------- .../block/terminal/TerminalViewController.kt | 163 +---------- .../phycon/client/model/TerminalModel.kt | 4 +- .../net/shadowfacts/phycon/init/PhyItems.kt | 1 - .../net/shadowfacts/phycon/init/PhyScreens.kt | 3 +- .../networking/C2STerminalRequestItem.kt | 7 +- .../C2STerminalUpdateDisplayedItems.kt | 9 +- .../S2CTerminalUpdateDisplayedItems.kt | 13 +- .../resources/assets/phycon/lang/en_us.json | 1 + 23 files changed, 1034 insertions(+), 911 deletions(-) create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlock.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreen.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt diff --git a/plugin/mousewheelie/src/main/java/net/shadowfacts/phycon/plugin/mousewheelie/mixin/MixinTerminalScreen.java b/plugin/mousewheelie/src/main/java/net/shadowfacts/phycon/plugin/mousewheelie/mixin/MixinTerminalScreen.java index 2f48ff3..c7249bd 100644 --- a/plugin/mousewheelie/src/main/java/net/shadowfacts/phycon/plugin/mousewheelie/mixin/MixinTerminalScreen.java +++ b/plugin/mousewheelie/src/main/java/net/shadowfacts/phycon/plugin/mousewheelie/mixin/MixinTerminalScreen.java @@ -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 implements ISpecialScrollableScreen, IContainerScreen { +@Mixin(AbstractTerminalScreen.class) +public abstract class MixinTerminalScreen extends HandledScreen implements ISpecialScrollableScreen, IContainerScreen { - private MixinTerminalScreen(TerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) { + private MixinTerminalScreen(AbstractTerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) { super(screenHandler, playerInventory, text); } diff --git a/plugin/rei/src/main/kotlin/net/shadowfacts/phycon/plugin/rei/PhyConPlugin.kt b/plugin/rei/src/main/kotlin/net/shadowfacts/phycon/plugin/rei/PhyConPlugin.kt index b4c66af..afb7118 100644 --- a/plugin/rei/src/main/kotlin/net/shadowfacts/phycon/plugin/rei/PhyConPlugin.kt +++ b/plugin/rei/src/main/kotlin/net/shadowfacts/phycon/plugin/rei/PhyConPlugin.kt @@ -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( diff --git a/src/main/java/net/shadowfacts/phycon/mixin/client/MixinHandledScreen.java b/src/main/java/net/shadowfacts/phycon/mixin/client/MixinHandledScreen.java index 6aa7bce..f6aae36 100644 --- a/src/main/java/net/shadowfacts/phycon/mixin/client/MixinHandledScreen.java +++ b/src/main/java/net/shadowfacts/phycon/mixin/client/MixinHandledScreen.java @@ -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; diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt index 7679b89..0c879a4 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt @@ -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() - override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT + override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT override val controller = ActivationController(40L, this) override var providerPriority = 0 diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlock.kt new file mode 100644 index 0000000..564b8ec --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlock.kt @@ -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: DeviceBlock( + 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 { + val set = EnumSet.allOf(Direction::class.java) + set.remove(state[FACING]) + return set + } + + override fun appendProperties(builder: StateManager.Builder) { + 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)) + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt new file mode 100644 index 0000000..53376e3 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt @@ -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 { + + 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() + val internalBuffer = TerminalBufferInventory(18) + + private val pendingRequests = LinkedList() + override val pendingInsertions = mutableListOf() + 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? = 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(stack, timestamp) { + var bufferSlot by Delegates.notNull() + } + + data class StackLocateRequest( + val stack: ItemStack, + val amount: Int, + val timestamp: Long, + var results: MutableSet> = 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 + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreen.kt new file mode 100644 index 0000000..ba8833f --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreen.kt @@ -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>( + handler: T, + playerInv: PlayerInventory, + title: Text, + val terminalBackgroundWidth: Int, + val terminalBackgroundHeight: Int, +): CacaoHandledScreen(handler, playerInv, title) { + + companion object { + private val clickHandlers = LinkedList.(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 + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt new file mode 100644 index 0000000..41743d2 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt @@ -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( + 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() + set(value) { + field = value + if (terminal.world!!.isClient) { + itemsForDisplay = value.map { + it.stack.copyWithCount(it.amount) + } + } + } + var itemsForDisplay = listOf() + 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, 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 { + val slotsInsertedInto = mutableListOf() + 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) +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt new file mode 100644 index 0000000..e2189cb --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt @@ -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, H: AbstractTerminalScreenHandler>( + 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) + 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) + 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) + 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: 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 + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlock.kt index 098a00e..d8cf037 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlock.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlock.kt @@ -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( - Settings.of(Material.METAL) - .strength(1.5f) - .sounds(BlockSoundGroup.METAL) -), - NetworkComponentBlock, - AttributeProvider { +class TerminalBlock: AbstractTerminalBlock() { companion object { val ID = Identifier(PhysicalConnectivity.MODID, "terminal") - val FACING = Properties.FACING - } - - override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection { - val set = EnumSet.allOf(Direction::class.java) - set.remove(state[FACING]) - return set - } - - override fun appendProperties(builder: StateManager.Builder) { - 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)) - } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt index 315b6f9..79a79b0 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt @@ -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 { +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() - val internalBuffer = TerminalBufferInventory(18) - - private val pendingRequests = LinkedList() - override val pendingInsertions = mutableListOf() - 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? = 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(stack, timestamp) { - var bufferSlot by Delegates.notNull() - } - -} - -data class StackLocateRequest( - val stack: ItemStack, - val amount: Int, - val timestamp: Long, - var results: MutableSet> = 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 - } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalFakeSlot.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalFakeSlot.kt index 39726dc..c315967 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalFakeSlot.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalFakeSlot.kt @@ -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] diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalRequestAmountViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalRequestAmountViewController.kt index 3727c3a..571682a 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalRequestAmountViewController.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalRequestAmountViewController.kt @@ -19,7 +19,7 @@ import kotlin.math.floor * @author shadowfacts */ class TerminalRequestAmountViewController( - val screen: TerminalScreen, + val screen: AbstractTerminalScreen<*, *>, val stack: ItemStack, ): ViewController() { diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreen.kt index e2cec76..e41e542 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreen.kt @@ -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(handler, playerInv, title) { +class TerminalScreen( + handler: TerminalScreenHandler, + playerInv: PlayerInventory, + title: Text, +): AbstractTerminalScreen( + handler, + playerInv, + title, + 252, + 222 +) { companion object { private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") - - private val clickHandlers = LinkedList 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) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreenHandler.kt index 0ece564..d1a3f1f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreenHandler.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalScreenHandler.kt @@ -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() - set(value) { - field = value - if (terminal.world!!.isClient) { - itemsForDisplay = value.map { - it.stack.copyWithCount(it.amount) - } - } - } - var itemsForDisplay = listOf() - private set + playerInv: PlayerInventory, + terminal: TerminalBlockEntity, +): AbstractTerminalScreenHandler(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, 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 { - val slotsInsertedInto = mutableListOf() - 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) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalViewController.kt index 596fb15..74ac65d 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalViewController.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalViewController.kt @@ -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( + screen, + handler, +) { } diff --git a/src/main/kotlin/net/shadowfacts/phycon/client/model/TerminalModel.kt b/src/main/kotlin/net/shadowfacts/phycon/client/model/TerminalModel.kt index 14e85fa..0954036 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/client/model/TerminalModel.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/client/model/TerminalModel.kt @@ -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, context: RenderContext ) { - val mesh = meshes[state[TerminalBlock.FACING].ordinal] + val mesh = meshes[state[AbstractTerminalBlock.FACING].ordinal] context.meshConsumer().accept(mesh) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt index bd28f67..87598dd 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt @@ -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 diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt index ed44f5d..dc228cc 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt @@ -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) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalRequestItem.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalRequestItem.kt index b9fca5d..e5e049d 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalRequestItem.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalRequestItem.kt @@ -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) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt index 4f0d094..7cfd2f1 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt @@ -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) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt index 14170f4..83c8908 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt @@ -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, query: String, settings: TerminalSettings, scrollPosition: Float, totalEntries: Int): Packet<*> { + operator fun invoke(terminal: AbstractTerminalBlockEntity, entries: List, 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(entryCount) + val entries = ArrayList(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) } diff --git a/src/main/resources/assets/phycon/lang/en_us.json b/src/main/resources/assets/phycon/lang/en_us.json index 0494f5e..3cf0cf9 100644 --- a/src/main/resources/assets/phycon/lang/en_us.json +++ b/src/main/resources/assets/phycon/lang/en_us.json @@ -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",