initial hacky implementation of multi-server support in the frontend
This commit is contained in:
parent
60d61752b3
commit
8a6c49a74b
14 changed files with 452 additions and 161 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
package dev.adamko.kafkatorio.webmap
|
package dev.adamko.kafkatorio.webmap
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
|
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
|
||||||
import dev.adamko.kafkatorio.webmap.state.FactorioGameState
|
import dev.adamko.kafkatorio.webmap.layout.headerNav
|
||||||
import dev.adamko.kafkatorio.webmap.state.FactorioUpdate
|
import dev.adamko.kafkatorio.webmap.layout.homePage
|
||||||
import dev.adamko.kafkatorio.webmap.state.createFactorioReduxStore
|
import dev.adamko.kafkatorio.webmap.layout.serverPage
|
||||||
|
import dev.adamko.kafkatorio.webmap.services.WebsocketService
|
||||||
import io.kvision.Application
|
import io.kvision.Application
|
||||||
import io.kvision.BootstrapCssModule
|
import io.kvision.BootstrapCssModule
|
||||||
import io.kvision.BootstrapIconsModule
|
import io.kvision.BootstrapIconsModule
|
||||||
|
|
@ -12,17 +13,21 @@ import io.kvision.ChartModule
|
||||||
import io.kvision.CoreModule
|
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.html.header
|
||||||
import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
|
import io.kvision.html.main
|
||||||
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.redux.createReduxStore
|
||||||
import io.kvision.require
|
import io.kvision.require
|
||||||
|
import io.kvision.routing.Routing
|
||||||
import io.kvision.startApplication
|
import io.kvision.startApplication
|
||||||
import kotlin.coroutines.CoroutineContext
|
import io.kvision.state.bind
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
|
@ -33,54 +38,78 @@ import kotlinx.coroutines.flow.onEach
|
||||||
val rootJob = Job()
|
val rootJob = Job()
|
||||||
|
|
||||||
|
|
||||||
class App(
|
class App : Application() {
|
||||||
reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
|
|
||||||
wsService: WebsocketService,
|
|
||||||
) : Application(), CoroutineScope {
|
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext = CoroutineName("WebMapApp")
|
private val coroutineScope: CoroutineScope = CoroutineScope(
|
||||||
|
Dispatchers.Default
|
||||||
|
+ SupervisorJob(rootJob)
|
||||||
|
+ CoroutineName("WebMapApp")
|
||||||
|
)
|
||||||
|
|
||||||
private var appState: MutableMap<String, Any> = mutableMapOf()
|
private var appState: MutableMap<String, Any> = mutableMapOf()
|
||||||
|
|
||||||
var reduxStore: ReduxStore<FactorioGameState, FactorioUpdate> by appState
|
var reduxStore: ReduxStore<SiteState, SiteAction> by appState
|
||||||
var wsService: WebsocketService by appState
|
private set
|
||||||
|
// var wsService: WebsocketService by appState
|
||||||
|
// private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.reduxStore = reduxStore
|
this.reduxStore = createReduxStore(SiteState::plus, SiteState())
|
||||||
this.wsService = wsService
|
// this.wsService = wsService
|
||||||
require("./css/kafkatorio.css")
|
require("./css/kafkatorio.css")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gameState: FactorioGameState
|
// private val gameState: FactorioGameState
|
||||||
get() = reduxStore.getState()
|
// get() = reduxStore.getState()
|
||||||
|
|
||||||
private val kvMaps: Maps
|
|
||||||
get() = gameState.map.kvMap
|
|
||||||
|
|
||||||
|
// private val kvMaps: Maps
|
||||||
|
// get() = gameState.map.kvMap
|
||||||
|
|
||||||
override fun start(state: Map<String, Any>) {
|
override fun start(state: Map<String, Any>) {
|
||||||
|
Routing.init(useHash = false)
|
||||||
|
|
||||||
this.appState = state.toMutableMap()
|
this.appState = state.toMutableMap()
|
||||||
|
|
||||||
|
val siteRouting = SiteRouting(reduxStore)
|
||||||
|
val websocketService = WebsocketService(reduxStore)
|
||||||
|
|
||||||
|
siteRouting.init()
|
||||||
|
|
||||||
root("kvapp") {
|
root("kvapp") {
|
||||||
|
|
||||||
|
header().bind(siteRouting.siteStateStore) { state ->
|
||||||
|
headerNav(state)
|
||||||
|
}
|
||||||
|
|
||||||
div("Kafkatorio Web Map")
|
div("Kafkatorio Web Map")
|
||||||
add(kvMaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
kvMaps.leafletMap {
|
main().bind(siteRouting.siteStateStore) { state ->
|
||||||
invalidateSize(true)
|
when (state.view) {
|
||||||
eachLayer({
|
SiteView.HOME -> homePage(state)
|
||||||
if (it is GridLayer<*>) {
|
SiteView.SERVER -> serverPage(state)
|
||||||
it.redraw()
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// div("Kafkatorio Web Map")
|
||||||
|
// add(kvMaps)
|
||||||
}
|
}
|
||||||
|
|
||||||
wsService.packetsFlow
|
// kvMaps.leafletMap {
|
||||||
|
// invalidateSize(true)
|
||||||
|
// eachLayer({
|
||||||
|
// if (it is GridLayer<*>) {
|
||||||
|
// it.redraw()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
websocketService.packetsFlow
|
||||||
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
|
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
|
||||||
.onEach { tileSavedEvent ->
|
.onEach { tileSavedEvent ->
|
||||||
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
|
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
|
||||||
gameState.map.refreshUpdatedTilePng(tileSavedEvent.filename)
|
reduxStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
|
||||||
}.launchIn(this)
|
// reduxStore.map.refreshUpdatedTilePng(tileSavedEvent.filename)
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,14 +125,13 @@ class App(
|
||||||
fun main() {
|
fun main() {
|
||||||
startApplication(
|
startApplication(
|
||||||
{
|
{
|
||||||
val reduxStore = createFactorioReduxStore()
|
// val reduxStore = createFactorioReduxStore()
|
||||||
val wsService = WebsocketService(
|
// val wsService = WebsocketService(
|
||||||
// "ws://localhost:9073/ws", // TODO load from config
|
// reduxStore = reduxStore,
|
||||||
reduxStore = reduxStore,
|
// )
|
||||||
)
|
|
||||||
App(
|
App(
|
||||||
reduxStore = reduxStore,
|
// reduxStore = reduxStore,
|
||||||
wsService = wsService,
|
// wsService = wsService,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
module.hot,
|
module.hot,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.adamko.kafkatorio.webmap
|
package dev.adamko.kafkatorio.webmap.config
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
|
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap.layout
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.webmap.SiteState
|
||||||
|
import dev.adamko.kafkatorio.webmap.SiteView
|
||||||
|
import io.kvision.core.Container
|
||||||
|
import io.kvision.html.div
|
||||||
|
import io.kvision.html.link
|
||||||
|
import io.kvision.html.nav
|
||||||
|
|
||||||
|
fun Container.headerNav(state: SiteState) {
|
||||||
|
nav(className = "navbar") {
|
||||||
|
div(className = "container") {
|
||||||
|
link("home", "#${SiteView.HOME.pathMatcher}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap.layout
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.webmap.SiteState
|
||||||
|
import io.kvision.core.Container
|
||||||
|
import io.kvision.html.div
|
||||||
|
import io.kvision.html.h5
|
||||||
|
import io.kvision.html.link
|
||||||
|
|
||||||
|
|
||||||
|
fun Container.homePage(state: SiteState) {
|
||||||
|
div(className = "home-page") {
|
||||||
|
servers(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Container.servers(state: SiteState) {
|
||||||
|
div(className = "servers") {
|
||||||
|
h5("Servers")
|
||||||
|
if (state.serverIds != null) {
|
||||||
|
state.serverIds.forEach { serverId ->
|
||||||
|
div {
|
||||||
|
link("server", "/servers/$serverId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
div("no servers available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap.layout
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.webmap.SiteState
|
||||||
|
import io.kvision.core.Container
|
||||||
|
import io.kvision.html.div
|
||||||
|
import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
|
||||||
|
|
||||||
|
|
||||||
|
fun Container.serverPage(state: SiteState) {
|
||||||
|
val factorioGameState = state.factorioGameState
|
||||||
|
div(className = "server-page") {
|
||||||
|
if (factorioGameState != null) {
|
||||||
|
|
||||||
|
add(factorioGameState.map.kvMap)
|
||||||
|
|
||||||
|
factorioGameState.map.kvMap.leafletMap {
|
||||||
|
invalidateSize(true)
|
||||||
|
eachLayer({
|
||||||
|
if (it is GridLayer<*>) {
|
||||||
|
it.redraw()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.adamko.kafkatorio.webmap
|
package dev.adamko.kafkatorio.webmap
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||||
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
|
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
|
||||||
import dev.adamko.kafkatorio.schema.common.ServerMapTilePngFilename
|
import dev.adamko.kafkatorio.schema.common.ServerMapTilePngFilename
|
||||||
import dev.adamko.kafkatorio.webmap.externals.TileOnLoadFn
|
import dev.adamko.kafkatorio.webmap.externals.TileOnLoadFn
|
||||||
|
|
@ -31,7 +32,9 @@ import org.w3c.fetch.RequestInit
|
||||||
import org.w3c.fetch.RequestMode
|
import org.w3c.fetch.RequestMode
|
||||||
|
|
||||||
|
|
||||||
class FactorioMap {
|
class FactorioMap(
|
||||||
|
private val serverId: FactorioServerId,
|
||||||
|
) {
|
||||||
|
|
||||||
val kvMap: Maps = Maps {
|
val kvMap: Maps = Maps {
|
||||||
width = 800.px
|
width = 800.px
|
||||||
|
|
@ -155,16 +158,18 @@ class FactorioMap {
|
||||||
return baseTileLayer
|
return baseTileLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tileLayerUrlTemplate(layer: ServerMapTileLayer): String {
|
||||||
|
return "/kafkatorio/data/servers/${serverId.id}/map/layers/${layer.dir}/s1/z{z}/x{x}/y{y}.png"
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DYNAMIC_RELOAD_ATT = "dynamic-reload"
|
const val DYNAMIC_RELOAD_ATT = "dynamic-reload"
|
||||||
|
|
||||||
private val serverName: String
|
// private val serverName: String?
|
||||||
get() = window.location.pathname
|
// get() = window.location.pathname
|
||||||
.substringAfter("kafkatorio/data/servers/")
|
// .substringAfter("servers")
|
||||||
.substringBefore("/")
|
// .substringBefore("/")
|
||||||
|
// .ifBlank { null }
|
||||||
|
|
||||||
private fun tileLayerUrlTemplate(layer: ServerMapTileLayer): String {
|
|
||||||
return "/kafkatorio/data/servers/$serverName/map/layers/${layer.dir}/s1/z{z}/x{x}/y{y}.png"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||||
|
import dev.adamko.kafkatorio.schema.common.Tick
|
||||||
|
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
|
||||||
|
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
||||||
|
import dev.adamko.kafkatorio.webmap.services.EventsServerClient
|
||||||
|
import dev.adamko.kafkatorio.webmap.state.FactorioGameState
|
||||||
|
import io.kvision.navigo.Match
|
||||||
|
import io.kvision.navigo.Navigo
|
||||||
|
import io.kvision.redux.RAction
|
||||||
|
import io.kvision.redux.ReduxStore
|
||||||
|
import io.kvision.routing.routing
|
||||||
|
import kotlin.js.RegExp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
class SiteRouting(
|
||||||
|
val siteStateStore: ReduxStore<SiteState, SiteAction>
|
||||||
|
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
routing
|
||||||
|
.on(SiteView.HOME) { homepageView() }
|
||||||
|
.on(SiteView.SERVER) { match ->
|
||||||
|
serverView(match)
|
||||||
|
}
|
||||||
|
.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun homepageView() {
|
||||||
|
siteStateStore.dispatch(SiteAction.HomePage)
|
||||||
|
launch {
|
||||||
|
val serverIds = EventsServerClient.serverIds()
|
||||||
|
siteStateStore.dispatch(SiteAction.ServerIdsLoaded(serverIds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serverView(match: Match) {
|
||||||
|
val serverId = match.data[0] as? String ?: error("invalid server id")
|
||||||
|
siteStateStore.dispatch(SiteAction.ServerPage(FactorioServerId(serverId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class SiteState(
|
||||||
|
val view: SiteView = SiteView.HOME,
|
||||||
|
|
||||||
|
val serverIds: List<FactorioServerId>? = null,
|
||||||
|
|
||||||
|
val factorioGameState: FactorioGameState? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun plus(action: SiteAction): SiteState = when (action) {
|
||||||
|
is SiteAction.HomePage -> copy(view = SiteView.HOME)
|
||||||
|
|
||||||
|
is SiteAction.ServerPage -> copy(
|
||||||
|
view = SiteView.SERVER,
|
||||||
|
factorioGameState = FactorioGameState(action.serverId),
|
||||||
|
)
|
||||||
|
|
||||||
|
is SiteAction.ServerIdsLoaded -> copy(serverIds = action.serverIds)
|
||||||
|
|
||||||
|
is SiteAction.FactorioUpdate -> copy(
|
||||||
|
factorioGameState = factorioGameState?.let { it + action }
|
||||||
|
)
|
||||||
|
|
||||||
|
is SiteAction.EventServerUpdate -> copy(
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface SiteAction : RAction {
|
||||||
|
object HomePage : SiteAction
|
||||||
|
data class ServerPage(val serverId: FactorioServerId) : SiteAction
|
||||||
|
|
||||||
|
|
||||||
|
data class ServerIdsLoaded(val serverIds: List<FactorioServerId>) : SiteAction
|
||||||
|
|
||||||
|
|
||||||
|
/** Updates from `events-server` */
|
||||||
|
sealed class FactorioUpdate : SiteAction {
|
||||||
|
data class Player(val tick: Tick, val data: PlayerUpdate) : FactorioUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EventServerUpdate(val packet: EventServerPacket) : SiteAction
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface SiteView {
|
||||||
|
val pathMatcher: Any
|
||||||
|
|
||||||
|
sealed interface StaticPath : SiteView {
|
||||||
|
override val pathMatcher: String
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface DynamicPath : SiteView {
|
||||||
|
override val pathMatcher: RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object HOME : StaticPath {
|
||||||
|
override val pathMatcher: String = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
object SERVER : DynamicPath {
|
||||||
|
override val pathMatcher: RegExp = RegExp("^servers/${FactorioServerId.validIdRegex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//val SiteView.urlMatcher: String
|
||||||
|
// get() = when (this) {
|
||||||
|
// SiteView.HOME -> "/"
|
||||||
|
// SiteView.SERVER -> "servers/${FactorioServerId.validIdRegex}"
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun Navigo.on(view: SiteView, handler: (Match) -> Unit): Navigo {
|
||||||
|
return when (view) {
|
||||||
|
is SiteView.StaticPath -> on(view.pathMatcher, { match: Match -> handler(match) })
|
||||||
|
is SiteView.DynamicPath -> on(view.pathMatcher, { match: Match -> handler(match) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//private fun Navigo.on(
|
||||||
|
// view: SiteView,
|
||||||
|
// before: BeforeHook? = null,
|
||||||
|
// after: AfterHook? = null,
|
||||||
|
// leave: LeaveHook? = null,
|
||||||
|
// already: AlreadyHook? = null,
|
||||||
|
// handler: (Match) -> Unit,
|
||||||
|
//): Navigo {
|
||||||
|
//
|
||||||
|
// val hooks: RouteHooks? =
|
||||||
|
// if (before != null || after != null || leave != null || already != null) {
|
||||||
|
// jso {
|
||||||
|
// if (before != null) this@jso.before = before
|
||||||
|
// if (after != null) this@jso.after = after
|
||||||
|
// if (leave != null) this@jso.leave = leave
|
||||||
|
// if (already != null) this@jso.already = already
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// null
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return when (view) {
|
||||||
|
// is SiteView.StaticPath -> when (hooks) {
|
||||||
|
// null -> on(view.pathMatcher, { match: Match -> handler(match) })
|
||||||
|
// else -> on(view.pathMatcher, { match: Match -> handler(match) }, hooks)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is SiteView.DynamicPath -> when (hooks) {
|
||||||
|
// null -> on(view.pathMatcher, { match: Match -> handler(match) })
|
||||||
|
// else -> on(view.pathMatcher, { match: Match -> handler(match) }, hooks)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap.services
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||||
|
import io.kvision.rest.RestClient
|
||||||
|
import io.kvision.rest.call
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.await
|
||||||
|
|
||||||
|
object EventsServerClient :
|
||||||
|
CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||||
|
|
||||||
|
private val restClient = RestClient()
|
||||||
|
|
||||||
|
|
||||||
|
/** Fetch all possible server IDs. */
|
||||||
|
suspend fun serverIds(): List<FactorioServerId> =
|
||||||
|
restClient
|
||||||
|
.call<List<FactorioServerId>>(window.location.origin + "/kafkatorio/data/servers")
|
||||||
|
.await()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.adamko.kafkatorio.webmap
|
package dev.adamko.kafkatorio.webmap.services
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.schema.packets.ConfigurationUpdate
|
import dev.adamko.kafkatorio.schema.packets.ConfigurationUpdate
|
||||||
import dev.adamko.kafkatorio.schema.packets.ConsoleChatUpdate
|
import dev.adamko.kafkatorio.schema.packets.ConsoleChatUpdate
|
||||||
|
|
@ -12,8 +12,11 @@ import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdate
|
||||||
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
|
||||||
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
|
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
|
||||||
import dev.adamko.kafkatorio.webmap.state.FactorioGameState
|
import dev.adamko.kafkatorio.webmap.SiteAction
|
||||||
import dev.adamko.kafkatorio.webmap.state.FactorioUpdate
|
import dev.adamko.kafkatorio.webmap.SiteState
|
||||||
|
import dev.adamko.kafkatorio.webmap.config.ApplicationProperties
|
||||||
|
import dev.adamko.kafkatorio.webmap.config.jsonMapper
|
||||||
|
import dev.adamko.kafkatorio.webmap.rootJob
|
||||||
import io.kvision.redux.ReduxStore
|
import io.kvision.redux.ReduxStore
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
|
|
@ -29,14 +32,13 @@ import org.w3c.dom.events.Event
|
||||||
|
|
||||||
|
|
||||||
class WebsocketService(
|
class WebsocketService(
|
||||||
wsUrl: String = Props.websocketServerUrl,
|
private val siteStateStore: ReduxStore<SiteState, SiteAction>
|
||||||
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
|
|
||||||
) : CoroutineScope {
|
) : CoroutineScope {
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext =
|
override val coroutineContext: CoroutineContext =
|
||||||
CoroutineName("WebsocketService") + Job(rootJob)
|
CoroutineName("WebsocketService") + Job(rootJob)
|
||||||
|
|
||||||
private val ws = WebSocket(wsUrl)
|
private val ws = WebSocket(ApplicationProperties.websocketServerUrl)
|
||||||
// private val ws = WebSocket("ws://localhost:12080/ws/foo")
|
// private val ws = WebSocket("ws://localhost:12080/ws/foo")
|
||||||
|
|
||||||
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
|
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
|
||||||
|
|
@ -78,7 +80,9 @@ class WebsocketService(
|
||||||
is EventServerPacket.Kafkatorio ->
|
is EventServerPacket.Kafkatorio ->
|
||||||
when (val packetData = packet.packet.data) {
|
when (val packetData = packet.packet.data) {
|
||||||
is PlayerUpdate ->
|
is PlayerUpdate ->
|
||||||
reduxStore.dispatch(FactorioUpdate.Player(packet.packet.tick, packetData))
|
siteStateStore.dispatch(
|
||||||
|
SiteAction.FactorioUpdate.Player(packet.packet.tick, packetData)
|
||||||
|
)
|
||||||
|
|
||||||
is ConfigurationUpdate,
|
is ConfigurationUpdate,
|
||||||
is ConsoleChatUpdate,
|
is ConsoleChatUpdate,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.adamko.kafkatorio.webmap
|
package dev.adamko.kafkatorio.webmap.services
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
|
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
|
||||||
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package dev.adamko.kafkatorio.webmap.state
|
||||||
|
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.schema.common.Colour
|
||||||
|
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
|
||||||
|
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
|
||||||
|
import dev.adamko.kafkatorio.schema.common.toHexString
|
||||||
|
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
||||||
|
import dev.adamko.kafkatorio.webmap.services.latLng
|
||||||
|
import io.kvision.maps.externals.leaflet.geo.LatLng
|
||||||
|
|
||||||
|
sealed interface PlayerProperties {
|
||||||
|
|
||||||
|
val position: MapEntityPosition?
|
||||||
|
val surfaceIndex: SurfaceIndex?
|
||||||
|
val colour: Colour?
|
||||||
|
val name: String?
|
||||||
|
val latLng: LatLng?
|
||||||
|
val isConnected: Boolean
|
||||||
|
val isShowOnMap: Boolean
|
||||||
|
|
||||||
|
|
||||||
|
data class Partial(
|
||||||
|
override val position: MapEntityPosition? = null,
|
||||||
|
override val surfaceIndex: SurfaceIndex? = null,
|
||||||
|
override val colour: Colour? = null,
|
||||||
|
override val name: String? = null,
|
||||||
|
override val isConnected: Boolean = false,
|
||||||
|
override val isShowOnMap: Boolean = false,
|
||||||
|
) : PlayerProperties {
|
||||||
|
override val latLng: LatLng? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class Complete(
|
||||||
|
override val position: MapEntityPosition,
|
||||||
|
override val surfaceIndex: SurfaceIndex,
|
||||||
|
override val colour: Colour,
|
||||||
|
override val name: String,
|
||||||
|
override val isConnected: Boolean = false,
|
||||||
|
override val isShowOnMap: Boolean = false,
|
||||||
|
) : PlayerProperties {
|
||||||
|
override val latLng: LatLng = position.latLng()
|
||||||
|
val hexColour: String = colour.toHexString(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun update(update: PlayerUpdate): PlayerProperties {
|
||||||
|
|
||||||
|
val position = update.position ?: this.position
|
||||||
|
val surfaceIndex = update.surfaceIndex ?: this.surfaceIndex
|
||||||
|
val colour = update.colour ?: this.colour
|
||||||
|
val name = update.name ?: this.name
|
||||||
|
val isConnected = update.isConnected
|
||||||
|
val isShowOnMap = update.isShowOnMap
|
||||||
|
|
||||||
|
return when {
|
||||||
|
position != null
|
||||||
|
&& surfaceIndex != null
|
||||||
|
&& colour != null
|
||||||
|
&& name != null ->
|
||||||
|
Complete(
|
||||||
|
position = position,
|
||||||
|
surfaceIndex = surfaceIndex,
|
||||||
|
colour = colour,
|
||||||
|
name = name,
|
||||||
|
isConnected = isConnected,
|
||||||
|
isShowOnMap = isShowOnMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
Partial(
|
||||||
|
position = position,
|
||||||
|
surfaceIndex = surfaceIndex,
|
||||||
|
colour = colour,
|
||||||
|
name = name,
|
||||||
|
isConnected = isConnected,
|
||||||
|
isShowOnMap = isShowOnMap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
package dev.adamko.kafkatorio.webmap.state
|
package dev.adamko.kafkatorio.webmap.state
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.schema.common.Colour
|
|
||||||
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
|
|
||||||
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
||||||
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
|
|
||||||
import dev.adamko.kafkatorio.schema.common.Tick
|
import dev.adamko.kafkatorio.schema.common.Tick
|
||||||
import dev.adamko.kafkatorio.schema.common.toHexString
|
|
||||||
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
||||||
import dev.adamko.kafkatorio.webmap.latLng
|
import dev.adamko.kafkatorio.webmap.SiteAction
|
||||||
import io.kvision.maps.Maps
|
import io.kvision.maps.Maps
|
||||||
import io.kvision.maps.externals.leaflet.geo.LatLng
|
|
||||||
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
|
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
|
||||||
import kotlinx.js.jso
|
import kotlinx.js.jso
|
||||||
|
|
||||||
|
|
||||||
fun handlePlayerUpdate(
|
fun handlePlayerUpdate(
|
||||||
gameState: FactorioGameState,
|
gameState: FactorioGameState,
|
||||||
update: FactorioUpdate.Player,
|
update: SiteAction.FactorioUpdate.Player,
|
||||||
): FactorioGameState {
|
): FactorioGameState {
|
||||||
|
|
||||||
val previous = gameState.players[update.data.key.index]
|
val previous = gameState.players[update.data.key.index]
|
||||||
|
|
@ -105,76 +100,3 @@ data class PlayerState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sealed interface PlayerProperties {
|
|
||||||
|
|
||||||
val position: MapEntityPosition?
|
|
||||||
val surfaceIndex: SurfaceIndex?
|
|
||||||
val colour: Colour?
|
|
||||||
val name: String?
|
|
||||||
val latLng: LatLng?
|
|
||||||
val isConnected: Boolean
|
|
||||||
val isShowOnMap: Boolean
|
|
||||||
|
|
||||||
|
|
||||||
data class Partial(
|
|
||||||
override val position: MapEntityPosition? = null,
|
|
||||||
override val surfaceIndex: SurfaceIndex? = null,
|
|
||||||
override val colour: Colour? = null,
|
|
||||||
override val name: String? = null,
|
|
||||||
override val isConnected: Boolean = false,
|
|
||||||
override val isShowOnMap: Boolean = false,
|
|
||||||
) : PlayerProperties {
|
|
||||||
override val latLng: LatLng? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class Complete(
|
|
||||||
override val position: MapEntityPosition,
|
|
||||||
override val surfaceIndex: SurfaceIndex,
|
|
||||||
override val colour: Colour,
|
|
||||||
override val name: String,
|
|
||||||
override val isConnected: Boolean = false,
|
|
||||||
override val isShowOnMap: Boolean = false,
|
|
||||||
) : PlayerProperties {
|
|
||||||
override val latLng: LatLng = position.latLng()
|
|
||||||
val hexColour: String = colour.toHexString(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun update(update: PlayerUpdate): PlayerProperties {
|
|
||||||
|
|
||||||
val position = update.position ?: this.position
|
|
||||||
val surfaceIndex = update.surfaceIndex ?: this.surfaceIndex
|
|
||||||
val colour = update.colour ?: this.colour
|
|
||||||
val name = update.name ?: this.name
|
|
||||||
val isConnected = update.isConnected
|
|
||||||
val isShowOnMap = update.isShowOnMap
|
|
||||||
|
|
||||||
return when {
|
|
||||||
position != null
|
|
||||||
&& surfaceIndex != null
|
|
||||||
&& colour != null
|
|
||||||
&& name != null ->
|
|
||||||
Complete(
|
|
||||||
position = position,
|
|
||||||
surfaceIndex = surfaceIndex,
|
|
||||||
colour = colour,
|
|
||||||
name = name,
|
|
||||||
isConnected = isConnected,
|
|
||||||
isShowOnMap = isShowOnMap,
|
|
||||||
)
|
|
||||||
|
|
||||||
else ->
|
|
||||||
Partial(
|
|
||||||
position = position,
|
|
||||||
surfaceIndex = surfaceIndex,
|
|
||||||
colour = colour,
|
|
||||||
name = name,
|
|
||||||
isConnected = isConnected,
|
|
||||||
isShowOnMap = isShowOnMap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,33 @@
|
||||||
package dev.adamko.kafkatorio.webmap.state
|
package dev.adamko.kafkatorio.webmap.state
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||||
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
||||||
import dev.adamko.kafkatorio.schema.common.Tick
|
|
||||||
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
|
||||||
import dev.adamko.kafkatorio.webmap.FactorioMap
|
import dev.adamko.kafkatorio.webmap.FactorioMap
|
||||||
|
import dev.adamko.kafkatorio.webmap.SiteAction
|
||||||
import io.kvision.maps.externals.leaflet.layer.LayerGroup
|
import io.kvision.maps.externals.leaflet.layer.LayerGroup
|
||||||
import io.kvision.redux.ReduxStore
|
|
||||||
import io.kvision.redux.createReduxStore
|
|
||||||
import redux.RAction
|
|
||||||
|
|
||||||
|
|
||||||
fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
|
//fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
|
||||||
createReduxStore(
|
// createReduxStore(
|
||||||
::factorioGameReducer,
|
// ::factorioGameReducer,
|
||||||
FactorioGameState(
|
// FactorioGameState(
|
||||||
map = FactorioMap(),
|
// map = FactorioMap(),
|
||||||
players = emptyMap(),
|
// players = emptyMap(),
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
|
|
||||||
|
|
||||||
data class FactorioGameState(
|
data class FactorioGameState(
|
||||||
val map: FactorioMap,
|
val serverId: FactorioServerId,
|
||||||
val players: Map<PlayerIndex, PlayerState>,
|
val map: FactorioMap = FactorioMap(serverId),
|
||||||
|
val players: Map<PlayerIndex, PlayerState> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val playerIconsLayer: LayerGroup
|
val playerIconsLayer: LayerGroup
|
||||||
get() = map.playerIconsLayer
|
get() = map.playerIconsLayer
|
||||||
|
|
||||||
|
operator fun plus(action: SiteAction.FactorioUpdate): FactorioGameState =
|
||||||
|
when (action) {
|
||||||
|
is SiteAction.FactorioUpdate.Player -> handlePlayerUpdate(this, action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sealed class FactorioUpdate : RAction {
|
|
||||||
data class Player(val tick: Tick, val data: PlayerUpdate) : FactorioUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun factorioGameReducer(gameState: FactorioGameState, update: FactorioUpdate): FactorioGameState =
|
|
||||||
when (update) {
|
|
||||||
is FactorioUpdate.Player -> handlePlayerUpdate(gameState, update)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue