From 8c37a0cef9afaecb0a4d1dd10b6962ec6488c92d Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 25 May 2022 12:50:34 +0200 Subject: [PATCH] trigger tile png reload in browser by sending 'saved tile' events over websocket - create new EventServerPacket for websocket packet data - move common models to events-schema --- .../kafkatorio/schema/common/factorioIds.kt | 7 ++ .../kafkatorio/schema/common/mapTile.kt | 26 ++++++- .../schema/packets/eventServerPacket.kt | 27 +++++++ .../processor/KafkatorioTopology.kt | 10 +-- .../processor/admin/kafkatorioTopics.kt | 5 ++ .../processor/tileserver/WebMapTileServer.kt | 6 +- .../processor/topology/colourMapChunks.kt | 2 + .../topology/factorioServerPackets.kt | 1 + .../kafkatorio/processor/topology/model.kt | 20 +----- .../processor/topology/packetDataStreams.kt | 1 + .../processor/topology/playerUpdates.kt | 46 ++++++++++-- .../processor/topology/prototypes.kt | 1 + .../processor/topology/saveMapTiles.kt | 44 ++++++++---- .../processor/misc/KxsBinaryTests.kt | 7 +- .../kafkatorio/processor/misc/serdeSize.kt | 10 ++- .../processor/topology/SaveMapChunksTest.kt | 2 + .../dev/adamko/kafkatorio/webmap/App.kt | 71 ++++++++++--------- .../dev/adamko/kafkatorio/webmap/map.kt | 64 ++++++++++++++++- .../adamko/kafkatorio/webmap/websockets.kt | 61 +++++++++++----- 19 files changed, 303 insertions(+), 108 deletions(-) create mode 100644 modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/packets/eventServerPacket.kt diff --git a/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/factorioIds.kt b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/factorioIds.kt index 5f50aec..9384d90 100644 --- a/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/factorioIds.kt +++ b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/factorioIds.kt @@ -75,3 +75,10 @@ data class EntityIdentifiersData( override val name: String, override val protoType: String, ) : EntityIdentifiers + + +@JvmInline +@Serializable +value class FactorioServerId(private val id: String) { + override fun toString() = id +} diff --git a/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/mapTile.kt b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/mapTile.kt index be80d62..360554b 100644 --- a/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/mapTile.kt +++ b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/common/mapTile.kt @@ -1,10 +1,10 @@ package dev.adamko.kafkatorio.schema.common + import dev.adamko.kafkatorio.schema.common.MapTileDictionary.PrototypeKey import kotlin.jvm.JvmInline import kotlinx.serialization.Serializable - const val MAP_CHUNK_SIZE = 32 @@ -69,3 +69,27 @@ data class MapBoundingBox( val topLeft: MapTilePosition, val bottomRight: MapTilePosition, ) + + +@Serializable +data class ServerMapChunkId( + val serverId: FactorioServerId, + val chunkPosition: MapChunkPosition, + val surfaceIndex: SurfaceIndex, + val chunkSize: ChunkSize, +) + + +@Serializable +@JvmInline +value class TilePngFilename( + val value: String, +) { + constructor(id: ServerMapChunkId) : this(buildString { + append("s${id.surfaceIndex}") + append("/z${id.chunkSize.zoomLevel}") + append("/x${id.chunkPosition.x}") + append("/y${id.chunkPosition.y}") + append(".png") + }) +} diff --git a/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/packets/eventServerPacket.kt b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/packets/eventServerPacket.kt new file mode 100644 index 0000000..363aded --- /dev/null +++ b/modules/events-schema/src/commonMain/kotlin/dev/adamko/kafkatorio/schema/packets/eventServerPacket.kt @@ -0,0 +1,27 @@ +package dev.adamko.kafkatorio.schema.packets + +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId +import dev.adamko.kafkatorio.schema.common.TilePngFilename +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +sealed class EventServerPacket { + + + @Serializable + @SerialName("kafkatorio.event-server-packet.Kafkatorio") + data class Kafkatorio( + val packet: KafkatorioPacket + ) : EventServerPacket() + + + @Serializable + @SerialName("kafkatorio.event-server-packet.ChunkTileSaved") + data class ChunkTileSaved( + val chunkId: ServerMapChunkId, + val filename: TilePngFilename, + ) : EventServerPacket() + +} diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/KafkatorioTopology.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/KafkatorioTopology.kt index 342a85f..179863e 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/KafkatorioTopology.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/KafkatorioTopology.kt @@ -3,7 +3,7 @@ package dev.adamko.kafkatorio.processor import dev.adamko.kafkatorio.processor.config.ApplicationProperties import dev.adamko.kafkatorio.processor.topology.colourMapChunks import dev.adamko.kafkatorio.processor.topology.factorioServerPacketStream -import dev.adamko.kafkatorio.processor.topology.playerUpdatesToWsServer +import dev.adamko.kafkatorio.processor.topology.broadcastToWebsocket import dev.adamko.kafkatorio.processor.topology.saveMapTiles import dev.adamko.kafkatorio.processor.topology.splitFactorioServerPacketStream import java.time.Duration @@ -39,7 +39,7 @@ internal class KafkatorioTopology( groupTilesMapChunks() saveTiles() - playerUpdates() + websocketBroadcaster() } private fun splitPackets() { @@ -64,13 +64,13 @@ internal class KafkatorioTopology( launchTopology("saveTiles", topology) } - private fun playerUpdates() { + private fun websocketBroadcaster() { val builder = StreamsBuilder() - playerUpdatesToWsServer(websocketServer, builder) + broadcastToWebsocket(websocketServer, builder) val topology = builder.build() - launchTopology("playerUpdates", topology) + launchTopology("websocketBroadcaster", topology) } private fun launchTopology( diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/admin/kafkatorioTopics.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/admin/kafkatorioTopics.kt index bb287e0..1bbfe9e 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/admin/kafkatorioTopics.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/admin/kafkatorioTopics.kt @@ -26,6 +26,9 @@ const val TOPIC_SUBDIVIDED_MAP_TILES = "$DOMAIN.map-tiles.subdivided" const val TOPIC_SUBDIVIDED_MAP_TILES_DEBOUNCED = "$TOPIC_SUBDIVIDED_MAP_TILES.debounced" +/** Broadcast these events out, via websocket */ +const val TOPIC_BROADCAST_TO_WEBSOCKET = "$DOMAIN.broadcast.websocket" + fun allTopics(): Set = buildSet { add(TOPIC_SRC_SERVER_LOG) @@ -38,6 +41,8 @@ fun allTopics(): Set = buildSet { add(TOPIC_SUBDIVIDED_MAP_TILES) add(TOPIC_SUBDIVIDED_MAP_TILES_DEBOUNCED) + add(TOPIC_BROADCAST_TO_WEBSOCKET) + addAll(packetTopicNames.values) } diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/tileserver/WebMapTileServer.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/tileserver/WebMapTileServer.kt index a01aaea..0519684 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/tileserver/WebMapTileServer.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/tileserver/WebMapTileServer.kt @@ -100,19 +100,19 @@ internal class WebMapTileServer( currentETag == null -> { // println("[${request.etagKey}] currentETag is null (requested: $requestedETag), executing request") response - .header("Cache-Control", "no-cache") + .header("Cache-Control", "no-cache, max-age=0, must-revalidate") } requestedETag == currentETag -> { // println("[${request.etagKey}] currentETag == requested, returning 304 $requestedETag") Response(Status.NOT_MODIFIED) - .header("Cache-Control", "no-cache") + .header("Cache-Control", "no-cache, max-age=0, must-revalidate") } else -> { // println("[${request.etagKey}] currentETag != requested, executing request and adding currentETag:$currentETag (requested:$requestedETag)") response .header("ETag", currentETag) // .header("Cache-Control", "no-store") - .header("Cache-Control", "no-cache") + .header("Cache-Control", "no-cache, max-age=0, must-revalidate") } } } diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/colourMapChunks.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/colourMapChunks.kt index e9c7525..2a171f0 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/colourMapChunks.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/colourMapChunks.kt @@ -6,6 +6,8 @@ import dev.adamko.kafkatorio.processor.misc.DebounceProcessor.Companion.addDebou import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ColourHex +import dev.adamko.kafkatorio.schema.common.FactorioServerId +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId import dev.adamko.kafkatorio.schema.common.leftTopTile import dev.adamko.kafkatorio.schema.common.toMapChunkPosition import dev.adamko.kafkatorio.schema.packets.MapChunkUpdate diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/factorioServerPackets.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/factorioServerPackets.kt index 37254c7..df4423e 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/factorioServerPackets.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/factorioServerPackets.kt @@ -4,6 +4,7 @@ package dev.adamko.kafkatorio.processor.topology import dev.adamko.kafkatorio.processor.admin.TOPIC_SRC_SERVER_LOG import dev.adamko.kafkatorio.processor.admin.topicName import dev.adamko.kafkatorio.processor.serdes.jsonMapper +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket import dev.adamko.kotka.extensions.consumedAs import dev.adamko.kotka.extensions.producedAs diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/model.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/model.kt index 496a2c4..558ecf0 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/model.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/model.kt @@ -1,32 +1,14 @@ package dev.adamko.kafkatorio.processor.topology import com.sksamuel.scrimage.color.RGBColor -import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ColourHex -import dev.adamko.kafkatorio.schema.common.MapChunkPosition import dev.adamko.kafkatorio.schema.common.MapTile import dev.adamko.kafkatorio.schema.common.MapTilePosition -import dev.adamko.kafkatorio.schema.common.SurfaceIndex +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId import java.awt.Color import kotlinx.serialization.Serializable -@JvmInline -@Serializable -value class FactorioServerId(private val id: String) { - override fun toString() = id -} - - -@Serializable -data class ServerMapChunkId( - val serverId: FactorioServerId, - val chunkPosition: MapChunkPosition, - val surfaceIndex: SurfaceIndex, - val chunkSize: ChunkSize, -) - - @Serializable data class ServerMapChunkTiles( val chunkId: ServerMapChunkId, diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/packetDataStreams.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/packetDataStreams.kt index 81f7e43..6ada2be 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/packetDataStreams.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/packetDataStreams.kt @@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.processor.topology import dev.adamko.kafkatorio.processor.admin.topicName import dev.adamko.kafkatorio.processor.serdes.jsonMapper +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket import dev.adamko.kafkatorio.schema.packets.KafkatorioPacketData import dev.adamko.kotka.extensions.consumedAs diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/playerUpdates.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/playerUpdates.kt index 48a910a..968a924 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/playerUpdates.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/playerUpdates.kt @@ -1,11 +1,17 @@ package dev.adamko.kafkatorio.processor.topology import dev.adamko.kafkatorio.processor.WebsocketServer +import dev.adamko.kafkatorio.processor.admin.TOPIC_BROADCAST_TO_WEBSOCKET import dev.adamko.kafkatorio.processor.admin.topicName +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio +import dev.adamko.kafkatorio.schema.packets.EventServerPacket import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kotka.extensions.consumedAs +import dev.adamko.kotka.extensions.producedAs +import dev.adamko.kotka.extensions.streams.forEach +import dev.adamko.kotka.extensions.streams.map import dev.adamko.kotka.kxs.serde import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -18,15 +24,41 @@ private val jsonMapper: Json = Json { } -fun playerUpdatesToWsServer( +private const val pid = "websocketBroadcast" + + +fun broadcastToWebsocket( websocketServer: WebsocketServer, builder: StreamsBuilder = StreamsBuilder(), ) { - builder.stream( - PlayerUpdate::class.topicName, - consumedAs("EventUpdate.Player-updates-for-ws-server", jsonMapper.serde(), jsonMapper.serde()), - ).foreach { _, value -> -// println("sending ${value.packetType} packet to websocket") - websocketServer.sendMessage(jsonMapper.encodeToString(value)) + + builder.preparePlayerUpdatesBroadcast() + + builder.stream( + TOPIC_BROADCAST_TO_WEBSOCKET, + consumedAs("$pid.packets-to-broadcast.consume", jsonMapper.serde(), jsonMapper.serde()), + ).forEach( + "$pid.websocket-server.send-message" + ) { _, esPacket -> + websocketServer.sendMessage(jsonMapper.encodeToString(esPacket)) } } + + +private fun StreamsBuilder.preparePlayerUpdatesBroadcast() { + stream( + PlayerUpdate::class.topicName, + consumedAs("$pid.player-updates", jsonMapper.serde(), jsonMapper.serde()), + ).map( + "$pid.player-updates.convert" + ) { serverId, playerPacket -> + serverId to EventServerPacket.Kafkatorio(playerPacket) + }.to( + TOPIC_BROADCAST_TO_WEBSOCKET, + producedAs( + "$pid.player-updates.stream", + jsonMapper.serde(FactorioServerId.serializer()), + jsonMapper.serde(EventServerPacket.serializer()), + ) + ) +} diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/prototypes.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/prototypes.kt index 7716819..56b9e7b 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/prototypes.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/prototypes.kt @@ -3,6 +3,7 @@ package dev.adamko.kafkatorio.processor.topology import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.schema.common.ColourHex import dev.adamko.kafkatorio.schema.common.FactorioPrototype +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.common.MapTile import dev.adamko.kafkatorio.schema.common.toHex import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate diff --git a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/saveMapTiles.kt b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/saveMapTiles.kt index c792940..970f1d2 100644 --- a/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/saveMapTiles.kt +++ b/modules/events-server/src/main/kotlin/dev/adamko/kafkatorio/processor/topology/saveMapTiles.kt @@ -3,11 +3,19 @@ package dev.adamko.kafkatorio.processor.topology import com.sksamuel.scrimage.ImmutableImage import com.sksamuel.scrimage.ScaleMethod import com.sksamuel.scrimage.nio.PngWriter +import dev.adamko.kafkatorio.processor.admin.TOPIC_BROADCAST_TO_WEBSOCKET import dev.adamko.kafkatorio.processor.admin.TOPIC_MAP_CHUNK_COLOURED_STATE +import dev.adamko.kafkatorio.processor.serdes.jsonMapper import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.schema.common.ColourHex +import dev.adamko.kafkatorio.schema.common.FactorioServerId +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId +import dev.adamko.kafkatorio.schema.common.TilePngFilename +import dev.adamko.kafkatorio.schema.packets.EventServerPacket import dev.adamko.kotka.extensions.consumedAs -import dev.adamko.kotka.extensions.streams.forEach +import dev.adamko.kotka.extensions.producedAs +import dev.adamko.kotka.extensions.streams.map +import dev.adamko.kotka.extensions.streams.mapValues import dev.adamko.kotka.kxs.serde import java.awt.image.BufferedImage import java.nio.file.Path @@ -41,15 +49,18 @@ fun saveMapTiles( ), ) - subdividedMapChunkTilesDebounced.forEachChunkSaveImage(tileDirectory) + val savedChunkFilenames: KStream = + subdividedMapChunkTilesDebounced.saveChunkAsImage(tileDirectory) + + savedChunkFilenames.broadcastSavedTileUpdates() return builder.build() } -private fun KStream>.forEachChunkSaveImage( +private fun KStream>.saveChunkAsImage( tileDirectory: Path, -) = forEach( +): KStream = mapValues( "$pid.save-tiles" ) { chunkId: ServerMapChunkId, tileColours: ServerMapChunkTiles -> @@ -77,6 +88,7 @@ private fun KStream>.forEachChu val filename = TilePngFilename(chunkId) saveTile(filename, tileDirectory, scaledImage) + filename } @@ -105,15 +117,17 @@ private fun saveTile( } -@JvmInline -private value class TilePngFilename( - val value: String, -) { - constructor(id: ServerMapChunkId) : this(buildString { - append("s${id.surfaceIndex}") - append("/z${id.chunkSize.zoomLevel}") - append("/x${id.chunkPosition.x}") - append("/y${id.chunkPosition.y}") - append(".png") - }) +private fun KStream.broadcastSavedTileUpdates() { + map( + "$pid.broadcast.filename-update.convert", + ) { id, filename -> + id.serverId to EventServerPacket.ChunkTileSaved(id, filename) + }.to( + TOPIC_BROADCAST_TO_WEBSOCKET, + producedAs( + "$pid.broadcast.filename-update.send", + jsonMapper.serde(FactorioServerId.serializer()), + jsonMapper.serde(EventServerPacket.serializer()), + ) + ) } diff --git a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/KxsBinaryTests.kt b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/KxsBinaryTests.kt index a2b77f0..eb6702c 100644 --- a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/KxsBinaryTests.kt +++ b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/KxsBinaryTests.kt @@ -1,11 +1,11 @@ package dev.adamko.kafkatorio.processor.misc import dev.adamko.kafkatorio.processor.serdes.kxsBinary -import dev.adamko.kafkatorio.processor.topology.FactorioServerId -import dev.adamko.kafkatorio.processor.topology.ServerMapChunkId +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ColourHex +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.common.MapChunkPosition import dev.adamko.kafkatorio.schema.common.MapTilePosition import dev.adamko.kafkatorio.schema.common.SurfaceIndex @@ -134,7 +134,8 @@ class KxsBinaryTests : FunSpec({ test("binary round trip") { val binaryEncoded = kxsBinary.encodeToByteArray(initial) - val binaryDecoded = kxsBinary.decodeFromByteArray>(binaryEncoded) + val binaryDecoded = + kxsBinary.decodeFromByteArray>(binaryEncoded) binaryDecoded shouldBeEqualToComparingFields initial } diff --git a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/serdeSize.kt b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/serdeSize.kt index aee7827..ec0591e 100644 --- a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/serdeSize.kt +++ b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/misc/serdeSize.kt @@ -1,10 +1,14 @@ package dev.adamko.kafkatorio.processor.misc import dev.adamko.kafkatorio.processor.serdes.kxsBinary -import dev.adamko.kafkatorio.processor.topology.FactorioServerId -import dev.adamko.kafkatorio.processor.topology.ServerMapChunkId +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles -import dev.adamko.kafkatorio.schema.common.* +import dev.adamko.kafkatorio.schema.common.ChunkSize +import dev.adamko.kafkatorio.schema.common.ColourHex +import dev.adamko.kafkatorio.schema.common.FactorioServerId +import dev.adamko.kafkatorio.schema.common.MapChunkPosition +import dev.adamko.kafkatorio.schema.common.MapTilePosition +import dev.adamko.kafkatorio.schema.common.SurfaceIndex import java.util.UUID import kotlinx.serialization.encodeToByteArray import org.junit.jupiter.api.Test diff --git a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/topology/SaveMapChunksTest.kt b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/topology/SaveMapChunksTest.kt index bc427b8..d1a8fb5 100644 --- a/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/topology/SaveMapChunksTest.kt +++ b/modules/events-server/src/test/kotlin/dev/adamko/kafkatorio/processor/topology/SaveMapChunksTest.kt @@ -3,8 +3,10 @@ package dev.adamko.kafkatorio.processor.topology import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ColourHex +import dev.adamko.kafkatorio.schema.common.FactorioServerId import dev.adamko.kafkatorio.schema.common.MapChunkPosition import dev.adamko.kafkatorio.schema.common.MapTilePosition +import dev.adamko.kafkatorio.schema.common.ServerMapChunkId import dev.adamko.kafkatorio.schema.common.SurfaceIndex import dev.adamko.kafkatorio.schema.common.iterateTiles import dev.adamko.kafkatorio.schema.common.toMapChunkPosition diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt index 6d054fd..f1de35a 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/App.kt @@ -1,5 +1,6 @@ package dev.adamko.kafkatorio.webmap +import dev.adamko.kafkatorio.schema.packets.EventServerPacket import dev.adamko.kafkatorio.webmap.state.FactorioGameState import dev.adamko.kafkatorio.webmap.state.FactorioUpdate import dev.adamko.kafkatorio.webmap.state.createFactorioReduxStore @@ -12,23 +13,34 @@ import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.html.div import io.kvision.maps.Maps -import io.kvision.maps.Maps.Companion.L import io.kvision.maps.externals.leaflet.layer.tile.GridLayer import io.kvision.module import io.kvision.panel.root import io.kvision.redux.ReduxStore import io.kvision.require import io.kvision.startApplication -import kotlin.time.Duration.Companion.seconds -import kotlinx.js.timers.setInterval -import org.w3c.dom.Image -import org.w3c.dom.asList +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + + +val rootJob = Job() class App( reduxStore: ReduxStore, wsService: WebsocketService, -) : Application() { +) : Application(), CoroutineScope { + + override val coroutineContext: CoroutineContext = + CoroutineName("WebMapApp") + Job(rootJob) + private var appState: MutableMap = mutableMapOf() var reduxStore: ReduxStore by appState @@ -40,8 +52,12 @@ class App( require("./css/kafkatorio.css") } + private val gameState: FactorioGameState + get() = reduxStore.getState() + private val kvMaps: Maps - get() = reduxStore.getState().map.kvMap + get() = gameState.map.kvMap + override fun start(state: Map) { this.appState = state.toMutableMap() @@ -62,35 +78,24 @@ class App( // this.asDynamic()._fadeAnimated = false } - setInterval(1.seconds) { - val doc = root.getElement()?.ownerDocument - val window = doc!!.defaultView!! + val doc = root.getElement()?.ownerDocument + val window = doc!!.defaultView!! - doc - .querySelectorAll("img.leaflet-tile-loaded") - .asList() - .filterIsInstance() - .map { img -> - val imgSrc = img.src.substringBeforeLast('?') -// println("fetching $imgSrc") + wsService.packetsFlow + .filterIsInstance() + .onEach { + println("[packetsFlow] triggering tile refresh ${it.filename.value}") + gameState.map.refreshUpdatedTilePng(doc, window, it.filename) + }.launchIn(this) - var newImg: Image? = Image() - - newImg!!.onload = { - - window.requestAnimationFrame { -// println("image loaded ${newImg!!.src}") - img.setAttribute("dynamic-reload", "true") - img.src = newImg!!.src - newImg = null // try to encourage garbage collection - } - } - newImg!!.src = imgSrc //+ "?t=${currentTimeMillis()}" - } - } } - override fun dispose(): Map = appState + override fun dispose(): Map { + rootJob.cancelChildren() + rootJob.cancel("app dispose") + rootJob.complete() + return appState + } } @@ -104,7 +109,7 @@ fun main() { ) App( reduxStore = reduxStore, - wsService = wsService + wsService = wsService, ) }, module.hot, diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt index 93de604..a0e82f1 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/map.kt @@ -1,5 +1,6 @@ package dev.adamko.kafkatorio.webmap +import dev.adamko.kafkatorio.schema.common.TilePngFilename import io.kvision.maps.Maps import io.kvision.maps.Maps.Companion.L import io.kvision.maps.externals.leaflet.DoneCallback @@ -12,7 +13,19 @@ import io.kvision.maps.externals.leaflet.layer.LayerGroup import io.kvision.maps.externals.leaflet.layer.tile.TileLayer import io.kvision.utils.obj import io.kvision.utils.px +import kotlinx.coroutines.await +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.w3c.dom.Document import org.w3c.dom.HTMLElement +import org.w3c.dom.Image +import org.w3c.dom.Window +import org.w3c.dom.asList +import org.w3c.fetch.NO_CORS +import org.w3c.fetch.RELOAD +import org.w3c.fetch.RequestCache +import org.w3c.fetch.RequestInit +import org.w3c.fetch.RequestMode class FactorioMap( @@ -64,6 +77,49 @@ class FactorioMap( } + suspend fun refreshUpdatedTilePng( + document: Document, + window: Window, + tilePngFilename: TilePngFilename, + ): Unit = coroutineScope { + launch { + document + .querySelectorAll("img.leaflet-tile-loaded") + .asList() + .filterIsInstance() + .filter { img -> + tilePngFilename.value in img.src + } + .forEach { img -> + val imgSrc = img.src //.substringBeforeLast('?') + + window.fetch( + imgSrc, + RequestInit( + cache = RequestCache.RELOAD, + mode = RequestMode.NO_CORS, + ) + ).then { + println("[refreshUpdatedTilePng] updating $imgSrc") + img.setAttribute(DYNAMIC_RELOAD_ATT, "true") + img.src = imgSrc + }.await() + } + } + +//// println("fetching $imgSrc") +// val newImg = Image() +// newImg.onload = { +//// window.requestAnimationFrame { +//// println("image loaded ${newImg!!.src}") +// img.setAttribute(DYNAMIC_RELOAD_ATT, "true") +// img.src = newImg.src +// Unit +//// } +// } +// newImg.src = imgSrc //+ "?t=${currentTimeMillis()}" + } + private fun buildFactorioTerrainLayer(): TileLayer { val baseTileLayer = L.tileLayer( @@ -87,8 +143,8 @@ class FactorioMap( val currentTileOnLoad: (DoneCallback, HTMLElement) -> Unit = baseTileLayer.asDynamic()._tileOnLoad as (DoneCallback, HTMLElement) -> Unit baseTileLayer.asDynamic()._tileOnLoad = { done: DoneCallback, tile: HTMLElement -> - if (tile.hasAttribute("dynamic-reload")) { - tile.removeAttribute("dynamic-reload") + if (tile.hasAttribute(DYNAMIC_RELOAD_ATT)) { + tile.removeAttribute(DYNAMIC_RELOAD_ATT) } else { currentTileOnLoad(done, tile) } @@ -96,4 +152,8 @@ class FactorioMap( return baseTileLayer } + + companion object { + const val DYNAMIC_RELOAD_ATT = "dynamic-reload" + } } diff --git a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/websockets.kt b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/websockets.kt index 51e7708..fc9ca41 100644 --- a/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/websockets.kt +++ b/modules/web-map/src/main/kotlin/dev/adamko/kafkatorio/webmap/websockets.kt @@ -4,7 +4,7 @@ import dev.adamko.kafkatorio.schema.packets.ConfigurationUpdate import dev.adamko.kafkatorio.schema.packets.ConsoleChatUpdate import dev.adamko.kafkatorio.schema.packets.ConsoleCommandUpdate import dev.adamko.kafkatorio.schema.packets.EntityUpdate -import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket +import dev.adamko.kafkatorio.schema.packets.EventServerPacket import dev.adamko.kafkatorio.schema.packets.MapChunkUpdate import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate @@ -12,45 +12,72 @@ import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate import dev.adamko.kafkatorio.webmap.state.FactorioGameState import dev.adamko.kafkatorio.webmap.state.FactorioUpdate import io.kvision.redux.ReduxStore +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch import org.w3c.dom.MessageEvent import org.w3c.dom.WebSocket class WebsocketService( - private val wsUrl: String, + wsUrl: String, private val reduxStore: ReduxStore, -) { +) : CoroutineScope { + + override val coroutineContext: CoroutineContext = + CoroutineName("WebsocketService") + Job(rootJob) + private val ws = WebSocket(wsUrl) + private val _packetsFlow = MutableSharedFlow() + val packetsFlow: SharedFlow + get() = _packetsFlow.asSharedFlow() + init { ws.onmessage = ::handleMessageEvent } private fun handleMessageEvent(msg: MessageEvent) { - val data = msg.data as? String + val data = (msg.data as? String)?.trim() if ( data != null - && data.trim().run { startsWith("{") && endsWith("}") } + && data.startsWith("{") + && data.endsWith("}") ) { println(data.replace('\n', ' ')) - val packet = jsonMapper.decodeFromString(KafkatorioPacket.serializer(), data) + val packet = jsonMapper.decodeFromString(EventServerPacket.serializer(), data) - when (val packetData = packet.data) { + launch { + _packetsFlow.emit(packet) + } - is PlayerUpdate -> reduxStore.dispatch(FactorioUpdate.Player(packet.tick, packetData)) - is ConfigurationUpdate, - is ConsoleChatUpdate, - is ConsoleCommandUpdate, - is PrototypesUpdate, - is SurfaceUpdate, - is EntityUpdate, - is MapChunkUpdate -> { - // to be continued... - } + when (packet) { + is EventServerPacket.ChunkTileSaved -> {} + + is EventServerPacket.Kafkatorio -> + when (val packetData = packet.packet.data) { + is PlayerUpdate -> reduxStore.dispatch( + FactorioUpdate.Player(packet.packet.tick, packetData) + ) + is ConfigurationUpdate, + is ConsoleChatUpdate, + is ConsoleCommandUpdate, + is PrototypesUpdate, + is SurfaceUpdate, + is EntityUpdate, + is MapChunkUpdate -> { + // to be continued... + } + } } } else { println(data)