diff --git a/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusCore.kt b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusCore.kt new file mode 100644 index 000000000..dc651cb56 --- /dev/null +++ b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusCore.kt @@ -0,0 +1,277 @@ +package net.torvald.terrarum.modulecomputers.gameactors + +import com.badlogic.gdx.utils.Queue +import net.torvald.random.HQRNG +import net.torvald.terrarum.INGAME +import net.torvald.terrarum.Point2i +import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.gameactors.BlockBox +import net.torvald.terrarum.modulebasegame.gameactors.Electric +import net.torvald.terrarum.modulebasegame.gameworld.NetFrame +import net.torvald.terrarum.modulebasegame.gameworld.NetRunner +import net.torvald.terrarum.serialise.Common +import net.torvald.terrarum.ui.UICanvas +import org.dyn4j.geometry.Vector2 +import kotlin.math.sign + +/** + * Created by minjaesong on 2025-03-01. + */ +open class FixtureRingBusCore : Electric { + + @Transient private val rng = HQRNG() + + private val mac = rng.nextInt() + + companion object { + const val INITIAL_LISTENING_TIMEOUT = 300 + const val SIGNAL_TOO_WEAK_THRESHOLD = 1.0 / 16.0 + } + + private constructor() : super() + + private var portEmit = Point2i(0, 0) + private var portSink = Point2i(1, 0) + + constructor(portEmit: Point2i, portSink: Point2i, blockBox: BlockBox, nameFun: () -> String, mainUI: UICanvas?) : super( + blockBox0 = blockBox, + nameFun = nameFun, + mainUI = mainUI + ) { + this.portEmit = portEmit + this.portSink = portSink + } + + private fun setEmitterAndSink() { + clearStatus() + setWireEmitterAt(portEmit.x, portEmit.y, "10base2") + setWireSinkAt(portSink.x, portSink.y, "10base2") + } + + init { + setEmitterAndSink() + + if (!INGAME.world.extraFields.containsKey("tokenring")) { + INGAME.world.extraFields["tokenring"] = NetRunner() + } + } + + override fun reload() { + super.reload() + setEmitterAndSink() + if (!INGAME.world.extraFields.containsKey("tokenring")) { + INGAME.world.extraFields["tokenring"] = NetRunner() + } + } + + internal val msgQueue = Queue>() + internal val msgLog = Queue>() + + private var statusAbort = false // will "eat away" any receiving frames unless the frame is a ballot frame + private var activeMonitorStatus = 0 // 0: unknown, 1: known and not me, 2: known and it's me + + private var lastAccessTime = -1L + + override fun updateSignal() { + val time_t = INGAME.world.worldTime.TIME_T + if (lastAccessTime == -1L) lastAccessTime = time_t + + + // monitor the input port + val inn = getWireStateAt(1, 0, "10base2") + + // if a signal is there + if (inn.x >= SIGNAL_TOO_WEAK_THRESHOLD) { + lastAccessTime = time_t + + val frameNumber = (inn.y + (0.5 * inn.y.sign)).toInt() + + if (frameNumber != 0) { // frame number must be non-zero + // if not in abort state, process the incoming frames + if (!statusAbort) { + // fetch frame from the world + try { + val frame = getFrameByNumber(frameNumber) + + // fast init (voting) cancellation, if applicable + if (activeMonitorStatus == 0 && frame.getFrameType() == "token") { + activeMonitorStatus = 1 + } + + // if I have a message to send or the frame should be captured, do something with it + if (msgQueue.notEmpty() || frame.shouldIintercept(mac)) { + // do something with the received frame + val newFrame = doSomethingWithFrame(frame) ?: frameNumber + setWireEmissionAt(portEmit.x, portEmit.y, Vector2(1.0, newFrame.toDouble())) + + // if the "do something" processs returns a new frame, mark the old frame as to be destroyed + if (newFrame != frameNumber) { + frame.discardFrame() + } + } + // else, just pass it along + else { + setWireEmissionAt(portEmit.x, portEmit.y, Vector2(1.0, frameNumber.toDouble())) + } + } + // frame lost due to poor savegame migration or something: send out ABORT signal + catch (e: NullPointerException) { + emitNewFrame(NetFrame.makeAbort(mac)) + statusAbort = true + } + } + // else, still watch for the new valid token + else { + // fetch frame from the world + try { + val frame = getFrameByNumber(frameNumber) + + // not an Active Monitor + if (activeMonitorStatus < 2) { + if (frame.getFrameType() == "token") { + // unlock myself and pass the token + statusAbort = false + setWireEmissionAt(portEmit.x, portEmit.y, Vector2(1.0, frameNumber.toDouble())) + } + else if (frame.getFrameType() == "abort") { + // lock myself (just in case) and pass the token + statusAbort = true + setWireEmissionAt(portEmit.x, portEmit.y, Vector2(1.0, frameNumber.toDouble())) + } + else { + // discard anything that is not a token or yet another abort + setWireEmissionAt(portEmit.x, portEmit.y, Vector2()) + } + } + // am Active Monitor + else { + if (frame.getFrameType() == "abort") { + // send out a new token + emitNewFrame(NetFrame.makeToken(mac)) + statusAbort = false + } + else { + // discard anything that is not an abort + setWireEmissionAt(portEmit.x, portEmit.y, Vector2()) + } + } + } + // frame lost due to poor savegame migration or something: discard token + catch (e: NullPointerException) { + setWireEmissionAt(portEmit.x, portEmit.y, Vector2()) + } + } + } + else { + setWireEmissionAt(portEmit.x, portEmit.y, Vector2()) + } + } + // if a signal is not there + else { + setWireEmissionAt(portEmit.x, portEmit.y, Vector2()) + + // if no-signal for 5 in-game minutes (around 5 real-life seconds) + if (time_t - lastAccessTime > INITIAL_LISTENING_TIMEOUT) { + // initialise the voting process + activeMonitorStatus = 0 + emitNewFrame(NetFrame.makeBallot(mac)) + lastAccessTime = time_t + } + } + } + + protected fun doSomethingWithFrame(incomingFrame: NetFrame): Int? { + return when (incomingFrame.getFrameType()) { + "token" -> doSomethingWithToken(incomingFrame) + "data" -> doSomethingWithData(incomingFrame) + "ack" -> doSomethingWithAck(incomingFrame) + "ballot" -> doSomethingWithBallot(incomingFrame) + "abort" -> 0 + else -> null /* returns the frame untouched */ + } + } + + private fun getFrameByNumber(number: Int) = (INGAME.world.extraFields["tokenring"] as NetRunner)[number] + + private fun emitNewFrame(frame: NetFrame): Int { + return (INGAME.world.extraFields["tokenring"] as NetRunner).addFrame(frame).also { + setWireEmissionAt(portEmit.x, portEmit.y, Vector2(1.0, it.toDouble())) + } + } + + protected fun doSomethingWithToken(incomingFrame: NetFrame): Int? { + if (msgQueue.isEmpty) return null + + val (recipient, msgByte) = msgQueue.removeFirst() + + return emitNewFrame(NetFrame.makeData(mac, recipient, msgByte)) + } + + protected fun doSomethingWithData(incomingFrame: NetFrame): Int? { + val rec = incomingFrame.getDataRecipient() + // if the message is for me, put incoming message into queue, then send out ack + if (rec == mac) { + msgLog.addLast(rec to incomingFrame) + + // make ack + return emitNewFrame(NetFrame.makeAck(mac, incomingFrame.getSender())) + } + else return null + } + + protected fun doSomethingWithAck(incomingFrame: NetFrame): Int? { + if (msgQueue.isEmpty) return null + + val topMsg = msgQueue.first() + + // if the ACK is sent to me... + if (incomingFrame.getDataRecipient() == mac && incomingFrame.getSender() == topMsg.first) { + + // ack or nak? + val successful = (incomingFrame.getAckStatus() == 0) + + // if successful, remove the message from the queue, then send out empty token + // if failed, keep the message, then send out empty token anyway + if (successful) { + msgQueue.removeFirst() + } + + // make an empty token + return emitNewFrame(NetFrame.makeToken(mac)) + } + else return null + } + + protected fun doSomethingWithBallot(incomingFrame: NetFrame): Int? { + val ballotStatus = incomingFrame.getFrameNumber() + + // frame is in election phase + if (ballotStatus == 0) { + // if i'm also in the election phase, participate + if (activeMonitorStatus == 0) { + if (incomingFrame.getBallot() < mac) { + incomingFrame.setBallot(mac) + } + + // check if the election must be finished + if (incomingFrame.getSender() == mac && incomingFrame.getBallot() == mac) { + activeMonitorStatus = 2 + + // send out first empty token + return emitNewFrame(NetFrame.makeToken(mac)) + } + } + // if i'm in the winner announcement phase, kill the frame + else { + incomingFrame.discardFrame() + return 0 + } + } + // frame is in winner announcement phase + else if (ballotStatus == 1) { + activeMonitorStatus = 1 + } + + return null + } +} \ No newline at end of file diff --git a/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusExerciser.kt b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusExerciser.kt index 3192a807c..557288bc5 100644 --- a/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusExerciser.kt +++ b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/gameactors/FixtureRingBusExerciser.kt @@ -1,269 +1,41 @@ package net.torvald.terrarum.modulecomputers.gameactors -import com.badlogic.gdx.utils.Queue -import net.torvald.random.HQRNG -import net.torvald.terrarum.INGAME +import net.torvald.terrarum.Point2i import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.modulebasegame.gameactors.BlockBox -import net.torvald.terrarum.modulebasegame.gameactors.Electric -import net.torvald.terrarum.modulebasegame.gameworld.NetFrame -import net.torvald.terrarum.modulebasegame.gameworld.NetRunner -import net.torvald.terrarum.serialise.Common -import org.dyn4j.geometry.Vector2 -import kotlin.math.sign +import net.torvald.terrarum.modulecomputers.ui.UIRingBusExerciser /** - * Created by minjaesong on 2025-03-01. + * Created by minjaesong on 2025-03-03. */ -class FixtureRingBusExerciser : Electric { - - @Transient private val rng = HQRNG() - - private val mac = rng.nextInt() - - companion object { - const val INITIAL_LISTENING_TIMEOUT = 300 - const val SIGNAL_TOO_WEAK_THRESHOLD = 1.0 / 16.0 - } +class FixtureRingBusExerciser : FixtureRingBusCore { constructor() : super( - BlockBox(BlockBox.NO_COLLISION, 2, 2), - nameFun = { Lang["ITEM_JUKEBOX"] }, - mainUI = UIRingBusExerciser() + portEmit = Point2i(0, 0), + portSink = Point2i(1, 0), + blockBox = BlockBox(BlockBox.NO_COLLISION, 2, 2), + nameFun = { Lang["ITEM_DEBUG_RING_BUS_EXERCISER"] }, + mainUI = UIRingBusExerciser(this) ) - private fun setEmitterAndSink() { - clearStatus() - setWireEmitterAt(0, 0, "10base2") - setWireSinkAt(1, 0, "10base2") - } - init { - setEmitterAndSink() - - if (!INGAME.world.extraFields.containsKey("tokenring")) { - INGAME.world.extraFields["tokenring"] = NetRunner() - } - } - - override fun reload() { - super.reload() - setEmitterAndSink() - if (!INGAME.world.extraFields.containsKey("tokenring")) { - INGAME.world.extraFields["tokenring"] = NetRunner() - } - } - - private val msgQueue = Queue>() - private val msgLog = Queue>() - - private var statusAbort = false // will "eat away" any receiving frames unless the frame is a ballot frame - private var activeMonitorStatus = 0 // 0: unknown, 1: known and not me, 2: known and it's me - - private var lastAccessTime = -1L - - override fun updateSignal() { - val time_t = INGAME.world.worldTime.TIME_T - if (lastAccessTime == -1L) lastAccessTime = time_t +} - // monitor the input port - val inn = getWireStateAt(1, 0, "10base2") - // if a signal is there - if (inn.x >= SIGNAL_TOO_WEAK_THRESHOLD) { - lastAccessTime = time_t +/** + * Created by minjaesong on 2025-03-03. + */ +class FixtureRingBusAnalyser : FixtureRingBusCore { - val frameNumber = (inn.y + (0.5 * inn.y.sign)).toInt() + constructor() : super( + portEmit = Point2i(0, 0), + portSink = Point2i(1, 0), + blockBox = BlockBox(BlockBox.NO_COLLISION, 2, 1), + nameFun = { Lang["ITEM_DEBUG_RING_BUS_Analyser"] }, + mainUI = UIRingBusAnalyser() + ) - if (frameNumber != 0) { // frame number must be non-zero - // if not in abort state, process the incoming frames - if (!statusAbort) { - // fetch frame from the world - try { - val frame = getFrameByNumber(frameNumber) - // fast init (voting) cancellation, if applicable - if (activeMonitorStatus == 0 && frame.getFrameType() == "token") { - activeMonitorStatus = 1 - } +} - // if I have a message to send or the frame should be captured, do something with it - if (msgQueue.notEmpty() || frame.shouldIintercept(mac)) { - // do something with the received frame - val newFrame = doSomethingWithFrame(frame) ?: frameNumber - setWireEmissionAt(0, 0, Vector2(1.0, newFrame.toDouble())) - - // if the "do something" processs returns a new frame, mark the old frame as to be destroyed - if (newFrame != frameNumber) { - frame.discardFrame() - } - } - // else, just pass it along - else { - setWireEmissionAt(0, 0, Vector2(1.0, frameNumber.toDouble())) - } - } - // frame lost due to poor savegame migration or something: send out ABORT signal - catch (e: NullPointerException) { - emitNewFrame(NetFrame.makeAbort(mac)) - statusAbort = true - } - } - // else, still watch for the new valid token - else { - // fetch frame from the world - try { - val frame = getFrameByNumber(frameNumber) - - // not an Active Monitor - if (activeMonitorStatus < 2) { - if (frame.getFrameType() == "token") { - // unlock myself and pass the token - statusAbort = false - setWireEmissionAt(0, 0, Vector2(1.0, frameNumber.toDouble())) - } - else if (frame.getFrameType() == "abort") { - // lock myself (just in case) and pass the token - statusAbort = true - setWireEmissionAt(0, 0, Vector2(1.0, frameNumber.toDouble())) - } - else { - // discard anything that is not a token or yet another abort - setWireEmissionAt(0, 0, Vector2()) - } - } - // am Active Monitor - else { - if (frame.getFrameType() == "abort") { - // send out a new token - emitNewFrame(NetFrame.makeToken(mac)) - statusAbort = false - } - else { - // discard anything that is not an abort - setWireEmissionAt(0, 0, Vector2()) - } - } - } - // frame lost due to poor savegame migration or something: discard token - catch (e: NullPointerException) { - setWireEmissionAt(0, 0, Vector2()) - } - } - } - else { - setWireEmissionAt(0, 0, Vector2()) - } - } - // if a signal is not there - else { - setWireEmissionAt(0, 0, Vector2()) - - // if no-signal for 5 in-game minutes (around 5 real-life seconds) - if (time_t - lastAccessTime > INITIAL_LISTENING_TIMEOUT) { - // initialise the voting process - activeMonitorStatus = 0 - emitNewFrame(NetFrame.makeBallot(mac)) - lastAccessTime = time_t - } - } - } - - protected fun doSomethingWithFrame(incomingFrame: NetFrame): Int? { - return when (incomingFrame.getFrameType()) { - "token" -> doSomethingWithToken(incomingFrame) - "data" -> doSomethingWithData(incomingFrame) - "ack" -> doSomethingWithAck(incomingFrame) - "ballot" -> doSomethingWithBallot(incomingFrame) - "abort" -> 0 - else -> null /* returns the frame untouched */ - } - } - - private fun getFrameByNumber(number: Int) = (INGAME.world.extraFields["tokenring"] as NetRunner)[number] - - private fun emitNewFrame(frame: NetFrame): Int { - return (INGAME.world.extraFields["tokenring"] as NetRunner).addFrame(frame).also { - setWireEmissionAt(0, 0, Vector2(1.0, it.toDouble())) - } - } - - protected fun doSomethingWithToken(incomingFrame: NetFrame): Int? { - if (msgQueue.isEmpty) return null - - val (recipient, msgStr) = msgQueue.removeFirst() - val msgByte = msgStr.toByteArray(Common.CHARSET) - - return emitNewFrame(NetFrame.makeData(mac, recipient, msgByte)) - } - - protected fun doSomethingWithData(incomingFrame: NetFrame): Int? { - val rec = incomingFrame.getDataRecipient() - // if the message is for me, put incoming message into queue, then send out ack - if (rec == mac) { - val str = incomingFrame.getDataContents()?.toString(Common.CHARSET) - msgLog.addLast(rec to (str ?: "(null)")) - - // make ack - return emitNewFrame(NetFrame.makeAck(mac, incomingFrame.getSender())) - } - else return null - } - - protected fun doSomethingWithAck(incomingFrame: NetFrame): Int? { - if (msgQueue.isEmpty) return null - - val topMsg = msgQueue.first() - - // if the ACK is sent to me... - if (incomingFrame.getDataRecipient() == mac && incomingFrame.getSender() == topMsg.first) { - - // ack or nak? - val successful = (incomingFrame.getAckStatus() == 0) - - // if successful, remove the message from the queue, then send out empty token - // if failed, keep the message, then send out empty token anyway - if (successful) { - msgQueue.removeFirst() - } - - // make an empty token - return emitNewFrame(NetFrame.makeToken(mac)) - } - else return null - } - - protected fun doSomethingWithBallot(incomingFrame: NetFrame): Int? { - val ballotStatus = incomingFrame.getFrameNumber() - - // frame is in election phase - if (ballotStatus == 0) { - // if i'm also in the election phase, participate - if (activeMonitorStatus == 0) { - if (incomingFrame.getBallot() < mac) { - incomingFrame.setBallot(mac) - } - - // check if the election must be finished - if (incomingFrame.getSender() == mac && incomingFrame.getBallot() == mac) { - activeMonitorStatus = 2 - - // send out first empty token - return emitNewFrame(NetFrame.makeToken(mac)) - } - } - // if i'm in the winner announcement phase, kill the frame - else { - incomingFrame.discardFrame() - return 0 - } - } - // frame is in winner announcement phase - else if (ballotStatus == 1) { - activeMonitorStatus = 1 - } - - return null - } -} \ No newline at end of file diff --git a/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusAnalyser.kt b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusAnalyser.kt new file mode 100644 index 000000000..bb4381cff --- /dev/null +++ b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusAnalyser.kt @@ -0,0 +1,101 @@ +package net.torvald.terrarum.modulecomputers.ui + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import net.torvald.terrarum.App +import net.torvald.terrarum.ifNaN +import net.torvald.terrarum.modulecomputers.gameactors.FixtureRingBusCore +import net.torvald.terrarum.ui.UICanvas +import net.torvald.terrarum.ui.UIItemVertSlider +import net.torvald.terrarum.ui.Toolkit +import kotlin.math.roundToInt + +class UIRingBusAnalyser(val host: FixtureRingBusCore) : UICanvas() { + + override var width = Toolkit.drawWidth + override var height = App.scr.height + + private val analyserPosX = 10 + private val analyserPosY = 10 + private val analyserWidth = width - 20 + private val analyserHeight = height - 20 + + private val TEXT_LINE_HEIGHT = 24 + private var analysisTextBuffer = ArrayList() + + private val analyserScroll = UIItemVertSlider(this, + analyserPosX - 18, + analyserPosY + 1, + 0.0, 0.0, 1.0, analyserHeight - 2, analyserHeight - 2 + ) + + init { + addUIitem(analyserScroll) + refreshAnalysis() + } + + private fun refreshAnalysis() { + analysisTextBuffer.clear() + + host.msgLog.forEach { frame -> + analysisTextBuffer.add(frame.toString()) + } + + // update scrollbar + analyserScroll.handleHeight = if (analysisTextBuffer.isEmpty()) + analyserHeight + else + (analyserHeight.toFloat() / analysisTextBuffer.size.times(TEXT_LINE_HEIGHT)) + .times(analyserHeight) + .roundToInt() + .coerceIn(12, analyserHeight) + } + + private fun drawAnalysis(batch: SpriteBatch) { + val scroll = (analyserScroll.value * analysisTextBuffer.size.times(TEXT_LINE_HEIGHT) + .minus(analyserHeight - 3)) + .ifNaN(0.0) + .roundToInt() + .coerceAtLeast(0) + + analysisTextBuffer.forEachIndexed { index, s -> + App.fontGame.draw(batch, s, + analyserPosX + 6f, + analyserPosY + 3f + index * TEXT_LINE_HEIGHT - scroll + ) + } + } + + override fun updateImpl(delta: Float) { + refreshAnalysis() + uiItems.forEach { it.update(delta) } + } + + override fun renderImpl(frameDelta: Float, batch: SpriteBatch, camera: OrthographicCamera) { + // Draw background box + batch.color = Color(0x7F) + Toolkit.fillArea(batch, analyserPosX, analyserPosY, analyserWidth, analyserHeight) + batch.color = Toolkit.Theme.COL_INACTIVE + Toolkit.drawBoxBorder(batch, analyserPosX, analyserPosY, analyserWidth, analyserHeight) + + // Draw text content + batch.color = Color.WHITE + drawAnalysis(batch) + + // Draw UI elements + uiItems.forEach { it.render(frameDelta, batch, camera) } + } + + override fun doOpening(delta: Float) { + refreshAnalysis() + } + + override fun doClosing(delta: Float) { + // nothing needed + } + + override fun dispose() { + + } +} \ No newline at end of file diff --git a/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusExerciser.kt b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusExerciser.kt new file mode 100644 index 000000000..eedd7b295 --- /dev/null +++ b/ModuleComputers/src/net/torvald/terrarum/modulecomputers/ui/UIRingBusExerciser.kt @@ -0,0 +1,101 @@ +package net.torvald.terrarum.modulecomputers.ui + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import net.torvald.terrarum.App +import net.torvald.terrarum.ifNaN +import net.torvald.terrarum.modulecomputers.gameactors.FixtureRingBusCore +import net.torvald.terrarum.ui.UICanvas +import net.torvald.terrarum.ui.UIItemVertSlider +import net.torvald.terrarum.ui.Toolkit +import kotlin.math.roundToInt + +class UIRingBusExerciser(val host: FixtureRingBusCore) : UICanvas() { + + override var width = Toolkit.drawWidth + override var height = App.scr.height + + private val analyserPosX = 10 + private val analyserPosY = 10 + private val analyserWidth = width - 20 + private val analyserHeight = height - 20 + + private val TEXT_LINE_HEIGHT = 24 + private var analysisTextBuffer = ArrayList() + + private val analyserScroll = UIItemVertSlider(this, + analyserPosX - 18, + analyserPosY + 1, + 0.0, 0.0, 1.0, analyserHeight - 2, analyserHeight - 2 + ) + + init { + addUIitem(analyserScroll) + refreshAnalysis() + } + + private fun refreshAnalysis() { + analysisTextBuffer.clear() + + host.msgLog.forEach { frame -> + analysisTextBuffer.add(frame.toString()) + } + + // update scrollbar + analyserScroll.handleHeight = if (analysisTextBuffer.isEmpty()) + analyserHeight + else + (analyserHeight.toFloat() / analysisTextBuffer.size.times(TEXT_LINE_HEIGHT)) + .times(analyserHeight) + .roundToInt() + .coerceIn(12, analyserHeight) + } + + private fun drawAnalysis(batch: SpriteBatch) { + val scroll = (analyserScroll.value * analysisTextBuffer.size.times(TEXT_LINE_HEIGHT) + .minus(analyserHeight - 3)) + .ifNaN(0.0) + .roundToInt() + .coerceAtLeast(0) + + analysisTextBuffer.forEachIndexed { index, s -> + App.fontGame.draw(batch, s, + analyserPosX + 6f, + analyserPosY + 3f + index * TEXT_LINE_HEIGHT - scroll + ) + } + } + + override fun updateImpl(delta: Float) { + refreshAnalysis() + uiItems.forEach { it.update(delta) } + } + + override fun renderImpl(frameDelta: Float, batch: SpriteBatch, camera: OrthographicCamera) { + // Draw background box + batch.color = Color(0x7F) + Toolkit.fillArea(batch, analyserPosX, analyserPosY, analyserWidth, analyserHeight) + batch.color = Toolkit.Theme.COL_INACTIVE + Toolkit.drawBoxBorder(batch, analyserPosX, analyserPosY, analyserWidth, analyserHeight) + + // Draw text content + batch.color = Color.WHITE + drawAnalysis(batch) + + // Draw UI elements + uiItems.forEach { it.render(frameDelta, batch, camera) } + } + + override fun doOpening(delta: Float) { + refreshAnalysis() + } + + override fun doClosing(delta: Float) { + // nothing needed + } + + override fun dispose() { + + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/NetFrame.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/NetFrame.kt index 8de4a5816..aa7a195ed 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/NetFrame.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/NetFrame.kt @@ -144,6 +144,18 @@ data class NetFrame(val byteArray: ByteArray) { it.writeBigInt32(recipient, 6) it.writeBigInt16(status, 10) }) + + fun Int.toMAC() = this.ushr(16).toString(16).uppercase().padStart(4,'0') + "." + this.and(65535).toString(16).uppercase().padStart(4,'0') } + + override fun toString(): String { + val frameType = getFrameType() + return "Frame($frameType[${getFrameNumber()}] from ${getSender().toMAC()})" + when (frameType) { + "data" -> ": to ${getDataRecipient().toMAC()}, datagram size: ${byteArray.size - 18} bytes" + "ack" -> ": to ${getDataRecipient().toMAC()}" + "ballot" -> ": current candidate: ${getBallot().toMAC()}" + else -> "" + } + } } \ No newline at end of file