big ol' refactor to split up the events-server and events-processors

This commit is contained in:
Adam 2022-09-13 17:56:57 +02:00
parent d82f25c6ee
commit 873983f9a3
71 changed files with 1013 additions and 757 deletions

View file

@ -4,15 +4,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
// application
}
//description = "serves Kafkatorio content over the web"
description = "Shared Kafkatorio processors functionality"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
val kafkaStateDir: Directory = layout.projectDirectory.dir(".state/kafka")
dependencies {
@ -37,12 +37,17 @@ dependencies {
implementation(libs.simpleSyslogParser)
implementation(libs.kafka.kotkaStreams)
implementation(libs.bundles.hoplite)
implementation(libs.ktorServer.authJwt)
// implementation(libs.nimbusJoseJwt)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
}
//application {
// mainClass.set("dev.adamko.kafkatorio.processor.admin.MainKt")
//}
tasks.withType<KotlinCompile>().configureEach {
@ -52,3 +57,10 @@ tasks.withType<KotlinCompile>().configureEach {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
val createKafkaTopics by tasks.registering(JavaExec::class) {
group = rootProject.name
classpath = sourceSets.main.get().runtimeClasspath
mainClass.set("dev.adamko.kafkatorio.processor.admin.MainKt")
}

View file

@ -1,7 +1,7 @@
package dev.adamko.kafkatorio.server.processor.admin
package dev.adamko.kafkatorio.processor.admin
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.processor.allTopics
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.config.allTopics
import java.util.concurrent.TimeUnit
import mu.KotlinLogging
import org.apache.kafka.clients.admin.Admin
@ -11,7 +11,7 @@ import org.apache.kafka.clients.admin.TopicListing
class KafkatorioKafkaAdmin(
appProps: ApplicationProperties
appProps: ApplicationProperties,
) {
private val logger = KotlinLogging.logger {}

View file

@ -0,0 +1,10 @@
package dev.adamko.kafkatorio.processor.admin
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
fun main() {
val appProps = ApplicationProperties.load()
val admin = KafkatorioKafkaAdmin(appProps)
admin.createKafkatorioTopics()
}

View file

@ -1,15 +1,16 @@
package dev.adamko.kafkatorio.server.config
package dev.adamko.kafkatorio.processor.config
import com.sksamuel.hoplite.ConfigLoaderBuilder
import com.sksamuel.hoplite.addEnvironmentSource
import com.sksamuel.hoplite.addResourceOrFileSource
import com.sksamuel.hoplite.sources.UserSettingsPropertySource
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.FactorioServerToken
import dev.adamko.kafkatorio.schema.common.MaskedValue
import java.nio.file.Path
val appProps = ApplicationProperties.load()
// loaded by hoplite
data class ApplicationProperties(
val webPort: PortVal,
val serverDataDir: Path,
@ -32,8 +33,13 @@ data class ApplicationProperties(
.allowUnresolvedSubstitutions()
.allowEmptySources()
.addEnvironmentSource()
.addResourceOrFileSource(
resourceOrFile = "${System.getProperty("user.home")}/.secret.kafkatorio-config.yml",
optional = true
)
.addResourceOrFileSource("/.secret.config.yml", optional = true)
.addResourceOrFileSource("/config.yml", optional = false)
.addPropertySource(UserSettingsPropertySource)
.build()
.loadConfigOrThrow()
}

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.config
package dev.adamko.kafkatorio.processor.config
/** Improved (slightly) version of http4k's `Port`. */

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.processor
package dev.adamko.kafkatorio.processor.config
import dev.adamko.kafkatorio.schema.packets.ConfigurationUpdate
import dev.adamko.kafkatorio.schema.packets.ConsoleChatUpdate

View file

@ -0,0 +1,12 @@
package dev.adamko.kafkatorio.processor.config
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import kotlinx.serialization.json.Json
@PublishedApi
internal val jsonMapper: Json = Json {
prettyPrint = true
prettyPrintIndent = " "
serializersModule = jsonMapperKafkatorio.serializersModule
}

View file

@ -1,10 +1,10 @@
package dev.adamko.kafkatorio.server.security
package dev.adamko.kafkatorio.processor.core
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.schema.common.FactorioServerToken
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import kotlin.reflect.jvm.jvmName
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.SerialName

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.processor
package dev.adamko.kafkatorio.processor.core
import dev.adamko.kotka.extensions.component1
import dev.adamko.kotka.extensions.component2

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.processor
package dev.adamko.kafkatorio.processor.core
import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler
import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse

View file

@ -0,0 +1,88 @@
package dev.adamko.kafkatorio.processor.core
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import java.util.Properties
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.io.path.Path
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.kafka.streams.KafkaStreams
import org.apache.kafka.streams.StreamsConfig
import org.apache.kafka.streams.Topology
import org.apache.kafka.streams.TopologyDescription
private val appProps = ApplicationProperties.load()
suspend fun launchTopology(
id: String,
topology: Topology,
additionalProperties: Map<String, String> = mapOf(),
): Unit = coroutineScope {
val props: Properties = appProps.kafkaStreamsConfig.toProperties()
additionalProperties.forEach { (k, v) -> props.setProperty(k, v) }
val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as String
props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId)
launch(CoroutineName(appId)) {
val streamsState: KafkaStreams.State = suspendCancellableCoroutine { cont ->
val streams = KafkaStreams(topology, props)
streams.setUncaughtExceptionHandler(StreamsExceptionHandler { e ->
cont.resumeWithException(e)
})
streams.setStateListener { newState: KafkaStreams.State, _ ->
when (newState) {
KafkaStreams.State.PENDING_SHUTDOWN,
KafkaStreams.State.NOT_RUNNING,
KafkaStreams.State.PENDING_ERROR,
KafkaStreams.State.ERROR -> if (cont.isActive) cont.resume(newState)
else -> Unit // do nothing
}
}
cont.invokeOnCancellation {
println("[$appId] coroutine cancelled -> closing. cause:$it")
streams.close()
}
printTopologyDescription(appId, topology)
streams.start()
println("launched Topology $appId")
}
println("exiting Topology $appId: $streamsState")
currentCoroutineContext().job.cancel("$appId: $streamsState")
}
}
private fun printTopologyDescription(appId: String, topology: Topology) {
val description: TopologyDescription = topology.describe()
val descFile = Path("./build/$appId.txt").toFile().apply {
parentFile.mkdirs()
if (!exists()) createNewFile()
writeText(description.toString())
}
println(
"""
|----------------
|$appId - ${descFile.canonicalPath}
|$description
|----------------
""".trimMargin()
)
}

View file

@ -3,6 +3,7 @@ package dev.adamko.kafkatorio.server.processor.topology
import dev.adamko.kafkatorio.library.kxsBinary
import dev.adamko.kafkatorio.library.resize
import dev.adamko.kafkatorio.library.toMapChunkPosition
import dev.adamko.kafkatorio.processor.core.DebounceProcessor.Companion.addDebounceProcessor
import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex
import dev.adamko.kafkatorio.schema.common.FactorioServerId
@ -12,9 +13,8 @@ import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.server.processor.DebounceProcessor.Companion.addDebounceProcessor
import dev.adamko.kafkatorio.server.processor.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_032_UPDATES
import dev.adamko.kafkatorio.server.processor.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_032_UPDATES
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_STATE
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.groupedAs
import dev.adamko.kotka.extensions.materializedAs

View file

@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.server.processor.topology
import dev.adamko.kafkatorio.library.kxsBinary
import dev.adamko.kafkatorio.library.toMapTilePosition
import dev.adamko.kafkatorio.processor.core.DebounceProcessor.Companion.addDebounceProcessor
import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex
import dev.adamko.kafkatorio.schema.common.FactorioPrototype
@ -16,7 +17,6 @@ import dev.adamko.kafkatorio.schema.packets.MapChunkEntityUpdate
import dev.adamko.kafkatorio.schema.packets.MapChunkResourceUpdate
import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.server.processor.DebounceProcessor.Companion.addDebounceProcessor
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.materializedAs
import dev.adamko.kotka.extensions.stream
@ -27,7 +27,6 @@ import dev.adamko.kotka.extensions.tables.join
import dev.adamko.kotka.kxs.serde
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.time.Duration.Companion.seconds
import org.apache.kafka.streams.StreamsBuilder
@ -232,6 +231,9 @@ private fun KTable<ServerMapChunkId, ServerMapChunkTiles<PrototypeHashCode>>.enr
private fun MapBoundingBox.iterator(origin: MapTilePosition): Iterator<MapTilePosition> {
// check if tile width/height should be divided by 2. I think I read that it's not divided by 2
// if it's an odd number?
val xOffset: Int = floor(tileWidth.toDouble() / 2).toInt()
val startX = origin.x - xOffset
val endX = origin.x + xOffset

View file

@ -4,9 +4,9 @@ import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacketDataError
import dev.adamko.kafkatorio.server.config.jsonMapper
import dev.adamko.kafkatorio.server.processor.TOPIC_SRC_SERVER_LOG
import dev.adamko.kafkatorio.server.processor.topicName
import dev.adamko.kafkatorio.processor.config.TOPIC_SRC_SERVER_LOG
import dev.adamko.kafkatorio.processor.config.jsonMapper
import dev.adamko.kafkatorio.processor.config.topicName
import dev.adamko.kotka.extensions.component1
import dev.adamko.kotka.extensions.component2
import dev.adamko.kotka.extensions.consumedAs

View file

@ -1,10 +1,10 @@
package dev.adamko.kafkatorio.server.processor.topology
import dev.adamko.kafkatorio.processor.config.jsonMapper
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacketData
import dev.adamko.kafkatorio.server.config.jsonMapper
import dev.adamko.kafkatorio.server.processor.topicName
import dev.adamko.kafkatorio.processor.config.topicName
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.streams.filter
import dev.adamko.kotka.extensions.streams.mapValues

View file

@ -10,11 +10,11 @@ import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ServerMapTilePngFilename
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.server.config.jsonMapper
import dev.adamko.kafkatorio.server.processor.TOPIC_BROADCAST_TO_WEBSOCKET
import dev.adamko.kafkatorio.server.processor.TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE
import dev.adamko.kafkatorio.server.processor.TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE
import dev.adamko.kafkatorio.server.processor.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.TOPIC_BROADCAST_TO_WEBSOCKET
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_TERRAIN_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.jsonMapper
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.producedAs
import dev.adamko.kotka.extensions.stream
@ -44,7 +44,7 @@ private const val WEB_MAP_TILE_SIZE_PX = 256
fun saveMapTiles(
builder: StreamsBuilder,
serverDataDir: Path,
): Topology {
) {
val subdividedMapChunkTilesDebounced: KStream<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> =
builder.stream(
@ -64,8 +64,6 @@ fun saveMapTiles(
subdividedMapChunkTilesDebounced.saveChunkAsImage(serverDataDir)
savedChunkFilenames.broadcastSavedTileUpdates()
return builder.build()
}

View file

@ -6,9 +6,8 @@ import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
import dev.adamko.kafkatorio.server.processor.TOPIC_BROADCAST_TO_WEBSOCKET
import dev.adamko.kafkatorio.server.processor.topicName
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import dev.adamko.kafkatorio.processor.config.TOPIC_BROADCAST_TO_WEBSOCKET
import dev.adamko.kafkatorio.processor.config.topicName
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.producedAs
import dev.adamko.kotka.extensions.stream
@ -31,8 +30,13 @@ private val jsonMapper: Json = Json {
private const val pid = "websocketBroadcast"
interface WebsocketBroadcaster {
suspend fun sendMessageToClients(message: String)
}
fun broadcastToWebsocket(
websocketServer: WebmapWebsocketServer,
websocketBroadcaster: WebsocketBroadcaster,
builder: StreamsBuilder,
) {
@ -45,7 +49,7 @@ fun broadcastToWebsocket(
"$pid.websocket-server.send-message"
) { _, esPacket ->
runBlocking {
websocketServer.sendMessageToClients(jsonMapper.encodeToString(esPacket))
websocketBroadcaster.sendMessageToClients(jsonMapper.encodeToString(esPacket))
}
}
}

View file

@ -5,7 +5,7 @@ server-data-dir: ${SERVER_DATA_DIR:-./server-data}
socket-server-host: ${SOCKET_SERVER_HOST:-127.0.0.1}
#socket-server-port: ${SOCKET_SERVER_PORT:-12092}
socket-server-port: ${SOCKET_SERVER_PORT:-12092}
kafka-admin-config:
@ -16,7 +16,7 @@ kafka-admin-config:
kafka-streams-config:
application.id: kafkatorio-events-processor
bootstrap.servers: ${KAFKA_BOOTSTRAP_SERVERS:-http://localhost:9092}
state.dir: ${KAFKA_STREAMS_STATE_DIR:-./kafka-state}
state.dir: ${KAFKA_STREAMS_STATE_DIR:-.state/kafka}
num.stream.threads: 1
processing.guarantee: exactly_once_v2
auto.offset.reset: earliest
@ -65,5 +65,3 @@ kafka-streams-config:
# # fetch.message.max.bytes=31457280
# # 100mb 104857600
#
#tileServer:
# webmapTileDir: 'D:\Users\Adam\Projects\games\kafkatorio\modules\events-server\kafkatorio-tiles'

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.security
package dev.adamko.kafkatorio.processor.core
import dev.adamko.kafkatorio.schema.common.MaskedValue
import dev.adamko.kafkatorio.server.config.ApplicationProperties

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.processor.topology
package dev.adamko.kafkatorio.processor.core.topology
import dev.adamko.kafkatorio.library.kxsBinary
import dev.adamko.kafkatorio.schema.common.ColourHex

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.processor.topology
package dev.adamko.kafkatorio.processor.core.topology
//import dev.adamko.kafkatorio.server.processor.TOPIC_GROUPED_MAP_CHUNKS_STATE
//import dev.adamko.kafkatorio.processor.config.getJsonMapper

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.processor.topology
package dev.adamko.kafkatorio.processor.core.topology
import dev.adamko.kafkatorio.library.iterateTiles
import dev.adamko.kafkatorio.library.kxsBinary

View file

@ -1,54 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
}
//description = "serves Kafkatorio content over the web"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
dependencies {
implementation(platform(projects.modules.versionsPlatform))
implementation(libs.kafka.streams)
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.logging)
implementation(libs.scrimage.core)
implementation(libs.okio.core)
implementation(libs.simpleSyslogParser)
implementation(libs.kafka.kotkaStreams)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

View file

@ -1,54 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
}
//description = "serves Kafkatorio content over the web"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
dependencies {
implementation(platform(projects.modules.versionsPlatform))
implementation(libs.kafka.streams)
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.logging)
implementation(libs.scrimage.core)
implementation(libs.okio.core)
implementation(libs.simpleSyslogParser)
implementation(libs.kafka.kotkaStreams)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

View file

@ -1,54 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
}
//description = "serves Kafkatorio content over the web"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
dependencies {
implementation(platform(projects.modules.versionsPlatform))
implementation(libs.kafka.streams)
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.logging)
implementation(libs.scrimage.core)
implementation(libs.okio.core)
implementation(libs.simpleSyslogParser)
implementation(libs.kafka.kotkaStreams)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

View file

@ -0,0 +1,91 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
}
description = "processors for Kafkatorio packets"
val projectId: String by project.extra
dependencies {
implementation(platform(projects.modules.versionsPlatform))
implementation(libs.kafka.streams)
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(projects.modules.eventsProcessorCore)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.logging)
implementation(libs.scrimage.core)
implementation(libs.okio.core)
implementation(libs.simpleSyslogParser)
implementation(libs.kafka.kotkaStreams)
}
application {
mainClass.set("dev.adamko.kafkatorio.processors.MainKt")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
//val startScriptEntitiesProcessor = registerStartScriptTask("EntitiesProcessor") {
// mainClass.set("dev.adamko.kafkatorio.processors.EntitiesProcessorKt")
//}
//
//val startScriptPacketsProcessor = registerStartScriptTask("PacketsProcessor") {
// mainClass.set("dev.adamko.kafkatorio.processors.PacketsProcessorKt")
//}
//
//val startScriptTilesProcessor = registerStartScriptTask("TilesProcessor") {
// mainClass.set("dev.adamko.kafkatorio.processors.TilesProcessorKt")
//}
//
//
//application.applicationDistribution.into("bin") {
// from(startScriptEntitiesProcessor)
// from(startScriptPacketsProcessor)
// from(startScriptTilesProcessor)
// fileMode = 0b000_111_101_101 // 0755
// duplicatesStrategy = DuplicatesStrategy.EXCLUDE
//}
tasks.named("run") {
val kafkaStateDir: Directory = layout.projectDirectory.dir(".state/kafka/")
doFirst {
kafkaStateDir.asFile.mkdirs()
}
}
val runEventsProcessors by tasks.registering {
group = rootProject.name
dependsOn(tasks.run)
}

View file

@ -0,0 +1,54 @@
package dev.adamko.kafkatorio.processors
import dev.adamko.kafkatorio.processor.core.launchTopology
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.PrototypeHashCode
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.packets.MapChunkEntityUpdate
import dev.adamko.kafkatorio.schema.packets.MapChunkResourceUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.server.processor.topology.ServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.colourEntityChunks
import dev.adamko.kafkatorio.server.processor.topology.convertEntityUpdateToServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.convertResourceUpdateToServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.streamPacketData
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.KStream
/** Building and resource tiles */
suspend fun groupEntitiesMapChunks(
layer: ServerMapTileLayer,
colouredUpdatesStreamTopic: String,
colouredChunkStateTopic: String,
) {
val builder = StreamsBuilder()
val entityChunksStream: KStream<ServerMapChunkId, ServerMapChunkTiles<PrototypeHashCode>> =
when (layer) {
ServerMapTileLayer.TERRAIN -> return
ServerMapTileLayer.RESOURCE ->
builder.streamPacketData<MapChunkResourceUpdate>()
.convertResourceUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
ServerMapTileLayer.BUILDING ->
builder.streamPacketData<MapChunkEntityUpdate>()
.convertEntityUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
}
val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
builder.streamPacketData()
val topology = colourEntityChunks(
builder,
entityChunksStream,
protosStream,
layer,
colouredUpdatesStreamTopic,
colouredChunkStateTopic,
)
launchTopology("groupTilesMapChunks.${layer.dir}", topology)
}

View file

@ -0,0 +1,23 @@
package dev.adamko.kafkatorio.processors
import dev.adamko.kafkatorio.processor.core.launchTopology
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.server.processor.topology.colourMapChunks
import dev.adamko.kafkatorio.server.processor.topology.streamPacketData
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.KStream
/** Terrain tiles */
suspend fun groupTilesMapChunks() {
val builder = StreamsBuilder()
val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
builder.streamPacketData()
val topology = colourMapChunks(builder, protosStream)
launchTopology("groupTilesMapChunks.${ServerMapTileLayer.TERRAIN.dir}", topology)
}

View file

@ -0,0 +1,36 @@
package dev.adamko.kafkatorio.processors
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_BUILDING_COLOURED_032_UPDATES
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_RESOURCE_COLOURED_032_UPDATES
import dev.adamko.kafkatorio.processor.config.TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
suspend fun main(): Unit = coroutineScope {
launch {
splitPackets()
}
launch {
groupTilesMapChunks()
}
launch {
groupEntitiesMapChunks(
layer = ServerMapTileLayer.BUILDING,
colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_032_UPDATES,
colouredChunkStateTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE,
)
}
launch {
groupEntitiesMapChunks(
layer = ServerMapTileLayer.RESOURCE,
colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_032_UPDATES,
colouredChunkStateTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE,
)
}
}

View file

@ -0,0 +1,17 @@
package dev.adamko.kafkatorio.processors
import dev.adamko.kafkatorio.processor.core.launchTopology
import dev.adamko.kafkatorio.server.processor.topology.factorioServerPacketStream
import dev.adamko.kafkatorio.server.processor.topology.splitFactorioServerPacketStream
import org.apache.kafka.streams.StreamsBuilder
suspend fun splitPackets() {
val builder = StreamsBuilder()
val packets = factorioServerPacketStream(builder)
splitFactorioServerPacketStream(packets)
val topology = builder.build()
launchTopology("splitPackets", topology)
}

View file

@ -1,79 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization")
application
}
//description = "serves Kafkatorio content over the web"
val projectId: String by project.extra
dependencies {
implementation(platform(projects.modules.versionsPlatform))
implementation(libs.kafka.streams)
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.logging)
implementation(libs.scrimage.core)
implementation(libs.okio.core)
//<editor-fold desc="Ktor">
implementation(libs.ktorSerialization.kotlinxJson)
with(libs.ktorServer) {
implementation(auth)
implementation(authJwt)
implementation(core)
implementation(callId)
implementation(callLogging)
implementation(contentNegotiaion)
implementation(cio)
implementation(autoHeadResponse)
implementation(partialContent)
implementation(resources)
implementation(statusPages)
implementation(websockets)
implementation(compression)
}
with(libs.ktorNetwork) {
implementation(core)
implementation(tls)
}
//</editor-fold>
implementation(libs.simpleSyslogParser)
// implementation(libs.nimbusJoseJwt)
implementation(libs.kafka.kotkaStreams)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

View file

@ -8,7 +8,7 @@ plugins {
}
//description = "serves Kafkatorio content over the web"
description = "Consumes Syslog Kafkatorio packets and sends them to Kafka"
val projectId: String by project.extra
@ -20,6 +20,7 @@ dependencies {
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(projects.modules.eventsProcessorCore)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
@ -62,11 +63,13 @@ dependencies {
// implementation(libs.nimbusJoseJwt)
implementation(libs.kafka.kotkaStreams)
implementation(libs.bundles.hoplite)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
mainClass.set("dev.adamko.kafkatorio.processor.syslog.SyslogMainKt")
}
@ -77,3 +80,9 @@ tasks.withType<KotlinCompile>().configureEach {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
val runSyslogServer by tasks.registering {
group = rootProject.name
dependsOn(tasks.run)
}

View file

@ -0,0 +1,11 @@
FROM gradle:7-jdk11 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle buildFatJar --no-daemon
FROM openjdk:11
EXPOSE 8080:8080
RUN mkdir /app
COPY --from=build /home/gradle/src/build/libs/*.jar /app/ktor-docker-sample.jar
ENTRYPOINT ["java","-jar","/app/ktor-docker-sample.jar"]

View file

@ -1,12 +1,12 @@
package dev.adamko.kafkatorio.server.processor
package dev.adamko.kafkatorio.processor.syslog
import com.github.palindromicity.syslog.dsl.SyslogFieldKeys
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.core.Authenticator
import dev.adamko.kafkatorio.processor.syslog.config.jsonMapper
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.config.jsonMapper
import dev.adamko.kafkatorio.server.security.Authenticator
import dev.adamko.kafkatorio.server.socket.SyslogMsg
import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
import dev.adamko.kafkatorio.processor.config.TOPIC_SRC_SERVER_LOG
import dev.adamko.kafkatorio.processor.config.TOPIC_SRC_SERVER_LOG_DLQ
import dev.adamko.kotka.kxs.kafkaSerializer
import java.util.Properties
import kotlin.coroutines.coroutineContext

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.socket
package dev.adamko.kafkatorio.processor.syslog
import com.github.palindromicity.syslog.dsl.SyslogFieldKeys

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.server.socket
package dev.adamko.kafkatorio.processor.syslog
import com.github.palindromicity.syslog.AllowableDeviations
import com.github.palindromicity.syslog.DefaultKeyProvider
@ -7,7 +7,7 @@ import com.github.palindromicity.syslog.NilPolicy
import com.github.palindromicity.syslog.SyslogParser
import com.github.palindromicity.syslog.SyslogParserBuilder
import com.github.palindromicity.syslog.SyslogSpecification
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.aSocket

View file

@ -0,0 +1,12 @@
package dev.adamko.kafkatorio.processor.syslog.config
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import kotlinx.serialization.json.Json
@PublishedApi
internal val jsonMapper: Json = Json {
prettyPrint = true
prettyPrintIndent = " "
serializersModule = jsonMapperKafkatorio.serializersModule
}

View file

@ -0,0 +1,20 @@
package dev.adamko.kafkatorio.processor.syslog
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.core.Authenticator
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/** Receive log messages from the syslog socket, and send them to a Kafka topic */
suspend fun main(): Unit = coroutineScope {
val appProps = ApplicationProperties.load()
val syslogServer = SyslogSocketServer(appProps)
val authenticator = Authenticator(appProps)
val producer = KafkatorioPacketKafkaProducer(appProps, syslogServer, authenticator)
launch { syslogServer.start() }
launch { producer.launch() }
}

View file

@ -8,11 +8,11 @@ plugins {
}
//description = "serves Kafkatorio content over the web"
description = "serves Kafkatorio content over web APIs"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
val kafkaStateDir: Directory = layout.projectDirectory.dir(".state/kafka")
dependencies {
@ -22,6 +22,7 @@ dependencies {
testImplementation(libs.kafka.streamsTestUtils)
implementation(projects.modules.eventsLibrary)
implementation(projects.modules.eventsProcessorCore)
implementation(libs.kotlinxSerialization.core)
implementation(libs.kotlinxSerialization.json)
@ -64,11 +65,13 @@ dependencies {
// implementation(libs.nimbusJoseJwt)
implementation(libs.kafka.kotkaStreams)
implementation(libs.bundles.hoplite)
}
application {
mainClass.set("dev.adamko.kafkatorio.server.EventsServerKt")
mainClass.set("dev.adamko.kafkatorio.server.web.MainKt")
}
@ -79,3 +82,10 @@ tasks.withType<KotlinCompile>().configureEach {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
val runEventsWebServer by tasks.registering {
group = rootProject.name
dependsOn(tasks.run)
}

View file

@ -1,10 +1,10 @@
package dev.adamko.kafkatorio.server.config
package dev.adamko.kafkatorio.server.web.config
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import kotlinx.serialization.json.Json
val jsonMapper: Json = Json {
internal val jsonMapper: Json = Json {
prettyPrint = true
prettyPrintIndent = " "
serializersModule = jsonMapperKafkatorio.serializersModule

View file

@ -0,0 +1,61 @@
package dev.adamko.kafkatorio.server.web
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.core.launchTopology
import dev.adamko.kafkatorio.server.processor.topology.broadcastToWebsocket
import dev.adamko.kafkatorio.server.processor.topology.saveMapTiles
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.StreamsConfig
suspend fun main(): Unit = coroutineScope {
val appProps = ApplicationProperties.load()
val wsServer = WebmapWebsocketServer()
launch {
websocketBroadcaster(wsServer)
}
// launch {
// broadcastToWebsocket(wsServer, StreamsBuilder())
// }
launch {
saveTiles(appProps)
}
launch {
startWebServer(
appProps = appProps,
wsServer = wsServer,
)
}
}
private suspend fun websocketBroadcaster(wsServer: WebmapWebsocketServer) {
val builder = StreamsBuilder()
broadcastToWebsocket(wsServer, builder)
val topology = builder.build()
launchTopology(
"websocketBroadcaster", topology, mapOf(
StreamsConfig.COMMIT_INTERVAL_MS_CONFIG to "${1.seconds.inWholeMilliseconds}",
StreamsConfig.producerPrefix(ProducerConfig.LINGER_MS_CONFIG) to "${1.milliseconds.inWholeMilliseconds}"
)
)
}
private suspend fun saveTiles(
appProps: ApplicationProperties
) {
val builder = StreamsBuilder()
saveMapTiles(builder, appProps.serverDataDir)
launchTopology("saveTiles", builder.build())
}

View file

@ -5,8 +5,8 @@ import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.common.ServerMapTilePngFilename
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.config.jsonMapper
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.server.web.config.jsonMapper
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.EntityTagVersion
import io.ktor.http.content.versions
@ -25,7 +25,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.encodeToJsonElement
fun Routing.factorioServersRoutes(
internal fun Routing.factorioServersRoutes(
appProps: ApplicationProperties,
) {

View file

@ -1,6 +1,6 @@
package dev.adamko.kafkatorio.server.web.rest
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.isSuccess
@ -22,7 +22,7 @@ import io.ktor.server.response.respondText
import io.ktor.server.routing.routing
fun Application.webmapTileServer(appProps: ApplicationProperties) {
internal fun Application.webmapTileServer(appProps: ApplicationProperties) {
install(AutoHeadResponse)
install(CallLogging) {

View file

@ -1,6 +1,7 @@
package dev.adamko.kafkatorio.server.web
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import dev.adamko.kafkatorio.server.web.rest.webmapTileServer
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import dev.adamko.kafkatorio.server.web.websocket.webmapWebsocketServer
@ -9,7 +10,7 @@ import io.ktor.server.engine.embeddedServer
import kotlinx.coroutines.coroutineScope
suspend fun startWebServer(
internal suspend fun startWebServer(
appProps: ApplicationProperties,
wsServer: WebmapWebsocketServer,
): Unit = coroutineScope {

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.server.web.websocket
import dev.adamko.kafkatorio.server.processor.topology.WebsocketBroadcaster
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
@ -24,7 +25,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
class WebmapWebsocketServer {
class WebmapWebsocketServer : WebsocketBroadcaster {
private val startTime = TimeSource.Monotonic.markNow()
@ -88,9 +89,9 @@ class WebmapWebsocketServer {
}
suspend fun sendMessageToClients(msg: String) {
log("sending message to clients: $msg")
messagesToClients.emit(msg)
override suspend fun sendMessageToClients(message: String) {
log("sending message to clients: $message")
messagesToClients.emit(message)
}

View file

@ -10,11 +10,11 @@ plugins {
}
description = "serves Kafkatorio content over the web"
description = "deprecated - this was split up into separate modules"
val projectId: String by project.extra
val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
val kafkaStateDir: Directory = layout.projectDirectory.dir(".state/kafka")
dependencies {

View file

@ -13,7 +13,7 @@ ENV SOCKET_SERVER_PORT=9002
EXPOSE $SOCKET_SERVER_PORT
ENV SERVER_DATA_DIR="/app/data"
ENV KAFKA_STREAMS_STATE_DIR="/app/kafka-state"
ENV KAFKA_STREAMS_STATE_DIR="/app/state/kafka"
VOLUME ${SERVER_DATA_DIR} \
${KAFKA_STREAMS_STATE_DIR}

View file

@ -1,32 +1,32 @@
package dev.adamko.kafkatorio.server
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.processor.eventsProcessor
import dev.adamko.kafkatorio.server.security.Authenticator
import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
import dev.adamko.kafkatorio.server.web.startWebServer
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
//import dev.adamko.kafkatorio.server.config.ApplicationProperties
//import dev.adamko.kafkatorio.server.processor.eventsProcessor
//import dev.adamko.kafkatorio.server.security.Authenticator
//import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
//import dev.adamko.kafkatorio.server.web.startWebServer
//import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
//import kotlinx.coroutines.coroutineScope
//import kotlinx.coroutines.launch
suspend fun main(): Unit = coroutineScope {
val appProps = ApplicationProperties.load()
val websocketServer = WebmapWebsocketServer()
val syslogSocketServer = SyslogSocketServer(appProps)
val authenticator = Authenticator(appProps)
launch {
startWebServer(appProps, websocketServer)
}
launch {
syslogSocketServer.start()
}
launch {
eventsProcessor(appProps, syslogSocketServer, websocketServer, authenticator)
}
}
//suspend fun main(): Unit = coroutineScope {
//
// val appProps = ApplicationProperties.load()
//
// val websocketServer = WebmapWebsocketServer()
// val syslogSocketServer = SyslogSocketServer(appProps)
// val authenticator = Authenticator(appProps)
//
// launch {
// startWebServer(appProps, websocketServer)
// }
//
// launch {
// syslogSocketServer.start()
// }
//
// launch {
// eventsProcessor(appProps, syslogSocketServer, websocketServer, authenticator)
// }
//}

View file

@ -1,243 +1,243 @@
package dev.adamko.kafkatorio.server.processor
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.PrototypeHashCode
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.packets.MapChunkEntityUpdate
import dev.adamko.kafkatorio.schema.packets.MapChunkResourceUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.processor.topology.ServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.broadcastToWebsocket
import dev.adamko.kafkatorio.server.processor.topology.colourEntityChunks
import dev.adamko.kafkatorio.server.processor.topology.colourMapChunks
import dev.adamko.kafkatorio.server.processor.topology.convertEntityUpdateToServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.convertResourceUpdateToServerMapChunkTiles
import dev.adamko.kafkatorio.server.processor.topology.factorioServerPacketStream
import dev.adamko.kafkatorio.server.processor.topology.saveMapTiles
import dev.adamko.kafkatorio.server.processor.topology.splitFactorioServerPacketStream
import dev.adamko.kafkatorio.server.processor.topology.streamPacketData
import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import java.util.Properties
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.io.path.Path
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.streams.KafkaStreams
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.StreamsConfig
import org.apache.kafka.streams.Topology
import org.apache.kafka.streams.TopologyDescription
import org.apache.kafka.streams.kstream.KStream
//import dev.adamko.kafkatorio.schema.common.FactorioServerId
//import dev.adamko.kafkatorio.schema.common.PrototypeHashCode
//import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
//import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
//import dev.adamko.kafkatorio.schema.packets.MapChunkEntityUpdate
//import dev.adamko.kafkatorio.schema.packets.MapChunkResourceUpdate
//import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
//import dev.adamko.kafkatorio.server.config.ApplicationProperties
//import dev.adamko.kafkatorio.server.processor.topology.ServerMapChunkTiles
//import dev.adamko.kafkatorio.server.processor.topology.broadcastToWebsocket
//import dev.adamko.kafkatorio.server.processor.topology.colourEntityChunks
//import dev.adamko.kafkatorio.server.processor.topology.colourMapChunks
//import dev.adamko.kafkatorio.server.processor.topology.convertEntityUpdateToServerMapChunkTiles
//import dev.adamko.kafkatorio.server.processor.topology.convertResourceUpdateToServerMapChunkTiles
//import dev.adamko.kafkatorio.server.processor.topology.factorioServerPacketStream
//import dev.adamko.kafkatorio.server.processor.topology.saveMapTiles
//import dev.adamko.kafkatorio.server.processor.topology.splitFactorioServerPacketStream
//import dev.adamko.kafkatorio.server.processor.topology.streamPacketData
//import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
//import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
//import java.util.Properties
//import kotlin.coroutines.resume
//import kotlin.coroutines.resumeWithException
//import kotlin.io.path.Path
//import kotlin.time.Duration.Companion.milliseconds
//import kotlin.time.Duration.Companion.seconds
//import kotlinx.coroutines.CoroutineName
//import kotlinx.coroutines.CoroutineScope
//import kotlinx.coroutines.cancel
//import kotlinx.coroutines.currentCoroutineContext
//import kotlinx.coroutines.job
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.suspendCancellableCoroutine
//import org.apache.kafka.clients.producer.ProducerConfig
//import org.apache.kafka.streams.KafkaStreams
//import org.apache.kafka.streams.StreamsBuilder
//import org.apache.kafka.streams.StreamsConfig
//import org.apache.kafka.streams.Topology
//import org.apache.kafka.streams.TopologyDescription
//import org.apache.kafka.streams.kstream.KStream
internal class KafkatorioTopology(
private val appProps: ApplicationProperties,
private val wsServer: WebmapWebsocketServer,
private val syslogServer: SyslogSocketServer,
private val kafkatorioPacketKafkaProducer: KafkatorioPacketKafkaProducer,
// private val appProps: ApplicationProperties,
// private val wsServer: WebmapWebsocketServer,
// private val syslogServer: SyslogSocketServer,
// private val kafkatorioPacketKafkaProducer: KafkatorioPacketKafkaProducer,
) {
private val builderScope = CoroutineScope(CoroutineName("KafkaTopology"))
// private val builderScope = CoroutineScope(CoroutineName("KafkaTopology"))
suspend fun start() = builderScope.launch {
splitPackets()
groupTilesMapChunks()
groupEntitiesMapChunks(
layer = ServerMapTileLayer.BUILDING,
colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_032_UPDATES,
colouredChunkStateTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE,
)
groupEntitiesMapChunks(
layer = ServerMapTileLayer.RESOURCE,
colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_032_UPDATES,
colouredChunkStateTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE,
)
saveTiles()
websocketBroadcaster()
syslogProducer()
}
// suspend fun start() = builderScope.launch {
// splitPackets()
//
// groupTilesMapChunks()
//
//// groupEntitiesMapChunks(
//// layer = ServerMapTileLayer.BUILDING,
//// colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_032_UPDATES,
//// colouredChunkStateTopic = TOPIC_MAP_CHUNK_BUILDING_COLOURED_STATE,
//// )
//
//// groupEntitiesMapChunks(
//// layer = ServerMapTileLayer.RESOURCE,
//// colouredUpdatesStreamTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_032_UPDATES,
//// colouredChunkStateTopic = TOPIC_MAP_CHUNK_RESOURCE_COLOURED_STATE,
//// )
//
// saveTiles()
//
// websocketBroadcaster()
//
// syslogProducer()
// }
private suspend fun splitPackets() {
val builder = StreamsBuilder()
val packets = factorioServerPacketStream(builder)
splitFactorioServerPacketStream(packets)
val topology = builder.build()
launchTopology("splitPackets", topology)
}
// private suspend fun splitPackets() {
// val builder = StreamsBuilder()
// val packets = factorioServerPacketStream(builder)
// splitFactorioServerPacketStream(packets)
//
// val topology = builder.build()
//
// launchTopology("splitPackets", topology)
// }
/** Terrain tiles */
private suspend fun groupTilesMapChunks() {
val builder = StreamsBuilder()
// /** Terrain tiles */
// private suspend fun groupTilesMapChunks() {
// val builder = StreamsBuilder()
//
// val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
// builder.streamPacketData()
//
// val topology = colourMapChunks(builder, protosStream)
//
// launchTopology("groupTilesMapChunks.${ServerMapTileLayer.TERRAIN.dir}", topology)
// }
val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
builder.streamPacketData()
val topology = colourMapChunks(builder, protosStream)
launchTopology("groupTilesMapChunks.${ServerMapTileLayer.TERRAIN.dir}", topology)
}
//
// /** Building and resource tiles */
// private suspend fun groupEntitiesMapChunks(
// layer: ServerMapTileLayer,
// colouredUpdatesStreamTopic: String,
// colouredChunkStateTopic: String,
// ) {
// val builder = StreamsBuilder()
//
// val entityChunksStream: KStream<ServerMapChunkId, ServerMapChunkTiles<PrototypeHashCode>> =
// when (layer) {
// ServerMapTileLayer.TERRAIN -> return
//
// ServerMapTileLayer.RESOURCE ->
// builder.streamPacketData<MapChunkResourceUpdate>()
// .convertResourceUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
//
// ServerMapTileLayer.BUILDING ->
// builder.streamPacketData<MapChunkEntityUpdate>()
// .convertEntityUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
// }
//
// val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
// builder.streamPacketData()
//
// val topology = colourEntityChunks(
// builder,
// entityChunksStream,
// protosStream,
// layer,
// colouredUpdatesStreamTopic,
// colouredChunkStateTopic,
// )
//
// launchTopology("groupTilesMapChunks.${layer.dir}", topology)
// }
/** Building and resource tiles */
private suspend fun groupEntitiesMapChunks(
layer: ServerMapTileLayer,
colouredUpdatesStreamTopic: String,
colouredChunkStateTopic: String,
) {
val builder = StreamsBuilder()
val entityChunksStream: KStream<ServerMapChunkId, ServerMapChunkTiles<PrototypeHashCode>> =
when (layer) {
ServerMapTileLayer.TERRAIN -> return
ServerMapTileLayer.RESOURCE ->
builder.streamPacketData<MapChunkResourceUpdate>()
.convertResourceUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
ServerMapTileLayer.BUILDING ->
builder.streamPacketData<MapChunkEntityUpdate>()
.convertEntityUpdateToServerMapChunkTiles("groupEntitiesMapChunks.${layer.dir}")
}
val protosStream: KStream<FactorioServerId, PrototypesUpdate> =
builder.streamPacketData()
val topology = colourEntityChunks(
builder,
entityChunksStream,
protosStream,
layer,
colouredUpdatesStreamTopic,
colouredChunkStateTopic,
)
launchTopology("groupTilesMapChunks.${layer.dir}", topology)
}
// private suspend fun saveTiles() {
// val builder = StreamsBuilder()
// val topology = saveMapTiles(builder, appProps.serverDataDir)
// launchTopology("saveTiles", topology)
// }
private suspend fun saveTiles() {
val builder = StreamsBuilder()
val topology = saveMapTiles(builder, appProps.serverDataDir)
launchTopology("saveTiles", topology)
}
// private suspend fun websocketBroadcaster() {
// val builder = StreamsBuilder()
// broadcastToWebsocket(wsServer, builder)
//
// val topology = builder.build()
//
// launchTopology(
// "websocketBroadcaster", topology, mapOf(
// StreamsConfig.COMMIT_INTERVAL_MS_CONFIG to "${1.seconds.inWholeMilliseconds}",
// StreamsConfig.producerPrefix(ProducerConfig.LINGER_MS_CONFIG) to "${1.milliseconds.inWholeMilliseconds}"
// )
// )
// }
private suspend fun websocketBroadcaster() {
val builder = StreamsBuilder()
broadcastToWebsocket(wsServer, builder)
val topology = builder.build()
launchTopology(
"websocketBroadcaster", topology, mapOf(
StreamsConfig.COMMIT_INTERVAL_MS_CONFIG to "${1.seconds.inWholeMilliseconds}",
StreamsConfig.producerPrefix(ProducerConfig.LINGER_MS_CONFIG) to "${1.milliseconds.inWholeMilliseconds}"
)
)
}
// /** Receive log messages from the syslog socket, and send them to a Kafka topic */
// private fun syslogProducer() {
// builderScope.launch {
// kafkatorioPacketKafkaProducer.launch()
// }
// }
/** Receive log messages from the syslog socket, and send them to a Kafka topic */
private fun syslogProducer() {
builderScope.launch {
kafkatorioPacketKafkaProducer.launch()
}
}
// private suspend fun launchTopology(
// id: String,
// topology: Topology,
// additionalProperties: Map<String, String> = mapOf(),
// ) {
//
// val props: Properties = appProps.kafkaStreamsConfig.toProperties()
//
// additionalProperties.forEach { (k, v) -> props.setProperty(k, v) }
//
// val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as String
// props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId)
//
// builderScope.launch(CoroutineName(appId)) {
//
// val streamsState: KafkaStreams.State = suspendCancellableCoroutine { cont ->
//
// val streams = KafkaStreams(topology, props)
// streams.setUncaughtExceptionHandler(StreamsExceptionHandler { e ->
// cont.resumeWithException(e)
// })
//
// streams.setStateListener { newState: KafkaStreams.State, _ ->
// when (newState) {
// KafkaStreams.State.NOT_RUNNING,
// KafkaStreams.State.ERROR -> if (cont.isActive) cont.resume(newState)
//
// else -> Unit // do nothing
// }
// }
//
// cont.invokeOnCancellation {
// println("[$appId] coroutine cancelled -> closing. cause:$it")
// streams.close()
// }
//
// printTopologyDescription(appId, topology)
// streams.start()
// println("launched Topology $appId")
// }
// println("exiting Topology $appId: $streamsState")
// currentCoroutineContext().job.cancel("$appId: $streamsState")
// }
// }
private suspend fun launchTopology(
id: String,
topology: Topology,
additionalProperties: Map<String, String> = mapOf(),
) {
val props: Properties = appProps.kafkaStreamsConfig.toProperties()
additionalProperties.forEach { (k, v) -> props.setProperty(k, v) }
val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as String
props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId)
builderScope.launch(CoroutineName(appId)) {
val streamsState: KafkaStreams.State = suspendCancellableCoroutine { cont ->
val streams = KafkaStreams(topology, props)
streams.setUncaughtExceptionHandler(StreamsExceptionHandler { e ->
cont.resumeWithException(e)
})
streams.setStateListener { newState: KafkaStreams.State, _ ->
when (newState) {
KafkaStreams.State.NOT_RUNNING,
KafkaStreams.State.ERROR -> if (cont.isActive) cont.resume(newState)
else -> Unit // do nothing
}
}
cont.invokeOnCancellation {
println("[$appId] coroutine cancelled -> closing. cause:$it")
streams.close()
}
printTopologyDescription(appId, topology)
streams.start()
println("launched Topology $appId")
}
println("exiting Topology $appId: $streamsState")
currentCoroutineContext().job.cancel("$appId: $streamsState")
}
}
// private fun printTopologyDescription(appId: String, topology: Topology) {
// val description: TopologyDescription = topology.describe()
//
// val descFile = Path("./build/$appId.txt").toFile().apply {
// parentFile.mkdirs()
// if (!exists()) createNewFile()
// writeText(description.toString())
// }
//
// println(
// """
// |----------------
// |$appId - ${descFile.canonicalPath}
// |$description
// |----------------
// """.trimMargin()
// )
// }
private fun printTopologyDescription(appId: String, topology: Topology) {
val description: TopologyDescription = topology.describe()
val descFile = Path("./build/$appId.txt").toFile().apply {
parentFile.mkdirs()
if (!exists()) createNewFile()
writeText(description.toString())
}
println(
"""
|----------------
|$appId - ${descFile.canonicalPath}
|$description
|----------------
""".trimMargin()
)
}
companion object {
private fun Map<String, String>.toProperties(): Properties =
entries.fold(Properties()) { props, (k, v) ->
props.apply { setProperty(k, v) }
}
}
// companion object {
// private fun Map<String, String>.toProperties(): Properties =
// entries.fold(Properties()) { props, (k, v) ->
// props.apply { setProperty(k, v) }
// }
// }
}

View file

@ -1,35 +1,35 @@
package dev.adamko.kafkatorio.server.processor
import dev.adamko.kafkatorio.server.config.ApplicationProperties
import dev.adamko.kafkatorio.server.processor.admin.KafkatorioKafkaAdmin
import dev.adamko.kafkatorio.server.security.Authenticator
import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
suspend fun eventsProcessor(
appProps: ApplicationProperties,
syslogServer: SyslogSocketServer,
wsServer: WebmapWebsocketServer,
authenticator: Authenticator,
): Unit = coroutineScope {
val admin = KafkatorioKafkaAdmin(appProps)
admin.createKafkatorioTopics()
val kafkatorioPacketKafkaProducer = KafkatorioPacketKafkaProducer(appProps, syslogServer, authenticator)
val topology = KafkatorioTopology(appProps, wsServer, syslogServer, kafkatorioPacketKafkaProducer)
launch {
topology.start()
println("launched KafkatorioTopology")
awaitCancellation()
}
}
//import dev.adamko.kafkatorio.server.config.ApplicationProperties
//import dev.adamko.kafkatorio.server.processor.admin.KafkatorioKafkaAdmin
//import dev.adamko.kafkatorio.server.security.Authenticator
//import dev.adamko.kafkatorio.server.socket.SyslogSocketServer
//import dev.adamko.kafkatorio.server.web.websocket.WebmapWebsocketServer
//import kotlinx.coroutines.awaitCancellation
//import kotlinx.coroutines.coroutineScope
//import kotlinx.coroutines.launch
//
//
//suspend fun eventsProcessor(
// appProps: ApplicationProperties,
// syslogServer: SyslogSocketServer,
// wsServer: WebmapWebsocketServer,
// authenticator: Authenticator,
//): Unit = coroutineScope {
//
// val admin = KafkatorioKafkaAdmin(appProps)
// admin.createKafkatorioTopics()
//
// val kafkatorioPacketKafkaProducer = KafkatorioPacketKafkaProducer(appProps, syslogServer, authenticator)
//
// val topology = KafkatorioTopology(appProps, wsServer, syslogServer, kafkatorioPacketKafkaProducer)
//
// launch {
// topology.start()
//
// println("launched KafkatorioTopology")
//
// awaitCancellation()
// }
//}

View file

@ -109,3 +109,10 @@ tasks.processRun {
tasks.processKill {
dependsOn(clientKill)
}
val runFactorioClient by tasks.registering {
group = rootProject.name
dependsOn(tasks.processRun)
}

View file

@ -35,7 +35,7 @@ val deployModToLocalServer by tasks.registering(Copy::class) {
into(factorioServerDataDir.dir("mods"))
doLast {
logger.lifecycle("Copying mods ${source.files} to $destinationDir")
logger.lifecycle("Copying mods ${source.files} into $destinationDir")
}
}
@ -75,3 +75,9 @@ idea {
excludeDirs.add(file("src/factorio-server"))
}
}
val runFactorioServer by tasks.registering {
group = rootProject.name
dependsOn(tasks.processRun)
}

View file

@ -20,7 +20,7 @@ services:
driver: syslog
options:
syslog-address: "tcp://host.docker.internal:12034" # pc
#syslog-address: "tcp://localhost:12034" # nuc
# syslog-address: "tcp://localhost:12034" # nuc
syslog-format: "rfc5424"
tag: ${KAFKATORIO_TOKEN}

View file

@ -27,3 +27,9 @@ tasks.dockerEnvUpdate {
// "SCHEMA_REGISTRY_MEM_LIMIT" to "256m",
)
}
val runKafkaCluster by tasks.registering {
group = rootProject.name
dependsOn(tasks.processRun)
}

View file

@ -2,6 +2,6 @@
COMPOSE_PROJECT_NAME=kafkatorio
GRAFANA_VERSION=8.5.0
KAFKATORIO_VERSION=0.9.0
KAFKA_DL_URL=https\://dlcdn.apache.org/kafka/3.2.1/kafka_2.13-3.2.1.tgz
KAFKA_VERSION=3.2.1
KAFKA_DL_URL=https\://dlcdn.apache.org/kafka/3.2.2/kafka_2.13-3.2.2.tgz
KAFKA_VERSION=3.2.2
PROMETHEUS_VERSION=v2.34.0

36
modules/modules.md Normal file
View file

@ -0,0 +1,36 @@
```mermaid
flowchart TD
events-library-->events-processors & events-mod & web-map & events-server-web
kafka[(Kafka)]
events-mod-->infra-factorio-client
subgraph Kafka Workers
events-processor-core --> events-processor-syslog & events-processors
end
kafka<==>events-processors
subgraph infra
subgraph factorio
infra-factorio-client<-->infra-factorio-server
end
infra-factorio-server-->infra-kafka-pipe
infra-kafka-pipe-->kafka
infra-kafka-cluster-->kafka
end
kafka-->events-server-web
events-server-web-->web-map
versions-platform
subgraph experiments
kt-rcon
end
```

View file

@ -178,3 +178,10 @@ tasks.assemble {
tasks.withType<io.kvision.gradle.tasks.KVConvertPoTask>().configureEach {
enabled = false
}
val runWebMap by tasks.registering {
group = rootProject.name
dependsOn(tasks.matching { it.name == "browserDevelopmentRun" })
}

View file

@ -58,13 +58,13 @@ object App : Application() {
require("./css/kafkatorio.css")
WebsocketService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.onEach { tileSavedEvent ->
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
siteStateStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
// reduxStore.map.refreshUpdatedTilePng(tileSavedEvent.filename)
}.launchIn(coroutineScope)
// WebsocketService.packetsFlow
// .filterIsInstance<EventServerPacket.ChunkTileSaved>()
// .onEach { tileSavedEvent ->
//// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
//// siteStateStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
// siteStateStore.getState().factorioGameState?.map?.refreshUpdatedTilePng(tileSavedEvent.filename)
// }.launchIn(coroutineScope)
}

View file

@ -3,8 +3,10 @@ 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.ServerMapTilePngFilename
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.webmap.externals.TileOnLoadFn
import dev.adamko.kafkatorio.webmap.externals.tileOnLoad
import dev.adamko.kafkatorio.webmap.services.WebsocketService
import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.DoneCallback
@ -18,8 +20,15 @@ import io.kvision.maps.externals.leaflet.layer.tile.TileLayer
import io.kvision.utils.px
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.await
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.js.jso
import org.w3c.dom.HTMLElement
@ -36,6 +45,12 @@ class FactorioMap(
private val serverId: FactorioServerId,
) {
private val coroutineScope: CoroutineScope = CoroutineScope(
Dispatchers.Default
+ SupervisorJob(rootJob)
+ CoroutineName("FactorioMap")
)
val kvMap: Maps = Maps {
minHeight = 800.px
minWidth = 800.px
@ -90,6 +105,15 @@ class FactorioMap(
playerIconsLayer.addTo(this)
}
WebsocketService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.onEach { tileSavedEvent ->
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
// siteStateStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
refreshUpdatedTilePng(tileSavedEvent.filename)
}.launchIn(coroutineScope)
}