diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt b/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt index 355cdcb..79971a5 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt @@ -55,6 +55,10 @@ object RenderHelper: DrawableHelper() { if (disabled) return RenderSystem.lineWidth(width) + RenderSystem.enableBlend() + RenderSystem.disableTexture() + RenderSystem.defaultBlendFunc() + RenderSystem.setShader(GameRenderer::getPositionColorShader) val tessellator = Tessellator.getInstance() val buffer = tessellator.buffer buffer.begin(VertexFormat.DrawMode.LINES, VertexFormats.POSITION_COLOR) diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt index 1afd68d..8a297fc 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt @@ -7,6 +7,7 @@ import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry import net.fabricmc.fabric.api.renderer.v1.RendererAccess import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial import net.shadowfacts.phycon.block.inserter.InserterScreen +import net.shadowfacts.phycon.block.netswitch.SwitchConsoleScreen import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreen import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreen import net.shadowfacts.phycon.init.PhyScreens @@ -44,6 +45,7 @@ object PhysicalConnectivityClient: ClientModInitializer { ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen) ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen) ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen) + ScreenRegistry.register(PhyScreens.SWITCH_CONSOLE, ::SwitchConsoleScreen) registerGlobalReceiver(S2CTerminalUpdateDisplayedItems) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchBlockEntity.kt index 850c1b8..8fd857f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchBlockEntity.kt @@ -20,6 +20,7 @@ import net.shadowfacts.phycon.frame.BasePacketFrame import net.shadowfacts.phycon.frame.NetworkSplitFrame import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.packet.ItemStackPacket +import net.shadowfacts.phycon.util.IntRingBuffer import net.shadowfacts.phycon.util.NetworkUtil import java.lang.ref.WeakReference import java.util.Deque @@ -38,9 +39,13 @@ class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockE private val macTable = mutableMapOf() private val destinationCache = Array?>(6) { null } + val packetStatistics = IntRingBuffer(60) // 1 minute's worth + private val currentSecondPacketStatistics = IntRingBuffer(20) private var packetsHandledThisTick = 0 private var delayedPackets: Deque> = LinkedList() + var statisticsObserver: (() -> Unit)? = null + fun interfaceForSide(side: Direction): SwitchInterface { return interfaces.find { it.side == side }!! } @@ -98,6 +103,16 @@ class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockE } fun tick() { + if (statisticsObserver != null) { + if (currentSecondPacketStatistics.size == 20) { + packetStatistics.add(currentSecondPacketStatistics.sum()) + currentSecondPacketStatistics.clear() + statisticsObserver?.invoke() + } else { + currentSecondPacketStatistics.add(packetsHandledThisTick) + } + } + packetsHandledThisTick = 0 while (delayedPackets.isNotEmpty() && packetsHandledThisTick <= SWITCHING_CAPACITY) { @@ -147,6 +162,11 @@ class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockE delayedPackets.addLast(frame to fromItf) } } + tag.getIntArray("PacketStatistics")?.also { statistics -> + if (statistics.isNotEmpty()) { + packetStatistics.replace(statistics) + } + } } override fun toUpdatePacket(): Packet? { @@ -156,6 +176,7 @@ class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockE override fun toInitialChunkDataNbt(): NbtCompound { val tag = NbtCompound() tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address }) + tag.putIntArray("PacketStatistics", packetStatistics.asContiguousArray()) return tag } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreen.kt new file mode 100644 index 0000000..533d8d6 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreen.kt @@ -0,0 +1,94 @@ +package net.shadowfacts.phycon.block.netswitch + +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.text.LiteralText +import net.minecraft.text.Text +import net.shadowfacts.cacao.CacaoHandledScreen +import net.shadowfacts.cacao.geometry.Point +import net.shadowfacts.cacao.geometry.Rect +import net.shadowfacts.cacao.geometry.Size +import net.shadowfacts.cacao.util.Color +import net.shadowfacts.cacao.util.RenderHelper +import net.shadowfacts.cacao.view.Label +import net.shadowfacts.cacao.view.View +import net.shadowfacts.cacao.viewcontroller.ViewController +import net.shadowfacts.cacao.window.ScreenHandlerWindow +import net.shadowfacts.kiwidsl.dsl +import org.lwjgl.glfw.GLFW + +/** + * @author shadowfacts + */ +class SwitchConsoleScreen( + handler: SwitchConsoleScreenHandler, + playerInventory: PlayerInventory, + title: Text, +): CacaoHandledScreen( + handler, + playerInventory, + title, +) { + + val root = SwitchConsoleViewController(handler.switch) + + init { + addWindow(ScreenHandlerWindow(handler, root)) + } + + override fun shouldPause() = false + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (keyCode == GLFW.GLFW_KEY_E) { + close() + return true + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + +} + +class SwitchConsoleViewController(val switch: SwitchBlockEntity): ViewController() { + override fun viewDidLoad() { + super.viewDidLoad() + + val stats = SwitchPacketStatisticsView(switch) + view.addSubview(stats) + view.solver.dsl { + stats.centerXAnchor equalTo (view.centerXAnchor + 50) + stats.centerYAnchor equalTo (view.centerYAnchor + 50) + } + } +} + +class SwitchPacketStatisticsView(val switch: SwitchBlockEntity): View() { + + init { + intrinsicContentSize = Size(180.0, 90.0) + } + + override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { + RenderHelper.fill(matrixStack, bounds, Color.BLACK) + if (switch.packetStatistics.size == 0) { + return + } + // TODO: drawLine isn't working for some reason + RenderHelper.drawLine(Point(bounds.left, bounds.top), Point(bounds.right, bounds.bottom), 1.0, 2f, Color.MAGENTA) + return + val maxPackets = switch.packetStatistics.maxOf { it } + val maxDataPointsCount = 60 + var lastPoint: Point? = null + val size = Size(3.0, 3.0) + for ((index, packets) in switch.packetStatistics.withIndex()) { + val x = (1 - (switch.packetStatistics.size - index).toDouble() / maxDataPointsCount) * bounds.width + val y = (1 - (packets.toDouble() / maxPackets)) * (bounds.height) + val point = Point(x, y) + if (lastPoint != null) { +// RenderHelper.fill(matrixStack, Rect(lastPoint, 3.0, 3.0), Color.RED) + RenderHelper.drawLine(lastPoint, point, 1.0, 2f, Color.RED) + } + lastPoint = point + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreenHandler.kt new file mode 100644 index 0000000..5d50497 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/netswitch/SwitchConsoleScreenHandler.kt @@ -0,0 +1,46 @@ +package net.shadowfacts.phycon.block.netswitch + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.network.PacketByteBuf +import net.minecraft.screen.ScreenHandler +import net.minecraft.util.Identifier +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.init.PhyBlocks +import net.shadowfacts.phycon.init.PhyScreens + +/** + * @author shadowfacts + */ +class SwitchConsoleScreenHandler( + syncId: Int, + val switch: SwitchBlockEntity, +): ScreenHandler(PhyScreens.SWITCH_CONSOLE, syncId) { + + companion object { + val ID = Identifier(PhysicalConnectivity.MODID, "switch_console") + } + + constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): + this( + syncId, + PhyBlocks.SWITCH.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! + ) + + init { + switch.statisticsObserver = { + switch.world!!.updateListeners(switch.pos, switch.cachedState, switch.cachedState, 3) + } + } + + override fun canUse(player: PlayerEntity): Boolean { + return true + } + + override fun close(player: PlayerEntity) { + super.close(player) + + switch.statisticsObserver = null + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt index 91a84f4..3f875cf 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt @@ -1,8 +1,14 @@ package net.shadowfacts.phycon.init +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType.ExtendedFactory import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry +import net.minecraft.screen.ScreenHandler import net.minecraft.screen.ScreenHandlerType +import net.minecraft.util.Identifier +import net.minecraft.util.registry.Registry import net.shadowfacts.phycon.block.inserter.InserterScreenHandler +import net.shadowfacts.phycon.block.netswitch.SwitchConsoleScreenHandler import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreenHandler import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler @@ -19,12 +25,19 @@ object PhyScreens { private set lateinit var REDSTONE_EMITTER: ScreenHandlerType private set + lateinit var SWITCH_CONSOLE: ScreenHandlerType + private set fun init() { - TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalBlock.ID, ::TerminalScreenHandler) - CRAFTING_TERMINAL = ScreenHandlerRegistry.registerExtended(CraftingTerminalBlock.ID, ::CraftingTerminalScreenHandler) - INSERTER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler) - REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler) + TERMINAL = register(TerminalBlock.ID, ::TerminalScreenHandler) + CRAFTING_TERMINAL = register(CraftingTerminalBlock.ID, ::CraftingTerminalScreenHandler) + INSERTER = register(InserterScreenHandler.ID, ::InserterScreenHandler) + REDSTONE_EMITTER = register(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler) + SWITCH_CONSOLE = register(SwitchConsoleScreenHandler.ID, ::SwitchConsoleScreenHandler) + } + + fun register(id: Identifier, factory: ExtendedFactory): ScreenHandlerType { + return Registry.register(Registry.SCREEN_HANDLER, id, ExtendedScreenHandlerType(factory)) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt b/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt index db58594..0e350f0 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt @@ -1,14 +1,25 @@ package net.shadowfacts.phycon.item +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory import net.minecraft.client.MinecraftClient +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerInventory import net.minecraft.item.Item import net.minecraft.item.ItemUsageContext +import net.minecraft.network.PacketByteBuf +import net.minecraft.screen.ScreenHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text import net.minecraft.util.ActionResult import net.minecraft.util.Identifier import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.block.DeviceBlock import net.shadowfacts.phycon.block.DeviceBlockEntity +import net.shadowfacts.phycon.block.netswitch.SwitchBlockEntity import net.shadowfacts.phycon.client.screen.console.DeviceConsoleScreen +import net.shadowfacts.phycon.block.netswitch.SwitchConsoleScreen +import net.shadowfacts.phycon.block.netswitch.SwitchConsoleScreenHandler +import net.shadowfacts.phycon.init.PhyBlocks /** * @author shadowfacts @@ -31,6 +42,14 @@ class ConsoleItem: Item(Settings().maxCount(1)) { } return ActionResult.SUCCESS } + } else if (block === PhyBlocks.SWITCH) { + val be = block.getBlockEntity(context.world, context.blockPos) + if (be != null) { + if (!context.world.isClient && context.player != null) { + openScreen(be, context.player!!) + } + return ActionResult.SUCCESS + } } return ActionResult.PASS } @@ -41,4 +60,19 @@ class ConsoleItem: Item(Settings().maxCount(1)) { MinecraftClient.getInstance().setScreen(screen) } + private fun openScreen(be: SwitchBlockEntity, player: PlayerEntity) { + val factory = object: ExtendedScreenHandlerFactory { + override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler { + return SwitchConsoleScreenHandler(syncId, be) + } + + override fun getDisplayName() = PhyBlocks.SWITCH.name + + override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { + buf.writeBlockPos(be.pos) + } + } + player.openHandledScreen(factory) + } + } diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/IntRingBuffer.kt b/src/main/kotlin/net/shadowfacts/phycon/util/IntRingBuffer.kt new file mode 100644 index 0000000..91202a6 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/IntRingBuffer.kt @@ -0,0 +1,69 @@ +package net.shadowfacts.phycon.util + +import kotlin.math.min + +/** + * @author shadowfacts + */ +class IntRingBuffer(size: Int): Iterable { + private val array = IntArray(size) + private var next = 0 + private var wrapped = false + + val size: Int + get() = if (wrapped) array.size else next + + fun add(value: Int) { + array[next] = value + next = (next + 1) % array.size + wrapped = wrapped || next == 0 + } + + fun asContiguousArray(): IntArray { + if (wrapped) { + val array = IntArray(array.size) + this.array.copyInto(array, destinationOffset = array.size - next, startIndex = 0, endIndex = next) + this.array.copyInto(array, destinationOffset = 0, startIndex = next, endIndex = array.size) + return array + } else { + return this.array.copyOfRange(0, next) + } + } + + fun replace(array: IntArray) { + val count = min(array.size, this.array.size) + array.copyInto(this.array, destinationOffset = 0, startIndex = array.size - count, endIndex = count) + next = (count + 1) % this.array.size + wrapped = array.size >= this.array.size + } + + fun clear() { + next = 0 + wrapped = false + } + + override fun iterator() = Iterator(this) + + class Iterator(private val ringBuffer: IntRingBuffer): IntIterator() { + private var nextIndex = if (ringBuffer.wrapped) ringBuffer.next else 0 + private var wrapped = false + + override fun nextInt(): Int { + val res = ringBuffer.array[nextIndex] + nextIndex += 1 + if (nextIndex >= ringBuffer.array.size) { + wrapped = true + nextIndex = 0 + } + return res + } + + override fun hasNext(): Boolean { + return if (ringBuffer.wrapped) { + !wrapped || nextIndex < ringBuffer.next + } else { + nextIndex < ringBuffer.next + } + } + } +} diff --git a/src/test/kotlin/net/shadowfacts/phycon/util/IntRingBufferTests.kt b/src/test/kotlin/net/shadowfacts/phycon/util/IntRingBufferTests.kt new file mode 100644 index 0000000..5a50625 --- /dev/null +++ b/src/test/kotlin/net/shadowfacts/phycon/util/IntRingBufferTests.kt @@ -0,0 +1,48 @@ +package net.shadowfacts.phycon.util + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertEquals + +/** + * @author shadowfacts + */ +class IntRingBufferTests { + + @Test + fun testEmpty() { + assertArrayEquals(intArrayOf(), IntRingBuffer(4).asContiguousArray()) + } + + @Test + fun testAsContiguousArray() { + val buffer = IntRingBuffer(4) + buffer.add(0) + buffer.add(1) + assertArrayEquals(intArrayOf(0, 1), buffer.asContiguousArray()) + + buffer.add(2) + buffer.add(3) + buffer.add(4) + assertArrayEquals(intArrayOf(1, 2, 3, 4), buffer.asContiguousArray()) + } + + @Test + fun testIterator() { + val buffer = IntRingBuffer(4) + + val iterator = buffer.iterator() + assertFalse(iterator.hasNext()) + + buffer.add(0) + buffer.add(1) + assertEquals(listOf(0, 1), buffer.toList()) + + buffer.add(2) + buffer.add(3) + buffer.add(4) + assertEquals(listOf(1, 2, 3, 4), buffer.toList()) + } + +}