so close to working performantly.... just the tiles are transparent???

- bump some versions
- MapTile no longer has objectName field
- fix nulls/optional fields in FactorioConfigurationUpdate
- use bytes to encode rgba colours - waaaayyyy smaller
- increase Kafka record size to ~10mb
This commit is contained in:
Adam 2022-02-08 01:35:52 +01:00
parent ea0bfb350b
commit bdb2d9577f
24 changed files with 619 additions and 404 deletions

View file

@ -43,7 +43,6 @@ interface MapChunkPosition {
}
interface MapTile {
objectName: string;
position: MapTilePosition;
prototypeName: string;
}

View file

@ -98,8 +98,6 @@ export namespace Converters {
export function convertTile(tile: LuaTile): MapTile {
return {
objectName: tile.object_name,
position: mapTilePosition(tile.position),
prototypeName: tile.prototype.name,
}

View file

@ -92,8 +92,7 @@ export function handleTilesUpdate(
let convertedTiles: MapTile[] = updatedTiles.map((it) => {
return {
objectName: "LuaTile",
position: it.position,
position: it.position, // TODO use converters for LuaTile
prototypeName: newTileType.name,
}
})

View file

@ -1,6 +1,6 @@
{
"name": "kafkatorio-events-mod",
"version": "0.2.7",
"version": "0.2.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "kafkatorio-events-mod",
"version": "0.2.7",
"version": "0.2.8",
"private": true,
"scripts": {
"build": "tstl",

View file

@ -23,7 +23,12 @@ fun Colour.toHexadecimal() = if (isPercentage()) {
(alpha * 255).coerceIn(0f..255f),
)
} else {
this
Colour(
red.coerceIn(0f..255f),
green.coerceIn(0f..255f),
blue.coerceIn(0f..255f),
alpha.coerceIn(0f..255f),
)
}
fun Colour.toPercentage() = if (isPercentage()) {

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.events.schema.converters.toHexadecimal
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
@ -56,7 +57,7 @@ data class MapBoundingBox(
* Red, green, blue and alpha values, all in range `[0, 1]` or all in range `[0, 255]` if any
* value is > 1.
*
* All values here are optional. Colour channels default to 0, the alpha channel defaults to 1.
* All values here are optional. Colour channels default to `0`, the alpha channel defaults to `1`.
*/
@Serializable
data class Colour(
@ -68,4 +69,39 @@ data class Colour(
val blue: Float = 0f,
@EncodeDefault
val alpha: Float = 1f,
) {
fun toHex() = toHexadecimal().let {
ColourHex(
it.red.toUInt().toUByte(),
it.green.toUInt().toUByte(),
it.blue.toUInt().toUByte(),
it.alpha.toUInt().toUByte(),
)
}
}
/** Size-efficient version of [Colour] */
@Serializable
data class ColourHex(
@EncodeDefault
val red: UByte = UByte.MIN_VALUE,
@EncodeDefault
val green: UByte = UByte.MIN_VALUE,
@EncodeDefault
val blue: UByte = UByte.MIN_VALUE,
@EncodeDefault
val alpha: UByte = UByte.MAX_VALUE,
) {
companion object {
val TRANSPARENT = ColourHex(0u, 0u, 0u, 0u)
}
}
@Serializable
data class MapTile(
val prototypeName: String,
val position: MapTilePosition,
)

View file

@ -16,13 +16,13 @@ data class FactorioConfigurationUpdate(
@Serializable
data class FactorioGameDataUpdate(
val oldVersion: String?,
val newVersion: String?,
val oldVersion: String? = null,
val newVersion: String? = null,
)
@Serializable
data class FactorioModInfo(
val modName: String,
val currentVersion: String?,
val previousVersion: String?,
val currentVersion: String? = null,
val previousVersion: String? = null,
)

View file

@ -137,17 +137,6 @@ data class MapTiles(
override val objectName = ObjectName.LuaTiles
}
@Serializable
data class MapTile(
val prototypeName: String,
val position: MapTilePosition,
)
// : FactorioObjectData()
{
// @EncodeDefault
val objectName: String = ""
}
@Serializable
data class ConsoleChatMessage(
val authorPlayerIndex: UInt?,

View file

@ -203,7 +203,6 @@ class FactorioEventTest : FunSpec({
"surfaceIndex": 1,
"tiles": [
{
"objectName": "LuaTile",
"position": {
"y": 10,
"x": -65
@ -211,23 +210,20 @@ class FactorioEventTest : FunSpec({
"prototypeName": "refined-concrete"
},
{
"objectName": "LuaTile",
"position": {
"y": 11,
"x": -65
},
"prototypeName": "refined-concrete"
"prototypeName": "red-earth"
},
{
"objectName": "LuaTile",
"position": {
"y": 10,
"x": -64
},
"prototypeName": "refined-concrete"
"prototypeName": "blue-cheese"
},
{
"objectName": "LuaTile",
"position": {
"y": 11,
"x": -64
@ -256,11 +252,11 @@ class FactorioEventTest : FunSpec({
position = MapTilePosition(-65, 10)
),
MapTile(
prototypeName = "refined-concrete",
prototypeName = "red-earth",
position = MapTilePosition(-65, 11)
),
MapTile(
prototypeName = "refined-concrete",
prototypeName = "blue-cheese",
position = MapTilePosition(-64, 10)
),
MapTile(
@ -279,6 +275,60 @@ class FactorioEventTest : FunSpec({
expected.data.objectName shouldBe FactorioObjectData.ObjectName.LuaTiles
}
test("Then: expect encode equals json") {
val encoded = jsonMapperKafkatorio.encodeToString(actual)
encoded.shouldEqualJson(json)
}
}
}
context("Given: packet FactorioConfigurationUpdate") {
// language=JSON
val json = """
{
"modVersion": "0.2.8",
"packetType": "CONFIG",
"allMods": [
{
"modName": "kafkatorio-events",
"currentVersion": "0.2.8",
"previousVersion": "0.2.7"
},
{
"modName": "base",
"currentVersion": "1.1.53"
}
],
"factorioData": {}
}
""".trimIndent()
context("When: decoded") {
val actual: FactorioConfigurationUpdate = jsonMapperKafkatorio.decodeFromString(json)
test("parse") {
val expected = FactorioConfigurationUpdate(
modVersion = "0.2.8",
factorioData = FactorioGameDataUpdate(null, null),
allMods = listOf(
FactorioModInfo(
modName = "kafkatorio-events",
currentVersion = "0.2.8",
previousVersion = "0.2.7"
),
FactorioModInfo(
modName = "base",
currentVersion = "1.1.53",
previousVersion = null
),
)
)
actual shouldBe expected
expected.packetType shouldBe KafkatorioPacket.PacketType.CONFIG
}
test("Then: expect encode equals json") {
val encoded = jsonMapperKafkatorio.encodeToString(actual)
encoded.shouldEqualJson(json)

View file

@ -1,16 +1,16 @@
package dev.adamko.kafkatorio.processor
import dev.adamko.kafkatorio.processor.topology.buildFactorioServerMap
import dev.adamko.kafkatorio.processor.topology.factorioServerPacketStream
import dev.adamko.kafkatorio.processor.topology.groupTilesIntoChunksWithColours
import dev.adamko.kafkatorio.processor.topology.playerUpdatesToWsServer
import dev.adamko.kafkatorio.processor.topology.saveMapTiles
import dev.adamko.kafkatorio.processor.topology.splitFactorioServerPacketStream
import dev.adamko.kafkatorio.processor.topology.tileProtoColourDictionary
import java.time.Duration
import org.apache.kafka.streams.KafkaStreams
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.TopologyDescription
class KafkatorioTopology(
private val websocketServer: WebsocketServer,
private val builder: StreamsBuilder = StreamsBuilder(),
@ -20,17 +20,27 @@ class KafkatorioTopology(
const val sourceTopic = "factorio-server-log"
}
fun build() {
val packets = factorioServerPacketStream(builder)
splitFactorioServerPacketStream(packets)
playerUpdatesToWsServer(websocketServer, builder)
val serverMapDataTable = buildFactorioServerMap(packets)
// val serverMapDataTable = buildFactorioServerMap(packets, tilePrototypesTable)
saveMapTiles(serverMapDataTable)
val tileProtoColourDict = tileProtoColourDictionary(packets)
val tilePrototypesTable = groupTilesIntoChunksWithColours(
packets,
tileProtoColourDict,
)
saveMapTiles(tilePrototypesTable)
// the old way...

View file

@ -1,127 +1,196 @@
package dev.adamko.kafkatorio.processor.topology
import dev.adamko.kafkatorio.events.schema.FactorioConfigurationUpdate
import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.FactorioPrototypes
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.MapChunk
import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.events.schema.MapTilePrototype
import dev.adamko.kafkatorio.events.schema.MapTiles
import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kotka.extensions.groupedAs
import dev.adamko.kotka.extensions.materializedAs
import dev.adamko.kotka.extensions.materializedWith
import dev.adamko.kotka.extensions.streams.aggregate
import dev.adamko.kotka.extensions.tables.mapValues
import dev.adamko.kotka.extensions.streams.flatMap
import dev.adamko.kotka.extensions.streams.reduce
import dev.adamko.kotka.extensions.tables.join
import dev.adamko.kotka.kxs.serde
import kotlinx.serialization.Serializable
import org.apache.kafka.streams.kstream.KStream
import org.apache.kafka.streams.kstream.KTable
//@Serializable
//data class FactorioServerMap(
// val serverId: FactorioServerId,
// val tilePrototypes: Map<PrototypeName, MapTilePrototype>,
// val surfaces: Map<SurfaceIndex, FactorioMapSurface>,
//)
//@Serializable
//data class FactorioMapSurface(
// val index: SurfaceIndex,
// val tiles: Map<MapTilePosition, PrototypeName>,
//)
@Serializable
data class FactorioServerMap(
data class ServerMapChunkId(
val serverId: FactorioServerId,
val tilePrototypes: Map<PrototypeName, MapTilePrototype>,
val surfaces: Map<SurfaceIndex, FactorioMapSurface>,
val chunkPosition: MapChunkPosition,
val surfaceIndex: SurfaceIndex,
val zoomLevel: ZoomLevel,
)
@Serializable
data class FactorioMapSurface(
val index: SurfaceIndex,
val tiles: Map<MapTilePosition, PrototypeName>,
)
/** Aggregate [KafkatorioPacket]s into a single [FactorioServerMap] (per [serverId][FactorioServerId]). */
fun buildFactorioServerMap(
factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket>
): KTable<FactorioServerId, FactorioServerMap> {
return factorioServerPacketStream
.groupByKey(
groupedAs("factorio-server-map-data-grouping", jsonMapper.serde(), jsonMapper.serde())
)
.aggregate<FactorioServerId, KafkatorioPacket, FactorioServerMapAggregator>(
"factorio-server-map-data-aggregate",
materializedAs("factorio-server-map-aggregate-store", kxsBinary.serde(), kxsBinary.serde()),
{ FactorioServerMapAggregator() },
) { _: FactorioServerId, packet: KafkatorioPacket, aggregate: FactorioServerMapAggregator ->
aggregate + packet
}
.mapValues(
"finalise-factorio-server-map-aggregate",
materializedWith(kxsBinary.serde(), kxsBinary.serde())
) { readOnlyKey, aggregate ->
val surfaces = aggregate.surfaces.mapValues { (_, surface) ->
FactorioMapSurface(surface.index, surface.tiles)
}
FactorioServerMap(readOnlyKey, aggregate.tilePrototypes, surfaces)
}
}
@Serializable
private data class FactorioServerMapAggregator(
val tilePrototypes: MutableMap<PrototypeName, MapTilePrototype> = mutableMapOf(),
val surfaces: MutableMap<SurfaceIndex, FactorioMapSurfaceAggregator> = mutableMapOf(),
data class ServerMapChunkTiles<Data>(
val chunkId: ServerMapChunkId,
val map: Map<MapTilePosition, Data>,
) {
operator fun plus(packet: KafkatorioPacket): FactorioServerMapAggregator {
return when (packet) {
is FactorioEvent -> {
when (val data = packet.data) {
is MapChunk -> plus(data)
is MapTiles -> plus(data)
// is MapTile -> {} // hmmm... no surface index
else -> this
}
}
is FactorioPrototypes -> plus(packet)
is FactorioConfigurationUpdate -> this
}
}
operator fun plus(mapTiles: MapTiles): FactorioServerMapAggregator {
// println("FactorioServerMapAggregator: adding ${mapTiles.tiles.size} MapTiles from surface ${mapTiles.surfaceIndex}")
val surfaceIndex = SurfaceIndex(mapTiles.surfaceIndex)
val surface = surfaces.getOrPut(surfaceIndex) { FactorioMapSurfaceAggregator(surfaceIndex) }
val surfaceTiles = mapTiles.tiles.associate { it.position to PrototypeName(it.prototypeName) }
surface.tiles.putAll(surfaceTiles)
return this
}
operator fun plus(mapChunk: MapChunk): FactorioServerMapAggregator {
// println("FactorioServerMapAggregator: adding mapChunk ${mapChunk.position}")
return plus(mapChunk.tiles)
}
operator fun plus(packet: FactorioPrototypes): FactorioServerMapAggregator {
// println("FactorioServerMapAggregator: adding FactorioPrototypes")
val packetPrototypes = packet.prototypes
.filterIsInstance<MapTilePrototype>()
.associateBy { PrototypeName(it.name) }
tilePrototypes.putAll(packetPrototypes)
return this
}
operator fun plus(other: ServerMapChunkTiles<Data>) = copy(map = map + other.map)
}
@Serializable
private data class FactorioMapSurfaceAggregator(
val index: SurfaceIndex,
val tiles: MutableMap<MapTilePosition, PrototypeName> = mutableMapOf(),
)
fun groupTilesIntoChunksWithColours(
factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket>,
tileProtoColourDict: KTable<FactorioServerId, TileColourDict>,
): KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> {
// group tiles by server & surface & chunk
val chunkedTilesTable: KTable<ServerMapChunkId, ServerMapChunkTiles<TileProtoHashCode>> =
factorioServerPacketStream
.flatMap("server-map-data.tiles.flatMapByChunk") { key: FactorioServerId, packet: KafkatorioPacket ->
val mapTileData = when (packet) {
is FactorioEvent ->
when (val data = packet.data) {
is MapChunk -> data.tiles
is MapTiles -> data
else -> null
}
else -> null
}
val zoomLevel = ZoomLevel.ZOOM_0
//
// ZoomLevel
// .values
// .mapNotNull { zoomLevel ->
mapTileData
?.tiles
?.groupBy { tile ->
tile.position.toMapChunkPosition(zoomLevel.chunkSize)
}
?.map { (chunkPos, tiles) ->
val chunkId = ServerMapChunkId(
key,
chunkPos,
SurfaceIndex(mapTileData.surfaceIndex),
zoomLevel,
)
val tilesMap = tiles.associate { it.position to TileProtoHashCode(it) }
val chunkTiles = ServerMapChunkTiles(chunkId, tilesMap)
chunkId to chunkTiles
} ?: listOf()
// }.flatten()
}
.groupByKey(
groupedAs("server-map-data.tiles.group", kxsBinary.serde(), kxsBinary.serde())
)
.reduce(
"server-map-data.tiles.reduce",
materializedAs("server-map-data.tiles.store", kxsBinary.serde(), kxsBinary.serde()),
) { a, b -> a + b }
val colourisedChunkTable: KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> =
chunkedTilesTable
.join(
other = tileProtoColourDict,
name = "server-map-data.join-tiles-with-prototypes",
materialized = materializedWith(kxsBinary.serde(), kxsBinary.serde()),
foreignKeyExtractor = { chunkTiles: ServerMapChunkTiles<TileProtoHashCode> -> chunkTiles.chunkId.serverId }
) { tiles: ServerMapChunkTiles<TileProtoHashCode>, colourDict: TileColourDict ->
ServerMapChunkTiles(
tiles.chunkId,
tiles.map.mapValues { (_, code) ->
colourDict.map.getOrElse(code) {
println("missing prototype $code")
ColourHex.TRANSPARENT
}
}
)
}
return colourisedChunkTable
}
//@Serializable
//private data class FactorioServerMapAggregator(
// val tilePrototypes: SortedMap<PrototypeName, Colour> = TreeMap(),
// val surfaces: MutableMap<SurfaceIndex, FactorioMapSurfaceAggregator> = mutableMapOf(),
//) {
//
// operator fun plus(packet: KafkatorioPacket): FactorioServerMapAggregator {
// return when (packet) {
// is FactorioEvent -> {
// when (val data = packet.data) {
// is MapChunk -> plus(data)
// is MapTiles -> plus(data)
// else -> this
// }
// }
// is FactorioPrototypes -> plus(packet)
// is FactorioConfigurationUpdate -> this
// }
// }
//
// operator fun plus(mapTiles: MapTiles): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding ${mapTiles.tiles.size} MapTiles from surface ${mapTiles.surfaceIndex}")
//
// val surfaceIndex = SurfaceIndex(mapTiles.surfaceIndex)
//
// val surface = surfaces.getOrPut(surfaceIndex) { FactorioMapSurfaceAggregator(surfaceIndex) }
//
// val surfaceTiles = mapTiles.tiles.associate { it.position to PrototypeName(it.prototypeName) }
// surface.tiles.putAll(surfaceTiles)
//
// return this
// }
//
// operator fun plus(mapChunk: MapChunk): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding mapChunk ${mapChunk.position}")
// return plus(mapChunk.tiles)
// }
//
// operator fun plus(packet: FactorioPrototypes): FactorioServerMapAggregator {
//// println("FactorioServerMapAggregator: adding FactorioPrototypes")
//
// val packetPrototypes = packet.prototypes
// .filterIsInstance<MapTilePrototype>()
// .associate { PrototypeName(it.name) to it.mapColour }
//
// tilePrototypes.putAll(packetPrototypes)
//
// return this
// }
//
//}
//
//@Serializable
//private data class FactorioMapSurfaceAggregator(
// val index: SurfaceIndex,
// val tiles: MutableMap<MapTilePosition, PrototypeName> = mutableMapOf(),
//)

View file

@ -3,10 +3,9 @@ package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.ImmutableImage
import com.sksamuel.scrimage.ScaleMethod
import com.sksamuel.scrimage.nio.PngWriter
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
import java.awt.image.BufferedImage
import java.io.File
import kotlin.math.abs
@ -15,17 +14,17 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import org.apache.kafka.streams.kstream.KTable
@ -36,106 +35,84 @@ private val cScope = CoroutineScope(
)
/** A bridge between Kafka and Kotlin */
private val serverMapDataFlow: MutableStateFlow<FactorioServerMap?> = MutableStateFlow(null)
private val chunkedTiles: MutableSharedFlow<ServerMapChunkTiles<ColourHex>> =
MutableSharedFlow(
0,
1,
BufferOverflow.SUSPEND
)
fun saveMapTiles(
serverMapTable: KTable<FactorioServerId, FactorioServerMap>
serverMapTable: KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>>
) {
serverMapTable
.toStream()
.foreach { _, value ->
runBlocking(Dispatchers.Default) {
serverMapDataFlow.emit(value)
if (value != null) {
runBlocking(Dispatchers.Default) {
println("emitting chunkedTiles ${value.chunkId} tiles: ${value.map.size}")
chunkedTiles.emit(value)
}
}
}
cScope.launch {
println("launching serverMapDataFlow merging")
serverMapDataFlow
.filterNotNull()
serverMapDataFlow
.filterNotNull()
.conflate()
// .transform { serverMapData ->
.flatMapMerge(10) { serverMapData ->
println("flatMapMerge serverMapData ${serverMapData.serverId}, surfaces ${serverMapData.surfaces.keys}")
val prototypes = serverMapData.tilePrototypes
serverMapData
.surfaces
.values
// .forEach { surfaceData ->
.asFlow()
.flatMapMerge(10) { surfaceData ->
println("flatMapMerge surfaceData ${surfaceData.index}")
// get all tile colours for the surface
val surfaceTileColours = surfaceData.tiles
.entries
.mapNotNull { (tilePosition, prototypeName) ->
prototypes[prototypeName]?.let { prototype ->
tilePosition to prototype.mapColour
}
}
println("surfaceTileColours.size ${surfaceTileColours.size}")
// split a surface into chunks, per zoom-level
ZoomLevel.values
// .forEach { zoom ->
.asFlow()
.flatMapMerge(10) { zoom ->
println("flatMapMerge zoom $zoom")
surfaceTileColours
.groupBy { (tilePosition, _) ->
tilePosition.toMapChunkPosition(zoom.chunkSize)
}
.entries
// .forEach { (chunkPosition, tileColours) ->
.asFlow()
.mapLatest { (chunkPosition, tileColours) ->
println("mapLatest chunkPosition $chunkPosition")
// mapLatest + delay = an alternative to debounce-per-key?
delay(30.seconds)
println("emitting ChunkData $chunkPosition")
ChunkData(
surfaceData.index,
zoom,
chunkPosition,
tileColours.toMap()
)
}
}
}
}
.onEach { chunkData ->
println("starting to save chunkData ${chunkData.chunkPosition}...")
val chunkImageFile = File(chunkData.filename.value)
if (chunkImageFile.parentFile.mkdirs()) {
println("created new map tile parentFile directory ${chunkImageFile.absolutePath}")
supervisorScope {
chunkedTiles
.conflate()
.distinctUntilChanged()
.debounce(5.seconds)
.map {
println("createMapTileImage ${it.chunkId}")
createMapTileImage(it)
}
val image =
createMapTileImage(chunkData.chunkPosition, chunkData.tileColours, chunkData.zoomLevel)
println("saving map tile $chunkImageFile")
val savedTile = image.output(PngWriter.NoCompression, chunkImageFile)
println("savedTile: $savedTile")
}
.launchIn(this)
.collect()
}
}
// cScope.launch {
// println("launching serverMapDataFlow merging")
//
// chunkedTiles
// .runningFold(
// mapOf<ServerMapChunkId, MutableSharedFlow<ServerMapChunkTiles<ColourHex>>>()
// ) { accumulator, value ->
//
// println("folding chunkedTiles ${value.chunkId}, current flows: ${accumulator.keys}")
//
// val chunkFlow = accumulator.getOrElse(value.chunkId) {
//
// println("creating new MutableSharedFlow for ${value.chunkId}")
//
// val input = MutableSharedFlow<ServerMapChunkTiles<ColourHex>>(
// replay = 0,
// extraBufferCapacity = 1,
// onBufferOverflow = BufferOverflow.DROP_OLDEST,
// )
//
// supervisorScope {
// println("launching MutableSharedFlow for ${value.chunkId}")
// input
// .debounce(30.seconds)
// .mapLatest {
// println("createMapTileImage ${it.chunkId}")
// createMapTileImage(it)
// }.launchIn(this)
// }
//
// input
// }
//
// chunkFlow.emit(value)
//
// accumulator + (value.chunkId to chunkFlow)
// }
// .collect()
//
// println("end of cScope.launch")
// }
}
@ -145,35 +122,58 @@ private value class TilePngFilename(
) {
constructor(
surfaceIndex: SurfaceIndex,
zoom: Int,
chunkX: Int,
chunkY: Int,
) : this("src/main/resources/kafkatorio-web-map/s${surfaceIndex.index}/z$zoom/x${chunkX}/y${chunkY}.png")
}
private data class ChunkData(
val surfaceIndex: SurfaceIndex,
val zoomLevel: ZoomLevel,
val chunkPosition: MapChunkPosition,
val tileColours: Map<MapTilePosition, Colour>,
) {
val filename = TilePngFilename(surfaceIndex, zoomLevel.level, chunkPosition.x, chunkPosition.y)
chunkId: ServerMapChunkId
) : this(buildString {
append("src/main/resources/kafkatorio-web-map/s")
append(chunkId.surfaceIndex.index)
append("/z")
append(chunkId.zoomLevel.level)
append("/x")
append(chunkId.chunkPosition.x)
append("/y")
append(chunkId.chunkPosition.y)
append(".png")
})
}
private fun createMapTileImage(
chunkTiles: ServerMapChunkTiles<ColourHex>
) {
println("emitting ChunkData ${chunkTiles.chunkId.chunkPosition}")
println("starting to save chunkData ${chunkTiles.chunkId.chunkPosition}...")
val filename = TilePngFilename(chunkTiles.chunkId)
val chunkImageFile = File(filename.value)
if (chunkImageFile.parentFile.mkdirs()) {
println("created new map tile parentFile directory ${chunkImageFile.absolutePath}")
}
val image =
createMapTileImage(
chunkTiles.chunkId.chunkPosition,
chunkTiles.map,
chunkTiles.chunkId.zoomLevel
)
println("saving map tile $chunkImageFile")
val savedTile = image.output(PngWriter.NoCompression, chunkImageFile)
println("savedTile: $savedTile")
}
private fun createMapTileImage(
chunkPosition: MapChunkPosition,
chunkColours: Map<MapTilePosition, Colour>,
chunkColours: Map<MapTilePosition, ColourHex>,
z: ZoomLevel
): ImmutableImage {
val chunkImage = ImmutableImage.filled(
z.chunkSize,
z.chunkSize,
COLOUR_TRANSPARENT.awt(),
ColourHex.TRANSPARENT.toRgbColor().awt(),
BufferedImage.TYPE_INT_ARGB
)

View file

@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.color.RGBColor
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.ColourHex
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlinx.serialization.Serializable
@ -23,12 +24,12 @@ value class FactorioServerId(val id: String) {
}
fun Colour.toRgbColor(): RGBColor {
fun ColourHex.toRgbColor(): RGBColor {
return RGBColor(
red.roundToInt(),
green.roundToInt(),
blue.roundToInt(),
alpha.roundToInt(),
red.toInt(),
green.toInt(),
blue.toInt(),
alpha.toInt(),
)
}
@ -36,15 +37,14 @@ fun Colour.toRgbColor(): RGBColor {
enum class ZoomLevel(
val level: Int
) {
ZOOM_0(0),
ZOOM_1(1),
ZOOM_2(2),
ZOOM_3(3),
ZOOM_4(4),
ZOOM_5(5),
ZOOM_0(0), // 512
ZOOM_1(1), // 256
ZOOM_2(2), // 128
ZOOM_3(3), // 64
ZOOM_4(4), // 32
;
val chunkSize: Int = 2f.pow(10 - level).roundToInt()
val chunkSize: Int = 2f.pow(9 - level).roundToInt()
init {
require(chunkSize > 0) { "chunkSize must be positive" }

View file

@ -1,19 +1,17 @@
package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.color.RGBColor
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.FactorioPrototypes
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTilePrototype
import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kotka.extensions.consumedAs
import dev.adamko.kotka.extensions.materializedWith
import dev.adamko.kotka.extensions.streams.flatMap
import dev.adamko.kotka.extensions.materializedAs
import dev.adamko.kotka.extensions.streams.mapValues
import dev.adamko.kotka.extensions.streams.toTable
import dev.adamko.kotka.kxs.serde
import kotlinx.serialization.Serializable
import org.apache.kafka.common.serialization.Serdes
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.KStream
import org.apache.kafka.streams.kstream.KTable
@ -24,50 +22,45 @@ value class PrototypeName(val name: String) {
}
//fun prototypesTable(builder: StreamsBuilder): KTable<PrototypeName, MapTilePrototype> {
// return builder.stream<FactorioPacketKey, FactorioPrototypes>(
// "kafkatorio.${KafkatorioPacket.PacketType.PROTOTYPES}.all",
// consumedAs(
// "consume-all-prototypes-packets",
// jsonMapper.serde(),
// jsonMapper.serde()
// )
// )
// .flatMap("map-MapTilePrototype") { _, prototypes: FactorioPrototypes ->
// prototypes.prototypes
// .filterIsInstance<MapTilePrototype>()
// .map { PrototypeName(it.name) to it }
// }
// .toTable(
// materializedWith(
// kxsBinary.serde(),
// kxsBinary.serde()
// )
// )
//}
@JvmInline
@Serializable
value class TileProtoHashCode(val code: Int) {
constructor(prototype: MapTilePrototype) : this(prototype.name.hashCode())
constructor(tile: MapTile) : this(tile.prototypeName.hashCode())
}
/** Get the [Colour] of each [MapTilePrototype] */
fun tilePrototypeColourTable(builder: StreamsBuilder): KTable<PrototypeName, Colour> {
return builder.stream<FactorioServerId, FactorioPrototypes>(
"kafkatorio.${KafkatorioPacket.PacketType.PROTOTYPES}.all",
consumedAs(
"consume-all-prototypes-packets",
jsonMapper.serde(),
jsonMapper.serde()
)
)
.flatMap("map-MapTilePrototype") { _, prototypes: FactorioPrototypes ->
prototypes.prototypes
.filterIsInstance<MapTilePrototype>()
.map {
PrototypeName(it.name) to it.mapColour
}
@JvmInline
@Serializable
value class TileColourDict(val map: Map<TileProtoHashCode, ColourHex>) {
operator fun plus(other: TileColourDict) = TileColourDict(this.map + other.map)
}
/** Aggregate [KafkatorioPacket]s into a single [FactorioServerMap] (per [serverId][FactorioServerId]). */
fun tileProtoColourDictionary(
factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket>
): KTable<FactorioServerId, TileColourDict> {
// get all the prototypes
return factorioServerPacketStream
.mapValues("server-map-data.tile-prototypes.mapValues") { _, packet ->
val map = when (packet) {
is FactorioPrototypes -> packet
.prototypes
.filterIsInstance<MapTilePrototype>()
.associate { TileProtoHashCode(it) to it.mapColour.toHex() }
else -> mapOf()
}
TileColourDict(map)
}
.toTable(
materializedWith(
kxsBinary.serde(),
kxsBinary.serde()
"server-map-data.tile-prototypes",
materializedAs(
"server-map-data.tile-prototypes.store",
jsonMapper.serde(),
jsonMapper.serde()
)
)
}

View file

@ -13,89 +13,89 @@ import kotlin.math.abs
import kotlin.math.roundToInt
import org.apache.kafka.streams.kstream.KTable
val COLOUR_TRANSPARENT = RGBColor.fromAwt(Color(0f, 0f, 0f, 0f))
fun saveTileImages(
webMapTiles: KTable<WebMapTileChunkPosition, WebMapTileChunkPixels>
) {
webMapTiles
.toStream("get-webmap-tile-updates")
.foreach { position: WebMapTileChunkPosition, pixels: WebMapTileChunkPixels ->
runCatching {
saveMapTilesPng(position, pixels.pixels)
}.onFailure { e ->
println("error saving map tile png chunk: $position")
e.printStackTrace()
throw e
}
}
}
//val saveMapTilesContext = Dispatchers.IO.limitedParallelism(1)
private fun saveMapTilesPng(
chunkPosition: WebMapTileChunkPosition,
pixels: Map<MapTilePosition, Colour>,
) {
val chunkOriginX: Int = chunkPosition.x * chunkPosition.chunkSize // - 1
val chunkOriginY: Int = chunkPosition.y * chunkPosition.chunkSize // - 1
val surfaceIndex: Int = chunkPosition.surfaceIndex.index
val chunkImage =
ImmutableImage.filled(
chunkPosition.chunkSize,
chunkPosition.chunkSize,
COLOUR_TRANSPARENT.awt(),
BufferedImage.TYPE_INT_ARGB
)
pixels.forEach { (tilePosition, colour) ->
val rgbColour = colour.toRgbColor()
val pixelX = abs(abs(tilePosition.x) - abs(chunkOriginX))
val pixelY = abs(abs(tilePosition.y) - abs(chunkOriginY))
chunkImage.setColor(
pixelX,
pixelY,
rgbColour
)
}
val zoom = 1u
val file =
File(tileFilename("$surfaceIndex", "$zoom", "${chunkPosition.x}", "${chunkPosition.y}"))
if (file.parentFile.mkdirs()) {
println("created new map tile parentFile directory ${file.absolutePath}")
}
println("saving map tile $file")
val savedTile = chunkImage.output(PngWriter.NoCompression, file)
println("savedTile: $savedTile")
}
fun tileFilename(
surfaceIndex: String,
zoom: String,
chunkX: String,
chunkY: String,
) = "src/main/resources/kafkatorio-web-map/s${surfaceIndex}/z$zoom/x${chunkX}/y${chunkY}.png"
//fun subdivide(tile: ImmutableImage, size: Int) {
//
// val halfSize = size / 2
//val COLOUR_TRANSPARENT: RGBColor = RGBColor.fromAwt(Color(0f, 0f, 0f, 0f))
//
// val bl = tile.resizeTo(halfSize, halfSize, Position.BottomLeft)
// val savedTile = bl.output(PngWriter.NoCompression, file)
//
// tile.resizeTo(halfSize, halfSize, Position.BottomRight)
// tile.resizeTo(halfSize, halfSize, Position.TopLeft)
// tile.resizeTo(halfSize, halfSize, Position.TopRight)
//fun saveTileImages(
// webMapTiles: KTable<WebMapTileChunkPosition, WebMapTileChunkPixels>
//) {
//
// webMapTiles
// .toStream("get-webmap-tile-updates")
// .foreach { position: WebMapTileChunkPosition, pixels: WebMapTileChunkPixels ->
// runCatching {
// saveMapTilesPng(position, pixels.pixels)
// }.onFailure { e ->
// println("error saving map tile png chunk: $position")
// e.printStackTrace()
// throw e
// }
// }
//}
//
////val saveMapTilesContext = Dispatchers.IO.limitedParallelism(1)
//
//
//private fun saveMapTilesPng(
// chunkPosition: WebMapTileChunkPosition,
// pixels: Map<MapTilePosition, Colour>,
//) {
// val chunkOriginX: Int = chunkPosition.x * chunkPosition.chunkSize // - 1
// val chunkOriginY: Int = chunkPosition.y * chunkPosition.chunkSize // - 1
// val surfaceIndex: Int = chunkPosition.surfaceIndex.index
//
// val chunkImage =
// ImmutableImage.filled(
// chunkPosition.chunkSize,
// chunkPosition.chunkSize,
// COLOUR_TRANSPARENT.awt(),
// BufferedImage.TYPE_INT_ARGB
// )
//
// pixels.forEach { (tilePosition, colour) ->
//
// val rgbColour = colour.toRgbColor()
//
// val pixelX = abs(abs(tilePosition.x) - abs(chunkOriginX))
// val pixelY = abs(abs(tilePosition.y) - abs(chunkOriginY))
//
// chunkImage.setColor(
// pixelX,
// pixelY,
// rgbColour
// )
//
// }
//
// val zoom = 1u
// val file =
// File(tileFilename("$surfaceIndex", "$zoom", "${chunkPosition.x}", "${chunkPosition.y}"))
//
// if (file.parentFile.mkdirs()) {
// println("created new map tile parentFile directory ${file.absolutePath}")
// }
//
// println("saving map tile $file")
// val savedTile = chunkImage.output(PngWriter.NoCompression, file)
// println("savedTile: $savedTile")
//}
//
//fun tileFilename(
// surfaceIndex: String,
// zoom: String,
// chunkX: String,
// chunkY: String,
//) = "src/main/resources/kafkatorio-web-map/s${surfaceIndex}/z$zoom/x${chunkX}/y${chunkY}.png"
//
////fun subdivide(tile: ImmutableImage, size: Int) {
////
//// val halfSize = size / 2
////
//// val bl = tile.resizeTo(halfSize, halfSize, Position.BottomLeft)
//// val savedTile = bl.output(PngWriter.NoCompression, file)
////
//// tile.resizeTo(halfSize, halfSize, Position.BottomRight)
//// tile.resizeTo(halfSize, halfSize, Position.TopLeft)
//// tile.resizeTo(halfSize, halfSize, Position.TopRight)
////
////}

View file

@ -0,0 +1,62 @@
package dev.adamko.kafkatorio.processor.misc
import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kafkatorio.processor.topology.FactorioServerId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkId
import dev.adamko.kafkatorio.processor.topology.ServerMapChunkTiles
import dev.adamko.kafkatorio.processor.topology.SurfaceIndex
import dev.adamko.kafkatorio.processor.topology.ZoomLevel
import java.util.UUID
import kotlinx.serialization.encodeToByteArray
import org.junit.jupiter.api.Test
class Blah {
@Test
fun asdad() {
val tiles = ServerMapChunkTiles<ColourHex>(
chunkId = ServerMapChunkId(
serverId = FactorioServerId(UUID.randomUUID().toString()),
chunkPosition = MapChunkPosition(Int.MAX_VALUE, Int.MAX_VALUE),
surfaceIndex = SurfaceIndex(Int.MAX_VALUE),
zoomLevel = ZoomLevel.ZOOM_0
),
buildMap {
repeat(512) { x ->
repeat(512) { y ->
put(MapTilePosition(x, y), ColourHex(255u, 255u, 255u, 255u))
}
}
}
)
val encode = kxsBinary.encodeToByteArray(tiles)
encode.inputStream()
val size = encode.size * Byte.SIZE_BYTES
println(size)
val map2 = buildList {
repeat(512) { x ->
repeat(512) { y ->
val element = (x to y) to Int.MAX_VALUE
add(element)
}
}
}
// .toTypedArray()
val encode2 = kxsBinary.encodeToByteArray(map2)
encode2.inputStream()
val size2 = encode2.size * Byte.SIZE_BYTES
println(size2)
}
}

View file

@ -1,4 +1,4 @@
# Do not edit manually. This file was created with task 'dockerEnv'
COMPOSE_PROJECT_NAME=kafkatorio
FACTORIO_VERSION=1.1.53
KAFKATORIO_VERSION=0.2.7
KAFKATORIO_VERSION=0.2.8

View file

@ -1,3 +1,3 @@
# Do not edit manually. This file was created with task 'dockerEnv'
COMPOSE_PROJECT_NAME=kafkatorio
KAFKATORIO_VERSION=0.2.7
KAFKATORIO_VERSION=0.2.8

View file

@ -120,3 +120,8 @@ log.retention.check.interval.ms=300000
### Compression ###
#compression.type=snappy
#linger.ms=5
### record size ###
message.max.bytes=10485760
replica.fetch.max.bytes=10485760

View file

@ -1,4 +1,4 @@
ARG KAFKA_DL_URL=https://dlcdn.apache.org/kafka/3.0.0/kafka_2.13-3.0.0.tgz
ARG KAFKA_DL_URL=https://dlcdn.apache.org/kafka/3.1.0/kafka_2.13-3.1.0.tgz
## builder ##
FROM debian:11-slim as Kafka-Download

View file

@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
plugins {
id("dev.adamko.kafkatorio.lang.kotlin-js")
kotlin("plugin.serialization")
id("io.kvision") version "5.7.1"
id("io.kvision") version "5.8.1"
}
kotlin {
@ -48,8 +48,8 @@ kotlin {
implementation(projects.modules.eventsSchema)
implementation("org.jetbrains.kotlinx:kotlinx-nodejs:0.0.7")
implementation("org.jetbrains.kotlinx:kotlinx-html:0.7.3")
implementation(libs.kotlinx.nodejs)
implementation(libs.kotlinx.html)
kvision("kvision")
kvision("kvision-bootstrap")