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:
parent
b15382660b
commit
8c37a0cef9
19 changed files with 303 additions and 108 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue