web-map tidy up, and add diagram to README
This commit is contained in:
parent
3976c3cc1b
commit
de419aeae8
9 changed files with 60 additions and 49 deletions
38
README.md
38
README.md
|
|
@ -10,8 +10,42 @@ and WebSocket APIs.
|
||||||
|
|
||||||
Development is ongoing. At present the only application is a
|
Development is ongoing. At present the only application is a
|
||||||
[live-view web map](https://factorio.adamko.dev), that shows the current status of a Factorio
|
[live-view web map](https://factorio.adamko.dev), that shows the current status of a Factorio
|
||||||
server, and any connected players. Other possibilities are exposing metrics, and inter-server
|
server, and any connected players. Other usage possibilities include exposing live metrics, and
|
||||||
communication.
|
inter-server communication.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
|
||||||
|
Factorio multiplayer server->>events-mod: in-game events
|
||||||
|
Note over events-mod: converts events to packets
|
||||||
|
events-mod->>logs processor: packets logged to console
|
||||||
|
logs processor->>Kafka: publishes packets<br/>(Requires API key!)
|
||||||
|
|
||||||
|
Kafka->>events-server:
|
||||||
|
|
||||||
|
Note over events-server: Packets are processed
|
||||||
|
|
||||||
|
events-server->>web-map: Web map tiles (REST API)
|
||||||
|
events-server-)web-map: WebSocket updates<br/>(e.g. player movement)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The Kafkatorio game mod, [`events-mod`](./modules/events-mod), collects event data, de-bouncing
|
||||||
|
and grouping events, and converts them to JSON packets
|
||||||
|
2. `events-mod` prints the packets to the Factorio game server logs
|
||||||
|
3. The Factorio logs are forwarded to a Kafka cluster, either using
|
||||||
|
* [syslog](https://docs.docker.com/config/containers/logging/syslog/) (using Docker logging
|
||||||
|
plugin)
|
||||||
|
* or [Kafka Pipe](./modules/infra-kafka-pipe) (a script that reads the server logs and forwards
|
||||||
|
them to a Kafka topic)
|
||||||
|
4. The packets are consumed by [`events-server`](./modules/events-server), which processes the
|
||||||
|
packets using Kafka Streams, creating and hosting map tiles.
|
||||||
|
|
||||||
|
`events-server` serves the map tiles via a REST API, and produces WebSocket messages regarding
|
||||||
|
updates (for example, when a player's location has changed).
|
||||||
|
5. The `web-map` uses Leaflet/JS to display the map, using the tiles and WebSocket messages from
|
||||||
|
`events-server`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,10 @@ object App : Application() {
|
||||||
|
|
||||||
var siteStateStore: ReduxStore<SiteState, SiteAction> by appState
|
var siteStateStore: ReduxStore<SiteState, SiteAction> by appState
|
||||||
private set
|
private set
|
||||||
// var wsService: WebsocketService by appState
|
|
||||||
// private set
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.siteStateStore = createReduxStore(SiteState::plus, SiteState())
|
this.siteStateStore = createReduxStore(SiteState::plus, SiteState())
|
||||||
// this.wsService = wsService
|
|
||||||
require("./css/kafkatorio.css")
|
require("./css/kafkatorio.css")
|
||||||
|
|
||||||
WebsocketService.packetsFlow
|
WebsocketService.packetsFlow
|
||||||
|
|
@ -78,9 +76,6 @@ object App : Application() {
|
||||||
|
|
||||||
this.appState = state.toMutableMap()
|
this.appState = state.toMutableMap()
|
||||||
|
|
||||||
// val siteRouting = SiteRouting(reduxStore)
|
|
||||||
// val websocketService = WebsocketService(siteStateStore)
|
|
||||||
|
|
||||||
SiteRouting.init()
|
SiteRouting.init()
|
||||||
|
|
||||||
root("kvapp") {
|
root("kvapp") {
|
||||||
|
|
@ -99,9 +94,6 @@ object App : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// div("Kafkatorio Web Map")
|
|
||||||
// add(kvMaps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// kvMaps.leafletMap {
|
// kvMaps.leafletMap {
|
||||||
|
|
@ -124,20 +116,10 @@ object App : Application() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
startApplication(
|
startApplication(
|
||||||
{
|
{ App },
|
||||||
// val reduxStore = createFactorioReduxStore()
|
|
||||||
// val wsService = WebsocketService(
|
|
||||||
// reduxStore = reduxStore,
|
|
||||||
// )
|
|
||||||
// App(
|
|
||||||
// reduxStore = reduxStore,
|
|
||||||
// wsService = wsService,
|
|
||||||
// )
|
|
||||||
|
|
||||||
App
|
|
||||||
},
|
|
||||||
module.hot,
|
module.hot,
|
||||||
BootstrapModule,
|
BootstrapModule,
|
||||||
// BootstrapCssModule,
|
// BootstrapCssModule,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import kotlinx.browser.window
|
||||||
|
|
||||||
|
|
||||||
object ApplicationProperties {
|
object ApplicationProperties {
|
||||||
|
|
||||||
|
/** Determine the WebSocket URL based on the window location URL. */
|
||||||
@Suppress("HttpUrlsUsage")
|
@Suppress("HttpUrlsUsage")
|
||||||
val websocketServerUrl: String
|
val websocketServerUrl: String
|
||||||
get() = window.location.origin
|
get() = window.location.origin
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@ package dev.adamko.kafkatorio.webmap.layout
|
||||||
import dev.adamko.kafkatorio.webmap.SiteState
|
import dev.adamko.kafkatorio.webmap.SiteState
|
||||||
import dev.adamko.kafkatorio.webmap.SiteView
|
import dev.adamko.kafkatorio.webmap.SiteView
|
||||||
import io.kvision.core.Container
|
import io.kvision.core.Container
|
||||||
import io.kvision.html.div
|
import io.kvision.navbar.nav
|
||||||
import io.kvision.html.link
|
import io.kvision.navbar.navLink
|
||||||
import io.kvision.html.nav
|
import io.kvision.navbar.navbar
|
||||||
|
|
||||||
|
|
||||||
fun Container.headerNav(state: SiteState) {
|
fun Container.headerNav(state: SiteState) {
|
||||||
nav(className = "navbar") {
|
navbar {
|
||||||
div(className = "container") {
|
nav {
|
||||||
link("home", SiteView.HOME.path)
|
navLink("Home", SiteView.HOME.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ private fun Container.servers(state: SiteState) {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
button(text = "$serverId") {
|
button(text = "$serverId", className = "btn btn-link") {
|
||||||
style = ButtonStyle.PRIMARY
|
style = ButtonStyle.PRIMARY
|
||||||
size = ButtonSize.SMALL
|
size = ButtonSize.SMALL
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
|
||||||
fun Container.serverPage(state: SiteState) {
|
fun Container.serverPage(state: SiteState) {
|
||||||
val factorioGameState = state.factorioGameState
|
val factorioGameState = state.factorioGameState
|
||||||
|
|
||||||
div(className = "server-page") {
|
div(className = "server-page container-fluid") {
|
||||||
if (factorioGameState != null) {
|
if (factorioGameState != null) {
|
||||||
|
|
||||||
add(factorioGameState.map.kvMap)
|
add(factorioGameState.map.kvMap)
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ class FactorioMap(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createServerTileLayer(
|
private fun createServerTileLayer(
|
||||||
layer: ServerMapTileLayer
|
layer: ServerMapTileLayer
|
||||||
): TileLayer<TileLayer.TileLayerOptions> {
|
): TileLayer<TileLayer.TileLayerOptions> {
|
||||||
|
|
@ -160,11 +161,11 @@ class FactorioMap(
|
||||||
return baseTileLayer
|
return baseTileLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tileLayerUrlTemplate(layer: ServerMapTileLayer): String {
|
private fun tileLayerUrlTemplate(layer: ServerMapTileLayer): String =
|
||||||
return "/kafkatorio/data/servers/${serverId.id}/map/layers/${layer.dir}/s1/z{z}/x{x}/y{y}.png"
|
"/kafkatorio/data/servers/${serverId.id}/map/layers/${layer.dir}/s1/z{z}/x{x}/y{y}.png"
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/** Add this attribute to `<img>` tiles that need to be dynamically refreshed. */
|
||||||
const val DYNAMIC_RELOAD_ATT = "dynamic-reload"
|
const val DYNAMIC_RELOAD_ATT = "dynamic-reload"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import dev.adamko.kafkatorio.webmap.rootJob
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
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.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
@ -33,14 +34,13 @@ import org.w3c.dom.events.Event
|
||||||
object WebsocketService : CoroutineScope {
|
object WebsocketService : CoroutineScope {
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext =
|
override val coroutineContext: CoroutineContext =
|
||||||
CoroutineName("WebsocketService") + Job(rootJob)
|
Dispatchers.Default + CoroutineName("WebsocketService") + Job(rootJob)
|
||||||
|
|
||||||
private val ws = WebSocket(ApplicationProperties.websocketServerUrl)
|
private val ws = WebSocket(ApplicationProperties.websocketServerUrl)
|
||||||
// private val ws = WebSocket("ws://localhost:12080/ws/foo")
|
|
||||||
|
|
||||||
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
|
private val packetsMutableFlow = MutableSharedFlow<EventServerPacket>()
|
||||||
val packetsFlow: SharedFlow<EventServerPacket>
|
val packetsFlow: SharedFlow<EventServerPacket>
|
||||||
get() = _packetsFlow.asSharedFlow()
|
get() = packetsMutableFlow.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
ws.onmessage = ::handleMessageEvent
|
ws.onmessage = ::handleMessageEvent
|
||||||
|
|
@ -69,7 +69,7 @@ object WebsocketService : CoroutineScope {
|
||||||
|
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
|
|
||||||
launch { _packetsFlow.emit(packet) }
|
launch { packetsMutableFlow.emit(packet) }
|
||||||
|
|
||||||
when (packet) {
|
when (packet) {
|
||||||
is EventServerPacket.ChunkTileSaved -> {}
|
is EventServerPacket.ChunkTileSaved -> {}
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,7 @@
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*html {*/
|
|
||||||
/* background: #2B2B2BFF;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
/*body {*/
|
|
||||||
/* background: #2B2B2BFF;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
|
/* make the Leaflet map background transparent, so it matches the site theme */
|
||||||
background-color: rgba(255, 0, 0, 0.0) !important;
|
background-color: rgba(255, 0, 0, 0.0) !important;
|
||||||
/*background: rgb(34, 34, 34) !important;*/
|
|
||||||
/*background: var(--bs-body-bg-rgb) !important;*/
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue