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 name: String,
override val protoType: String, override val protoType: String,
) : EntityIdentifiers ) : 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 package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.schema.common.MapTileDictionary.PrototypeKey import dev.adamko.kafkatorio.schema.common.MapTileDictionary.PrototypeKey
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32 const val MAP_CHUNK_SIZE = 32
@ -69,3 +69,27 @@ data class MapBoundingBox(
val topLeft: MapTilePosition, val topLeft: MapTilePosition,
val bottomRight: 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.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.topology.colourMapChunks import dev.adamko.kafkatorio.processor.topology.colourMapChunks
import dev.adamko.kafkatorio.processor.topology.factorioServerPacketStream 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.saveMapTiles
import dev.adamko.kafkatorio.processor.topology.splitFactorioServerPacketStream import dev.adamko.kafkatorio.processor.topology.splitFactorioServerPacketStream
import java.time.Duration import java.time.Duration
@ -39,7 +39,7 @@ internal class KafkatorioTopology(
groupTilesMapChunks() groupTilesMapChunks()
saveTiles() saveTiles()
playerUpdates() websocketBroadcaster()
} }
private fun splitPackets() { private fun splitPackets() {
@ -64,13 +64,13 @@ internal class KafkatorioTopology(
launchTopology("saveTiles", topology) launchTopology("saveTiles", topology)
} }
private fun playerUpdates() { private fun websocketBroadcaster() {
val builder = StreamsBuilder() val builder = StreamsBuilder()
playerUpdatesToWsServer(websocketServer, builder) broadcastToWebsocket(websocketServer, builder)
val topology = builder.build() val topology = builder.build()
launchTopology("playerUpdates", topology) launchTopology("websocketBroadcaster", topology)
} }
private fun launchTopology( 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" 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 { fun allTopics(): Set<String> = buildSet {
add(TOPIC_SRC_SERVER_LOG) add(TOPIC_SRC_SERVER_LOG)
@ -38,6 +41,8 @@ fun allTopics(): Set<String> = buildSet {
add(TOPIC_SUBDIVIDED_MAP_TILES) add(TOPIC_SUBDIVIDED_MAP_TILES)
add(TOPIC_SUBDIVIDED_MAP_TILES_DEBOUNCED) add(TOPIC_SUBDIVIDED_MAP_TILES_DEBOUNCED)
add(TOPIC_BROADCAST_TO_WEBSOCKET)
addAll(packetTopicNames.values) addAll(packetTopicNames.values)
} }

View file

@ -100,19 +100,19 @@ internal class WebMapTileServer(
currentETag == null -> { currentETag == null -> {
// println("[${request.etagKey}] currentETag is null (requested: $requestedETag), executing request") // println("[${request.etagKey}] currentETag is null (requested: $requestedETag), executing request")
response response
.header("Cache-Control", "no-cache") .header("Cache-Control", "no-cache, max-age=0, must-revalidate")
} }
requestedETag == currentETag -> { requestedETag == currentETag -> {
// println("[${request.etagKey}] currentETag == requested, returning 304 $requestedETag") // println("[${request.etagKey}] currentETag == requested, returning 304 $requestedETag")
Response(Status.NOT_MODIFIED) Response(Status.NOT_MODIFIED)
.header("Cache-Control", "no-cache") .header("Cache-Control", "no-cache, max-age=0, must-revalidate")
} }
else -> { else -> {
// println("[${request.etagKey}] currentETag != requested, executing request and adding currentETag:$currentETag (requested:$requestedETag)") // println("[${request.etagKey}] currentETag != requested, executing request and adding currentETag:$currentETag (requested:$requestedETag)")
response response
.header("ETag", currentETag) .header("ETag", currentETag)
// .header("Cache-Control", "no-store") // .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.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex 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.leftTopTile
import dev.adamko.kafkatorio.schema.common.toMapChunkPosition import dev.adamko.kafkatorio.schema.common.toMapChunkPosition
import dev.adamko.kafkatorio.schema.packets.MapChunkUpdate 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.TOPIC_SRC_SERVER_LOG
import dev.adamko.kafkatorio.processor.admin.topicName import dev.adamko.kafkatorio.processor.admin.topicName
import dev.adamko.kafkatorio.processor.serdes.jsonMapper 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.KafkatorioPacket
import dev.adamko.kotka.extensions.consumedAs import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.producedAs import dev.adamko.kotka.extensions.producedAs

View file

@ -1,32 +1,14 @@
package dev.adamko.kafkatorio.processor.topology package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.color.RGBColor 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.ColourHex
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTile import dev.adamko.kafkatorio.schema.common.MapTile
import dev.adamko.kafkatorio.schema.common.MapTilePosition 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 java.awt.Color
import kotlinx.serialization.Serializable 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 @Serializable
data class ServerMapChunkTiles<Data>( data class ServerMapChunkTiles<Data>(
val chunkId: ServerMapChunkId, 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.admin.topicName
import dev.adamko.kafkatorio.processor.serdes.jsonMapper 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.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacketData import dev.adamko.kafkatorio.schema.packets.KafkatorioPacketData
import dev.adamko.kotka.extensions.consumedAs import dev.adamko.kotka.extensions.consumedAs

View file

@ -1,11 +1,17 @@
package dev.adamko.kafkatorio.processor.topology package dev.adamko.kafkatorio.processor.topology
import dev.adamko.kafkatorio.processor.WebsocketServer 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.processor.admin.topicName
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio 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.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kotka.extensions.consumedAs 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 dev.adamko.kotka.kxs.serde
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json 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, websocketServer: WebsocketServer,
builder: StreamsBuilder = StreamsBuilder(), builder: StreamsBuilder = StreamsBuilder(),
) { ) {
builder.stream<FactorioServerId, KafkatorioPacket>(
PlayerUpdate::class.topicName, builder.preparePlayerUpdatesBroadcast()
consumedAs("EventUpdate.Player-updates-for-ws-server", jsonMapper.serde(), jsonMapper.serde()),
).foreach { _, value -> builder.stream<FactorioServerId, EventServerPacket>(
// println("sending ${value.packetType} packet to websocket") TOPIC_BROADCAST_TO_WEBSOCKET,
websocketServer.sendMessage(jsonMapper.encodeToString(value)) 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.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.schema.common.ColourHex import dev.adamko.kafkatorio.schema.common.ColourHex
import dev.adamko.kafkatorio.schema.common.FactorioPrototype 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.MapTile
import dev.adamko.kafkatorio.schema.common.toHex import dev.adamko.kafkatorio.schema.common.toHex
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate 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.ImmutableImage
import com.sksamuel.scrimage.ScaleMethod import com.sksamuel.scrimage.ScaleMethod
import com.sksamuel.scrimage.nio.PngWriter 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.admin.TOPIC_MAP_CHUNK_COLOURED_STATE
import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.schema.common.ColourHex 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.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 dev.adamko.kotka.kxs.serde
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.nio.file.Path 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() return builder.build()
} }
private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.forEachChunkSaveImage( private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.saveChunkAsImage(
tileDirectory: Path, tileDirectory: Path,
) = forEach( ): KStream<ServerMapChunkId, TilePngFilename> = mapValues(
"$pid.save-tiles" "$pid.save-tiles"
) { chunkId: ServerMapChunkId, tileColours: ServerMapChunkTiles<ColourHex> -> ) { chunkId: ServerMapChunkId, tileColours: ServerMapChunkTiles<ColourHex> ->
@ -77,6 +88,7 @@ private fun KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>.forEachChu
val filename = TilePngFilename(chunkId) val filename = TilePngFilename(chunkId)
saveTile(filename, tileDirectory, scaledImage) saveTile(filename, tileDirectory, scaledImage)
filename
} }
@ -105,15 +117,17 @@ private fun saveTile(
} }
@JvmInline private fun KStream<ServerMapChunkId, TilePngFilename>.broadcastSavedTileUpdates() {
private value class TilePngFilename( map<ServerMapChunkId, TilePngFilename, FactorioServerId, EventServerPacket>(
val value: String, "$pid.broadcast.filename-update.convert",
) { ) { id, filename ->
constructor(id: ServerMapChunkId) : this(buildString { id.serverId to EventServerPacket.ChunkTileSaved(id, filename)
append("s${id.surfaceIndex}") }.to(
append("/z${id.chunkSize.zoomLevel}") TOPIC_BROADCAST_TO_WEBSOCKET,
append("/x${id.chunkPosition.x}") producedAs(
append("/y${id.chunkPosition.y}") "$pid.broadcast.filename-update.send",
append(".png") jsonMapper.serde(FactorioServerId.serializer()),
}) jsonMapper.serde(EventServerPacket.serializer()),
)
)
} }

View file

@ -1,11 +1,11 @@
package dev.adamko.kafkatorio.processor.misc package dev.adamko.kafkatorio.processor.misc
import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.processor.topology.FactorioServerId import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles
import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex 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.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTilePosition import dev.adamko.kafkatorio.schema.common.MapTilePosition
import dev.adamko.kafkatorio.schema.common.SurfaceIndex import dev.adamko.kafkatorio.schema.common.SurfaceIndex
@ -134,7 +134,8 @@ class KxsBinaryTests : FunSpec({
test("binary round trip") { test("binary round trip") {
val binaryEncoded = kxsBinary.encodeToByteArray(initial) val binaryEncoded = kxsBinary.encodeToByteArray(initial)
val binaryDecoded = kxsBinary.decodeFromByteArray<ServerMapChunkTiles<ColourHex>>(binaryEncoded) val binaryDecoded =
kxsBinary.decodeFromByteArray<ServerMapChunkTiles<ColourHex>>(binaryEncoded)
binaryDecoded shouldBeEqualToComparingFields initial binaryDecoded shouldBeEqualToComparingFields initial
} }

View file

@ -1,10 +1,14 @@
package dev.adamko.kafkatorio.processor.misc package dev.adamko.kafkatorio.processor.misc
import dev.adamko.kafkatorio.processor.serdes.kxsBinary import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.processor.topology.FactorioServerId import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles 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 java.util.UUID
import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.encodeToByteArray
import org.junit.jupiter.api.Test 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.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.schema.common.ChunkSize import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex 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.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTilePosition 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.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.iterateTiles import dev.adamko.kafkatorio.schema.common.iterateTiles
import dev.adamko.kafkatorio.schema.common.toMapChunkPosition import dev.adamko.kafkatorio.schema.common.toMapChunkPosition

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.webmap 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.FactorioGameState
import dev.adamko.kafkatorio.webmap.state.FactorioUpdate import dev.adamko.kafkatorio.webmap.state.FactorioUpdate
import dev.adamko.kafkatorio.webmap.state.createFactorioReduxStore import dev.adamko.kafkatorio.webmap.state.createFactorioReduxStore
@ -12,23 +13,34 @@ import io.kvision.CoreModule
import io.kvision.FontAwesomeModule import io.kvision.FontAwesomeModule
import io.kvision.html.div import io.kvision.html.div
import io.kvision.maps.Maps import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.layer.tile.GridLayer import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
import io.kvision.module import io.kvision.module
import io.kvision.panel.root import io.kvision.panel.root
import io.kvision.redux.ReduxStore import io.kvision.redux.ReduxStore
import io.kvision.require import io.kvision.require
import io.kvision.startApplication import io.kvision.startApplication
import kotlin.time.Duration.Companion.seconds import kotlin.coroutines.CoroutineContext
import kotlinx.js.timers.setInterval import kotlinx.coroutines.CoroutineName
import org.w3c.dom.Image import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.asList 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( class App(
reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>, reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
wsService: WebsocketService, wsService: WebsocketService,
) : Application() { ) : Application(), CoroutineScope {
override val coroutineContext: CoroutineContext =
CoroutineName("WebMapApp") + Job(rootJob)
private var appState: MutableMap<String, Any> = mutableMapOf() private var appState: MutableMap<String, Any> = mutableMapOf()
var reduxStore: ReduxStore<FactorioGameState, FactorioUpdate> by appState var reduxStore: ReduxStore<FactorioGameState, FactorioUpdate> by appState
@ -40,8 +52,12 @@ class App(
require("./css/kafkatorio.css") require("./css/kafkatorio.css")
} }
private val gameState: FactorioGameState
get() = reduxStore.getState()
private val kvMaps: Maps private val kvMaps: Maps
get() = reduxStore.getState().map.kvMap get() = gameState.map.kvMap
override fun start(state: Map<String, Any>) { override fun start(state: Map<String, Any>) {
this.appState = state.toMutableMap() this.appState = state.toMutableMap()
@ -62,35 +78,24 @@ class App(
// this.asDynamic()._fadeAnimated = false // this.asDynamic()._fadeAnimated = false
} }
setInterval(1.seconds) { val doc = root.getElement()?.ownerDocument
val doc = root.getElement()?.ownerDocument val window = doc!!.defaultView!!
val window = doc!!.defaultView!!
doc wsService.packetsFlow
.querySelectorAll("img.leaflet-tile-loaded") .filterIsInstance<EventServerPacket.ChunkTileSaved>()
.asList() .onEach {
.filterIsInstance<Image>() println("[packetsFlow] triggering tile refresh ${it.filename.value}")
.map { img -> gameState.map.refreshUpdatedTilePng(doc, window, it.filename)
val imgSrc = img.src.substringBeforeLast('?') }.launchIn(this)
// println("fetching $imgSrc")
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( App(
reduxStore = reduxStore, reduxStore = reduxStore,
wsService = wsService wsService = wsService,
) )
}, },
module.hot, module.hot,

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.webmap package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.schema.common.TilePngFilename
import io.kvision.maps.Maps import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.DoneCallback 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.maps.externals.leaflet.layer.tile.TileLayer
import io.kvision.utils.obj import io.kvision.utils.obj
import io.kvision.utils.px 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.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( 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> { private fun buildFactorioTerrainLayer(): TileLayer<TileLayer.TileLayerOptions> {
val baseTileLayer = L.tileLayer( val baseTileLayer = L.tileLayer(
@ -87,8 +143,8 @@ class FactorioMap(
val currentTileOnLoad: (DoneCallback, HTMLElement) -> Unit = val currentTileOnLoad: (DoneCallback, HTMLElement) -> Unit =
baseTileLayer.asDynamic()._tileOnLoad as (DoneCallback, HTMLElement) -> Unit baseTileLayer.asDynamic()._tileOnLoad as (DoneCallback, HTMLElement) -> Unit
baseTileLayer.asDynamic()._tileOnLoad = { done: DoneCallback, tile: HTMLElement -> baseTileLayer.asDynamic()._tileOnLoad = { done: DoneCallback, tile: HTMLElement ->
if (tile.hasAttribute("dynamic-reload")) { if (tile.hasAttribute(DYNAMIC_RELOAD_ATT)) {
tile.removeAttribute("dynamic-reload") tile.removeAttribute(DYNAMIC_RELOAD_ATT)
} else { } else {
currentTileOnLoad(done, tile) currentTileOnLoad(done, tile)
} }
@ -96,4 +152,8 @@ class FactorioMap(
return baseTileLayer 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.ConsoleChatUpdate
import dev.adamko.kafkatorio.schema.packets.ConsoleCommandUpdate import dev.adamko.kafkatorio.schema.packets.ConsoleCommandUpdate
import dev.adamko.kafkatorio.schema.packets.EntityUpdate 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.MapChunkUpdate
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate 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.FactorioGameState
import dev.adamko.kafkatorio.webmap.state.FactorioUpdate import dev.adamko.kafkatorio.webmap.state.FactorioUpdate
import io.kvision.redux.ReduxStore 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.MessageEvent
import org.w3c.dom.WebSocket import org.w3c.dom.WebSocket
class WebsocketService( class WebsocketService(
private val wsUrl: String, wsUrl: String,
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>, private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
) { ) : CoroutineScope {
override val coroutineContext: CoroutineContext =
CoroutineName("WebsocketService") + Job(rootJob)
private val ws = WebSocket(wsUrl) private val ws = WebSocket(wsUrl)
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
val packetsFlow: SharedFlow<EventServerPacket>
get() = _packetsFlow.asSharedFlow()
init { init {
ws.onmessage = ::handleMessageEvent ws.onmessage = ::handleMessageEvent
} }
private fun handleMessageEvent(msg: MessageEvent) { private fun handleMessageEvent(msg: MessageEvent) {
val data = msg.data as? String val data = (msg.data as? String)?.trim()
if ( if (
data != null data != null
&& data.trim().run { startsWith("{") && endsWith("}") } && data.startsWith("{")
&& data.endsWith("}")
) { ) {
println(data.replace('\n', ' ')) 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)) when (packet) {
is ConfigurationUpdate, is EventServerPacket.ChunkTileSaved -> {}
is ConsoleChatUpdate,
is ConsoleCommandUpdate, is EventServerPacket.Kafkatorio ->
is PrototypesUpdate, when (val packetData = packet.packet.data) {
is SurfaceUpdate, is PlayerUpdate -> reduxStore.dispatch(
is EntityUpdate, FactorioUpdate.Player(packet.packet.tick, packetData)
is MapChunkUpdate -> { )
// to be continued... is ConfigurationUpdate,
} is ConsoleChatUpdate,
is ConsoleCommandUpdate,
is PrototypesUpdate,
is SurfaceUpdate,
is EntityUpdate,
is MapChunkUpdate -> {
// to be continued...
}
}
} }
} else { } else {
println(data) println(data)