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

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