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
This commit is contained in:
Adam 2022-05-25 12:50:34 +02:00
parent b15382660b
commit 8c37a0cef9
19 changed files with 303 additions and 108 deletions

View file

@ -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
}

View file

@ -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")
})
}

View file

@ -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()
}

View file

@ -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(

View file

@ -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<String> = buildSet {
add(TOPIC_SRC_SERVER_LOG)
@ -38,6 +41,8 @@ fun allTopics(): Set<String> = buildSet {
add(TOPIC_SUBDIVIDED_MAP_TILES)
add(TOPIC_SUBDIVIDED_MAP_TILES_DEBOUNCED)
add(TOPIC_BROADCAST_TO_WEBSOCKET)
addAll(packetTopicNames.values)
}

View file

@ -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")
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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<Data>(
val chunkId: ServerMapChunkId,

View file

@ -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

View file

@ -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<FactorioServerId, KafkatorioPacket>(
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<FactorioServerId, EventServerPacket>(
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<FactorioServerId, KafkatorioPacket>(
PlayerUpdate::class.topicName,
consumedAs("$pid.player-updates", jsonMapper.serde(), jsonMapper.serde()),
).map<FactorioServerId, KafkatorioPacket, FactorioServerId, EventServerPacket>(
"$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()),
)
)
}

View file

@ -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

View file

@ -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<ServerMapChunkId, TilePngFilename> =
subdividedMapChunkTilesDebounced.saveChunkAsImage(tileDirectory)
savedChunkFilenames.broadcastSavedTileUpdates()
return builder.build()
}
private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.forEachChunkSaveImage(
private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.saveChunkAsImage(
tileDirectory: Path,
) = forEach(
): KStream<ServerMapChunkId, TilePngFilename> = mapValues(
"$pid.save-tiles"
) { chunkId: ServerMapChunkId, tileColours: ServerMapChunkTiles<ColourHex> ->
@ -77,6 +88,7 @@ private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.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<ServerMapChunkId, TilePngFilename>.broadcastSavedTileUpdates() {
map<ServerMapChunkId, TilePngFilename, FactorioServerId, EventServerPacket>(
"$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()),
)
)
}

View file

@ -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<ServerMapChunkTiles<ColourHex>>(binaryEncoded)
val binaryDecoded =
kxsBinary.decodeFromByteArray<ServerMapChunkTiles<ColourHex>>(binaryEncoded)
binaryDecoded shouldBeEqualToComparingFields initial
}

View file

@ -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

View file

@ -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

View file

@ -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<FactorioGameState, FactorioUpdate>,
wsService: WebsocketService,
) : Application() {
) : Application(), CoroutineScope {
override val coroutineContext: CoroutineContext =
CoroutineName("WebMapApp") + Job(rootJob)
private var appState: MutableMap<String, Any> = mutableMapOf()
var reduxStore: ReduxStore<FactorioGameState, FactorioUpdate> 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<String, Any>) {
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<Image>()
.map { img ->
val imgSrc = img.src.substringBeforeLast('?')
// println("fetching $imgSrc")
wsService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.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<String, Any> = appState
override fun dispose(): Map<String, Any> {
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,

View file

@ -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<Image>()
.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<TileLayer.TileLayerOptions> {
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"
}
}

View file

@ -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<FactorioGameState, FactorioUpdate>,
) {
) : CoroutineScope {
override val coroutineContext: CoroutineContext =
CoroutineName("WebsocketService") + Job(rootJob)
private val ws = WebSocket(wsUrl)
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
val packetsFlow: SharedFlow<EventServerPacket>
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)