massive tile processing speed improvement
This commit is contained in:
parent
a557c437cc
commit
2c518e396b
3 changed files with 237 additions and 246 deletions
|
|
@ -16,7 +16,6 @@ import dev.adamko.kafkatorio.events.schema.MapTilePosition
|
||||||
import dev.adamko.kafkatorio.events.schema.MapTilePrototype
|
import dev.adamko.kafkatorio.events.schema.MapTilePrototype
|
||||||
import dev.adamko.kafkatorio.events.schema.MapTiles
|
import dev.adamko.kafkatorio.events.schema.MapTiles
|
||||||
import dev.adamko.kafkatorio.events.schema.converters.leftTopTile
|
import dev.adamko.kafkatorio.events.schema.converters.leftTopTile
|
||||||
import dev.adamko.kafkatorio.events.schema.converters.rightBottomTile
|
|
||||||
import dev.adamko.kafkatorio.events.schema.converters.toHexadecimal
|
import dev.adamko.kafkatorio.events.schema.converters.toHexadecimal
|
||||||
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
|
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
|
||||||
import dev.adamko.kotka.extensions.consumedAs
|
import dev.adamko.kotka.extensions.consumedAs
|
||||||
|
|
@ -24,14 +23,16 @@ import dev.adamko.kotka.extensions.groupedAs
|
||||||
import dev.adamko.kotka.extensions.materializedAs
|
import dev.adamko.kotka.extensions.materializedAs
|
||||||
import dev.adamko.kotka.extensions.materializedWith
|
import dev.adamko.kotka.extensions.materializedWith
|
||||||
import dev.adamko.kotka.extensions.producedAs
|
import dev.adamko.kotka.extensions.producedAs
|
||||||
|
import dev.adamko.kotka.extensions.streams.aggregate
|
||||||
import dev.adamko.kotka.extensions.streams.filter
|
import dev.adamko.kotka.extensions.streams.filter
|
||||||
import dev.adamko.kotka.extensions.streams.flatMap
|
import dev.adamko.kotka.extensions.streams.groupBy
|
||||||
import dev.adamko.kotka.extensions.streams.map
|
import dev.adamko.kotka.extensions.streams.map
|
||||||
import dev.adamko.kotka.extensions.streams.mapValues
|
import dev.adamko.kotka.extensions.streams.mapValues
|
||||||
import dev.adamko.kotka.extensions.streams.merge
|
import dev.adamko.kotka.extensions.streams.merge
|
||||||
|
import dev.adamko.kotka.extensions.streams.reduce
|
||||||
import dev.adamko.kotka.extensions.streams.to
|
import dev.adamko.kotka.extensions.streams.to
|
||||||
import dev.adamko.kotka.extensions.streams.toTable
|
|
||||||
import dev.adamko.kotka.extensions.tables.join
|
import dev.adamko.kotka.extensions.tables.join
|
||||||
|
import dev.adamko.kotka.extensions.tables.mapValues
|
||||||
import dev.adamko.kotka.kxs.serde
|
import dev.adamko.kotka.kxs.serde
|
||||||
import dev.adamko.kotka.topicdata.TopicRecord
|
import dev.adamko.kotka.topicdata.TopicRecord
|
||||||
import dev.adamko.kotka.topicdata.flatMapTopicRecords
|
import dev.adamko.kotka.topicdata.flatMapTopicRecords
|
||||||
|
|
@ -42,6 +43,7 @@ import java.time.Duration
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import org.apache.kafka.common.serialization.Serdes
|
import org.apache.kafka.common.serialization.Serdes
|
||||||
|
|
@ -68,37 +70,41 @@ class KafkatorioTopology(
|
||||||
const val sourceTopic = "factorio-server-log"
|
const val sourceTopic = "factorio-server-log"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var tilePrototypesStore: ReadOnlyKeyValueStore<String, ValueAndTimestamp<MapTilePrototype>>
|
// private lateinit var tilePrototypesStore: ReadOnlyKeyValueStore<String, ValueAndTimestamp<MapTilePrototype>>
|
||||||
|
|
||||||
fun build() {
|
fun build() {
|
||||||
|
|
||||||
parseAndSplitServerLog()
|
parseAndSplitServerLog()
|
||||||
sendPlayerUpdatesToWebSocket()
|
sendPlayerUpdatesToWebSocket()
|
||||||
// createMapTileImages()
|
// createMapTileImages()
|
||||||
val tilePrototypesTable: KTable<String, MapTilePrototype> = tilePrototypesTable()
|
val tilePrototypesTable: KTable<SurfaceIndex, SurfacePrototypesData> = tilePrototypesTable()
|
||||||
mapChunksStream(tilePrototypesTable)
|
|
||||||
|
val tilesTable: KTable<TileUpdateRecordKey, TileUpdateRecord> = buildTilesTable()
|
||||||
|
|
||||||
|
val tilesGroupedByChunk: KTable<MapChunkDataPosition, MapChunkData> =
|
||||||
|
buildChunkedTilesTable(tilesTable)
|
||||||
|
|
||||||
|
createChunkTileImage(tilePrototypesTable, tilesGroupedByChunk)
|
||||||
|
|
||||||
val topology = builder.build()
|
val topology = builder.build()
|
||||||
|
|
||||||
val streams = KafkaStreams(topology, appProps.kafkaConfig)
|
val streams = KafkaStreams(topology, appProps.kafkaConfig)
|
||||||
|
|
||||||
// streams.cleanUp()
|
// streams.cleanUp()
|
||||||
|
|
||||||
streams.setUncaughtExceptionHandler(StreamsExceptionHandler())
|
streams.setUncaughtExceptionHandler(StreamsExceptionHandler())
|
||||||
|
Runtime.getRuntime().addShutdownHook(Thread { streams.close(Duration.ofSeconds(1)) })
|
||||||
Runtime.getRuntime().addShutdownHook(Thread { streams.close(Duration.ofSeconds(10)) })
|
|
||||||
|
|
||||||
|
|
||||||
val description: TopologyDescription = topology.describe()
|
val description: TopologyDescription = topology.describe()
|
||||||
println(description)
|
println(description)
|
||||||
|
|
||||||
streams.start()
|
streams.start()
|
||||||
|
|
||||||
tilePrototypesStore = streams.store(
|
// tilePrototypesStore = streams.store(
|
||||||
StoreQueryParameters.fromNameAndType(
|
// StoreQueryParameters.fromNameAndType(
|
||||||
tilePrototypesTable.queryableStoreName(),
|
// tilePrototypesTable.queryableStoreName(),
|
||||||
QueryableStoreTypes.timestampedKeyValueStore<String, MapTilePrototype>()
|
// QueryableStoreTypes.timestampedKeyValueStore<String, MapTilePrototype>()
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseAndSplitServerLog() {
|
private fun parseAndSplitServerLog() {
|
||||||
|
|
@ -124,11 +130,11 @@ class KafkatorioTopology(
|
||||||
) { _: String, value: KafkatorioPacket, _: RecordContext ->
|
) { _: String, value: KafkatorioPacket, _: RecordContext ->
|
||||||
// println("[$key] sending event:${value.eventType} to topic:${value.data.objectName()}")
|
// println("[$key] sending event:${value.eventType} to topic:${value.data.objectName()}")
|
||||||
when (value) {
|
when (value) {
|
||||||
is FactorioEvent ->
|
is FactorioEvent ->
|
||||||
"kafkatorio.${value.packetType.name}.${value.data.objectName.name}"
|
"kafkatorio.${value.packetType.name}.${value.data.objectName.name}"
|
||||||
is FactorioConfigurationUpdate ->
|
is FactorioConfigurationUpdate ->
|
||||||
"kafkatorio.${value.packetType.name}.FactorioConfigurationUpdate"
|
"kafkatorio.${value.packetType.name}.FactorioConfigurationUpdate"
|
||||||
is FactorioPrototypes ->
|
is FactorioPrototypes ->
|
||||||
"kafkatorio.${value.packetType.name}.all"
|
"kafkatorio.${value.packetType.name}.all"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,30 +152,67 @@ class KafkatorioTopology(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tilePrototypesTable(): KTable<String, MapTilePrototype> {
|
@Serializable
|
||||||
|
data class SurfaceIndex(
|
||||||
|
val surfaceIndex: Int
|
||||||
|
) {
|
||||||
|
override fun toString() = "$surfaceIndex"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SurfacePrototypesData(
|
||||||
|
val surfaceIndex: SurfaceIndex,
|
||||||
|
val mapTilePrototypes: Set<MapTilePrototype>,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun tilePrototypesTable(): KTable<SurfaceIndex, SurfacePrototypesData> {
|
||||||
|
|
||||||
// val tilePrototypesStreamName = "kafkatorio.stream.prototypes-tiles"
|
// val tilePrototypesStreamName = "kafkatorio.stream.prototypes-tiles"
|
||||||
// val tilePrototypesStoreName = "kafkatorio.store.prototypes-tiles"
|
// val tilePrototypesStoreName = "kafkatorio.store.prototypes-tiles"
|
||||||
|
|
||||||
return builder.stream(
|
return builder.stream(
|
||||||
"kafkatorio.${KafkatorioPacket.PacketType.PROTOTYPES}.all",
|
"kafkatorio.${KafkatorioPacket.PacketType.PROTOTYPES}.all",
|
||||||
Consumed.with(Serdes.String(), jsonMapper.serde<FactorioPrototypes>())
|
consumedAs(
|
||||||
|
"consume-all-prototypes-packets",
|
||||||
|
Serdes.String(),
|
||||||
|
jsonMapper.serde<FactorioPrototypes>()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.flatMap("flatten-map-tile-prototypes") { _, prototypes: FactorioPrototypes ->
|
.map("flatten-map-tile-prototypes") { _, prototypes: FactorioPrototypes ->
|
||||||
prototypes.prototypes
|
val tilePrototypes = prototypes.prototypes
|
||||||
.filterIsInstance<MapTilePrototype>()
|
.filterIsInstance<MapTilePrototype>()
|
||||||
.map { proto -> proto.name to proto }
|
.toSet()
|
||||||
|
val key = SurfaceIndex(1) // TODO make surfaceIndex come from Factorio, not hard coded
|
||||||
|
val data = SurfacePrototypesData(key, tilePrototypes)
|
||||||
|
key to data
|
||||||
}
|
}
|
||||||
.peek { _, value ->
|
.peek { _, value ->
|
||||||
println("tile prototype: ${value.name}")
|
println("tile prototypes: ${value.mapTilePrototypes.joinToString { it.name }}")
|
||||||
}
|
}
|
||||||
.toTable(
|
.groupByKey(
|
||||||
materializedAs(
|
groupedAs(
|
||||||
"kafkatorio.prototypes.map-tiles",
|
"grouping-tile-prototypes-by-surface-index",
|
||||||
jsonMapper.serde(),
|
jsonMapper.serde(),
|
||||||
jsonMapper.serde()
|
jsonMapper.serde()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.reduce(
|
||||||
|
"group-surface-prototypes",
|
||||||
|
materializedWith(
|
||||||
|
// "kafkatorio.surface-prototypes",
|
||||||
|
jsonMapper.serde(),
|
||||||
|
jsonMapper.serde()
|
||||||
|
)
|
||||||
|
) { value1: SurfacePrototypesData, value2: SurfacePrototypesData ->
|
||||||
|
value1.copy(mapTilePrototypes = value1.mapTilePrototypes + value2.mapTilePrototypes)
|
||||||
|
}
|
||||||
|
// .toTable(
|
||||||
|
// materializedAs(
|
||||||
|
// "kafkatorio.prototypes.map-tiles",
|
||||||
|
// jsonMapper.serde(),
|
||||||
|
// jsonMapper.serde()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
// .to(
|
// .to(
|
||||||
// tilePrototypesStreamName,
|
// tilePrototypesStreamName,
|
||||||
// )
|
// )
|
||||||
|
|
@ -190,137 +233,166 @@ class KafkatorioTopology(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MapTileWithPrototype(
|
data class TileUpdateRecordKey(
|
||||||
val position: MapTilePosition,
|
val surfaceIndex: SurfaceIndex,
|
||||||
val prototype: MapTilePrototype,
|
val tilePosition: MapTilePosition,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TileUpdateRecord(
|
||||||
|
val surfaceIndex: SurfaceIndex,
|
||||||
|
val tilePosition: MapTilePosition,
|
||||||
|
val tile: MapTile,
|
||||||
|
) : TopicRecord<TileUpdateRecordKey> {
|
||||||
|
@Transient
|
||||||
|
override val topicKey: TileUpdateRecordKey = TileUpdateRecordKey(surfaceIndex, tilePosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTilesTable(): KTable<TileUpdateRecordKey, TileUpdateRecord> {
|
||||||
|
|
||||||
|
val luaTilesUpdatesStream: KStream<String, MapTiles> = builder.stream(
|
||||||
|
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
|
||||||
|
consumedAs("consume-map-tiles-packets", Serdes.String(), jsonMapper.serde<FactorioEvent>())
|
||||||
|
)
|
||||||
|
.filter("events.filter.map-tiles") { _: String, event: FactorioEvent -> event.data is MapTiles }
|
||||||
|
.mapValues("events.extract-map-tiles") { _, event: FactorioEvent -> (event.data as? MapTiles)!! }
|
||||||
|
|
||||||
|
val chunkTilesUpdateStream: KStream<String, MapTiles> = builder.stream(
|
||||||
|
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
|
||||||
|
consumedAs("consume-map-chunk-packets", Serdes.String(), jsonMapper.serde<FactorioEvent>())
|
||||||
|
)
|
||||||
|
.filter("events.filter.map-chunks") { _: String, event: FactorioEvent -> event.data is MapChunk }
|
||||||
|
.mapValues("events.extract-map-chunks") { _, event: FactorioEvent -> (event.data as? MapChunk)!!.tiles }
|
||||||
|
|
||||||
|
return luaTilesUpdatesStream
|
||||||
|
.merge("luaTilesUpdatesStream-and-chunkTilesUpdateStream", chunkTilesUpdateStream)
|
||||||
|
.flatMapTopicRecords("all-tiles-convert-to-TileUpdateRecord") { _, mapTiles: MapTiles ->
|
||||||
|
// println("all map tiles update ${mapTiles.tiles.size}")
|
||||||
|
mapTiles.tiles.map { tile ->
|
||||||
|
TileUpdateRecord(
|
||||||
|
SurfaceIndex(mapTiles.surfaceIndex),
|
||||||
|
tile.position,
|
||||||
|
tile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTable(materializedWith(jsonMapper.serde(), jsonMapper.serde()))
|
||||||
|
// .suppress(
|
||||||
|
// Suppressed.untilTimeLimit(
|
||||||
|
// Duration.ofSeconds(30),
|
||||||
|
// Suppressed.BufferConfig
|
||||||
|
// .maxRecords(100)
|
||||||
|
// .emitEarlyWhenFull()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MapChunkDataPosition(
|
data class MapChunkDataPosition(
|
||||||
val position: MapChunkPosition,
|
val position: MapChunkPosition,
|
||||||
val surfaceIndex: Int,
|
val surfaceIndex: SurfaceIndex,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MapChunkData(
|
data class MapChunkData(
|
||||||
val chunkPosition: MapChunkDataPosition,
|
val chunkPosition: MapChunkDataPosition,
|
||||||
val tiles: Set<MapTile>,
|
val tiles: Set<MapTile>,
|
||||||
val tilePrototypes: Set<MapTilePrototype>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun mapChunksStream(tilePrototypesTable: KTable<String, MapTilePrototype>) {
|
private fun buildChunkedTilesTable(
|
||||||
|
tilesTable: KTable<TileUpdateRecordKey, TileUpdateRecord>
|
||||||
val luaTilesUpdatesStream: KStream<String, MapTiles> = builder.stream(
|
): KTable<MapChunkDataPosition, MapChunkData> {
|
||||||
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
|
|
||||||
Consumed.with(Serdes.String(), jsonMapper.serde<FactorioEvent>())
|
|
||||||
)
|
|
||||||
// .peek { k, v ->
|
|
||||||
// println("trying to handle MapChunk update... $k: ${v.eventType}")
|
|
||||||
// }
|
|
||||||
.filter("events.filter.map-tiles") { _: String, event: FactorioEvent -> event.data is MapTiles }
|
|
||||||
.mapValues("events.extract-map-tiles") { _, event: FactorioEvent -> (event.data as? MapTiles)!! }
|
|
||||||
// .peek { key, value ->
|
|
||||||
// println("MapChunk update $key, tiles count: ${value.tiles.size}")
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
val chunkTilesUpdateStream: KStream<String, MapTiles> = builder.stream(
|
|
||||||
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
|
|
||||||
Consumed.with(Serdes.String(), jsonMapper.serde<FactorioEvent>())
|
|
||||||
)
|
|
||||||
.peek { k, v ->
|
|
||||||
println("trying to handle LuaTiles update... $k: ${v.eventType} / ${v.data is MapChunk}")
|
|
||||||
}
|
|
||||||
.filter("events.filter.map-chunks") { _: String, event: FactorioEvent -> event.data is MapChunk }
|
|
||||||
.mapValues("events.extract-map-chunks") { _, event: FactorioEvent -> (event.data as? MapChunk)!!.tiles }
|
|
||||||
.peek { key, value ->
|
|
||||||
println("LuaTiles update $key, tiles count: ${value.tiles.size}")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TileUpdateRecordKey(
|
data class MapChunkDataAggregator(
|
||||||
val surfaceIndex: Int,
|
val tiles: MutableSet<MapTile> = mutableSetOf(),
|
||||||
val tilePosition: MapTilePosition,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
return tilesTable
|
||||||
data class TileUpdateRecord(
|
|
||||||
val surfaceIndex: Int,
|
|
||||||
val tilePosition: MapTilePosition,
|
|
||||||
val tile: MapTile,
|
|
||||||
val tilePrototype: MapTilePrototype?,
|
|
||||||
) : TopicRecord<TileUpdateRecordKey> {
|
|
||||||
override val topicKey: TileUpdateRecordKey = TileUpdateRecordKey(surfaceIndex, tilePosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
val allTileUpdatesStream: KStream<TileUpdateRecordKey, TileUpdateRecord> =
|
|
||||||
luaTilesUpdatesStream
|
|
||||||
.merge("luaTilesUpdatesStream-and-chunkTilesUpdateStream", chunkTilesUpdateStream)
|
|
||||||
.flatMapTopicRecords("all-tiles-convert-to-TileUpdateRecord") { _, mapTiles: MapTiles ->
|
|
||||||
// println("all map tiles update ${mapTiles.tiles.size}")
|
|
||||||
mapTiles.tiles.map { tile ->
|
|
||||||
TileUpdateRecord(
|
|
||||||
mapTiles.surfaceIndex,
|
|
||||||
tile.position,
|
|
||||||
tile,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toTable(
|
|
||||||
"all-tile-updates-table",
|
|
||||||
materializedWith(jsonMapper.serde(), jsonMapper.serde())
|
|
||||||
)
|
|
||||||
.join(
|
|
||||||
tilePrototypesTable,
|
|
||||||
"enrich-tiles-with-prototypes",
|
|
||||||
materializedWith(jsonMapper.serde(), jsonMapper.serde()),
|
|
||||||
{ it.tile.prototypeName },
|
|
||||||
) { v1: TileUpdateRecord, v2: MapTilePrototype ->
|
|
||||||
v1.copy(tilePrototype = v2)
|
|
||||||
}
|
|
||||||
.toStream()
|
|
||||||
.filter { _, value ->
|
|
||||||
value.tilePrototype != null
|
|
||||||
}
|
|
||||||
|
|
||||||
val chunksTable: KTable<MapChunkDataPosition, MapChunkData> =
|
|
||||||
allTileUpdatesStream
|
|
||||||
.map("map-tile-updates-to-MapChunkData") { _: TileUpdateRecordKey, value: TileUpdateRecord ->
|
|
||||||
val key = MapChunkDataPosition(
|
|
||||||
value.tilePosition.toMapChunkPosition(),
|
|
||||||
value.surfaceIndex,
|
|
||||||
)
|
|
||||||
key to MapChunkData(
|
|
||||||
key,
|
|
||||||
setOf(value.tile),
|
|
||||||
setOfNotNull(value.tilePrototype)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// group all tiles by chunk position
|
|
||||||
.groupByKey(
|
|
||||||
groupedAs("group-tile-updates-per-chunk-position", jsonMapper.serde(), jsonMapper.serde())
|
|
||||||
)
|
|
||||||
.reduce { tileA, tileB ->
|
|
||||||
tileA.copy(
|
|
||||||
tiles = tileA.tiles + tileB.tiles,
|
|
||||||
tilePrototypes = tileA.tilePrototypes + tileB.tilePrototypes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.suppress(
|
|
||||||
Suppressed.untilTimeLimit(
|
|
||||||
Duration.ofSeconds(30),
|
|
||||||
Suppressed.BufferConfig.maxRecords(32*32*2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
chunksTable
|
|
||||||
.toStream()
|
.toStream()
|
||||||
.foreach { key: MapChunkDataPosition, value: MapChunkData ->
|
// .repartition(
|
||||||
|
// repartitionedAs(
|
||||||
|
// "repartition-all-tiles-before-chunk-grouping",
|
||||||
|
// jsonMapper.serde(),
|
||||||
|
// jsonMapper.serde()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
.groupBy<TileUpdateRecordKey, TileUpdateRecord, MapChunkDataPosition>(
|
||||||
|
groupedAs("grouping-tiles-by-chunk-pos", jsonMapper.serde(), jsonMapper.serde()),
|
||||||
|
) { key: TileUpdateRecordKey, value: TileUpdateRecord ->
|
||||||
|
MapChunkDataPosition(
|
||||||
|
value.tilePosition.toMapChunkPosition(),
|
||||||
|
key.surfaceIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.aggregate<MapChunkDataPosition, TileUpdateRecord, MapChunkDataAggregator>(
|
||||||
|
"reducing-tiles-grouped-by-chunk-position",
|
||||||
|
materializedWith(jsonMapper.serde(), jsonMapper.serde()),
|
||||||
|
{ MapChunkDataAggregator() },
|
||||||
|
) { _: MapChunkDataPosition, value: TileUpdateRecord, aggregate: MapChunkDataAggregator ->
|
||||||
|
aggregate.tiles.add(value.tile)
|
||||||
|
aggregate
|
||||||
|
}
|
||||||
|
// TODO maybe set serdes here?
|
||||||
|
.mapValues(
|
||||||
|
"tiles-grouped-by-chunk-position-finalise-aggregation",
|
||||||
|
materializedWith(jsonMapper.serde(), jsonMapper.serde()),
|
||||||
|
) { k, v ->
|
||||||
|
MapChunkData(k, v.tiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createChunkTileImage(
|
||||||
|
tilePrototypesTable: KTable<SurfaceIndex, SurfacePrototypesData>,
|
||||||
|
tilesGroupedByChunk: KTable<MapChunkDataPosition, MapChunkData>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChunkTilesAndProtos(
|
||||||
|
val chunkTiles: MapChunkData,
|
||||||
|
val protos: SurfacePrototypesData
|
||||||
|
)
|
||||||
|
|
||||||
|
// tilesGroupedByChunk
|
||||||
|
// .join<ChunkTilesAndProtos, SurfaceIndex, SurfacePrototypesData>(
|
||||||
|
// tilePrototypesTable,
|
||||||
|
// { cd: MapChunkData -> cd.chunkPosition.surfaceIndex },
|
||||||
|
// { chunkTiles: MapChunkData, protos: SurfacePrototypesData ->
|
||||||
|
// ChunkTilesAndProtos(chunkTiles, protos)
|
||||||
|
// },
|
||||||
|
// namedAs("joining-chunks-tiles-prototypes"),
|
||||||
|
// materializedAs(
|
||||||
|
// "joined-chunked-tiles-with-prototypes",
|
||||||
|
// jsonMapper.serde(),
|
||||||
|
// jsonMapper.serde()
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
tilesGroupedByChunk
|
||||||
|
.join<MapChunkDataPosition, MapChunkData, SurfaceIndex, SurfacePrototypesData, ChunkTilesAndProtos>(
|
||||||
|
tilePrototypesTable,
|
||||||
|
"enrich-tiles-with-prototype-colour",
|
||||||
|
materializedAs(
|
||||||
|
"joined-chunked-tiles-with-prototypes",
|
||||||
|
jsonMapper.serde(),
|
||||||
|
jsonMapper.serde()
|
||||||
|
),
|
||||||
|
{ it.chunkPosition.surfaceIndex },
|
||||||
|
) { chunkTiles: MapChunkData, protos: SurfacePrototypesData ->
|
||||||
|
ChunkTilesAndProtos(chunkTiles, protos)
|
||||||
|
}
|
||||||
|
.suppress(
|
||||||
|
Suppressed.untilTimeLimit(
|
||||||
|
Duration.ofSeconds(30),
|
||||||
|
Suppressed.BufferConfig.maxRecords(30)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toStream()
|
||||||
|
.foreach { _, (chunkTiles: MapChunkData, protos: SurfacePrototypesData) ->
|
||||||
runCatching {
|
runCatching {
|
||||||
saveMapTilesPng(key, value)
|
saveMapTilesPng(chunkTiles, protos)
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
println("error saving map tile png chunk:${value.chunkPosition}")
|
println("error saving map tile png chunk:${chunkTiles.chunkPosition}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
@ -328,14 +400,16 @@ class KafkatorioTopology(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun saveMapTilesPng(key: MapChunkDataPosition, chunk: MapChunkData) {
|
private fun saveMapTilesPng(chunkTiles: MapChunkData, protos: SurfacePrototypesData) {
|
||||||
|
|
||||||
|
val chunkPos = chunkTiles.chunkPosition
|
||||||
|
|
||||||
val tilePrototypes: Map<String, MapTilePrototype?> =
|
val tilePrototypes: Map<String, MapTilePrototype?> =
|
||||||
chunk.tilePrototypes.associateBy { it.name }.withDefault { null }
|
protos.mapTilePrototypes.associateBy { it.name }.withDefault { null }
|
||||||
|
|
||||||
val chunkOriginX = chunk.chunkPosition.position.leftTopTile.x
|
val chunkOriginX = chunkTiles.chunkPosition.position.leftTopTile.x
|
||||||
// val chunkOriginY = chunk.chunkPosition.position.rightBottomTile.y
|
// val chunkOriginY = chunk.chunkPosition.position.rightBottomTile.y
|
||||||
val chunkOriginY = chunk.chunkPosition.position.leftTopTile.y
|
val chunkOriginY = chunkTiles.chunkPosition.position.leftTopTile.y
|
||||||
|
|
||||||
val chunkImage =
|
val chunkImage =
|
||||||
ImmutableImage.filled(
|
ImmutableImage.filled(
|
||||||
|
|
@ -345,7 +419,7 @@ class KafkatorioTopology(
|
||||||
BufferedImage.TYPE_INT_ARGB
|
BufferedImage.TYPE_INT_ARGB
|
||||||
)
|
)
|
||||||
|
|
||||||
chunk.tiles.forEach { tile ->
|
chunkTiles.tiles.forEach { tile ->
|
||||||
|
|
||||||
val prototypeColour = tilePrototypes
|
val prototypeColour = tilePrototypes
|
||||||
.getValue(tile.prototypeName)
|
.getValue(tile.prototypeName)
|
||||||
|
|
@ -378,7 +452,9 @@ class KafkatorioTopology(
|
||||||
|
|
||||||
val zoom = 1u
|
val zoom = 1u
|
||||||
val file =
|
val file =
|
||||||
File("src/main/resources/kafkatorio-web-map/s${key.surfaceIndex}/z$zoom/x${key.position.x}/y${key.position.y}.png")
|
File(
|
||||||
|
"src/main/resources/kafkatorio-web-map/s${chunkPos.surfaceIndex}/z$zoom/x${chunkOriginX}/y${chunkOriginY}.png"
|
||||||
|
)
|
||||||
|
|
||||||
if (file.parentFile.mkdirs()) {
|
if (file.parentFile.mkdirs()) {
|
||||||
println("created new map tile parentFile directory ${file.absolutePath}")
|
println("created new map tile parentFile directory ${file.absolutePath}")
|
||||||
|
|
@ -389,100 +465,4 @@ class KafkatorioTopology(
|
||||||
chunkImage.output(PngWriter.MaxCompression, file)
|
chunkImage.output(PngWriter.MaxCompression, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// private fun createMapTileImages() {
|
|
||||||
//
|
|
||||||
// val mapTilesByPrototypeName: KStream<String, MapTile> =
|
|
||||||
// builder.stream(
|
|
||||||
// "kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
|
|
||||||
// Consumed.with(Serdes.String(), KafkatorioPacketSerde)
|
|
||||||
// )
|
|
||||||
// .filter { _, packet -> packet is FactorioEvent && packet.data is MapTiles }
|
|
||||||
// .mapValues { packet -> ((packet as? FactorioEvent)?.data as? MapTiles)!! }
|
|
||||||
// .flatMap { _, mapTiles ->
|
|
||||||
// mapTiles
|
|
||||||
// .tiles
|
|
||||||
// .map { tile -> tile.prototypeName to tile }
|
|
||||||
// .map { it.toKeyValue() }
|
|
||||||
// }
|
|
||||||
//// .groupByKey()
|
|
||||||
//
|
|
||||||
// @Serializable
|
|
||||||
// data class TileSurfaceCoord(
|
|
||||||
// val surfaceIndex: Int,
|
|
||||||
// val position: MapTilePosition,
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// @Serializable
|
|
||||||
// data class TileSurfaceState(
|
|
||||||
// val surfaceIndex: Int,
|
|
||||||
// val position: MapTilePosition,
|
|
||||||
// val prototypeName: String,
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// val mapTilesTable: KTable<TileSurfaceCoord, TileSurfaceState> =
|
|
||||||
// builder.stream(
|
|
||||||
// "kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
|
|
||||||
// Consumed.with(Serdes.String(), KafkatorioPacketSerde)
|
|
||||||
// )
|
|
||||||
// .filter { _, packet -> packet is FactorioEvent && packet.data is MapTiles }
|
|
||||||
// .mapValues { packet -> ((packet as? FactorioEvent)?.data as? MapTiles)!! }
|
|
||||||
// .flatMap({ _, mapTiles: MapTiles ->
|
|
||||||
// mapTiles.tiles.map { tile: MapTile ->
|
|
||||||
// val key = TileSurfaceCoord(mapTiles.surfaceIndex, tile.position)
|
|
||||||
// val value = TileSurfaceState(mapTiles.surfaceIndex, tile.position, tile.prototypeName)
|
|
||||||
// (key to value).toKeyValue()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// .toTable(
|
|
||||||
// materializedAs(
|
|
||||||
// "kafkatorio.table.map-tile-status",
|
|
||||||
// jsonMapper.serde(),
|
|
||||||
// jsonMapper.serde(),
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// val tilePrototypesTable: KTable<String, MapTilePrototype> =
|
|
||||||
// builder.stream(
|
|
||||||
// "kafkatorio.${KafkatorioPacket.PacketType.CONFIG}.FactorioConfigurationUpdate",
|
|
||||||
// Consumed.with(Serdes.String(), KafkatorioPacketSerde)
|
|
||||||
// )
|
|
||||||
// .flatMap(KeyValueMapper<String, KafkatorioPacket, List<KeyValue<String, MapTilePrototype>>> { _, packet ->
|
|
||||||
// when (packet) {
|
|
||||||
// is FactorioConfigurationUpdate ->
|
|
||||||
// packet.prototypes
|
|
||||||
// .filterIsInstance<MapTilePrototype>()
|
|
||||||
// .map { proto -> proto.name to proto }
|
|
||||||
// .map { it.toKeyValue() }
|
|
||||||
// is FactorioEvent ->
|
|
||||||
// emptyList()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .toTable(
|
|
||||||
// materializedAs("kafkatorio.table.tile-prototypes", Serdes.String(), jsonMapper.serde())
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// @Serializable
|
|
||||||
// data class TileSurfaceData(
|
|
||||||
// val surfaceIndex: Int,
|
|
||||||
// val position: MapTilePosition,
|
|
||||||
// val prototype: MapTilePrototype,
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// val enrichedTiles: KTable<TileSurfaceCoord, TileSurfaceData> =
|
|
||||||
// mapTilesTable.join<TileSurfaceData, String, MapTilePrototype>(
|
|
||||||
// tilePrototypesTable,
|
|
||||||
// { tile: TileSurfaceState ->
|
|
||||||
// tile.prototypeName
|
|
||||||
// },
|
|
||||||
// { tile: TileSurfaceState, proto: MapTilePrototype ->
|
|
||||||
// TileSurfaceData(tile.surfaceIndex, tile.position, proto)
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package dev.adamko.kafkatorio.processor
|
||||||
|
|
||||||
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
||||||
import dev.adamko.kafkatorio.processor.tileserver.WebMapTileServer
|
import dev.adamko.kafkatorio.processor.tileserver.WebMapTileServer
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.http4k.server.PolyHandler
|
import org.http4k.server.PolyHandler
|
||||||
import org.http4k.server.Undertow
|
import org.http4k.server.Undertow
|
||||||
|
|
@ -10,7 +11,7 @@ import org.http4k.server.asServer
|
||||||
|
|
||||||
val appProps = ApplicationProperties()
|
val appProps = ApplicationProperties()
|
||||||
|
|
||||||
fun main() = runBlocking {
|
fun main() = runBlocking<Unit> {
|
||||||
|
|
||||||
val wsServer = WebsocketServer()
|
val wsServer = WebsocketServer()
|
||||||
val tileServer = WebMapTileServer()
|
val tileServer = WebMapTileServer()
|
||||||
|
|
@ -18,11 +19,16 @@ fun main() = runBlocking {
|
||||||
val topology = KafkatorioTopology(wsServer)
|
val topology = KafkatorioTopology(wsServer)
|
||||||
topology.build()
|
topology.build()
|
||||||
|
|
||||||
PolyHandler(
|
val webServer = PolyHandler(
|
||||||
tileServer.build(),
|
tileServer.build(),
|
||||||
wsServer.build(),
|
wsServer.build(),
|
||||||
).asServer(Undertow(9073))
|
).asServer(Undertow(9073))
|
||||||
.start()
|
|
||||||
.block()
|
// Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
|
// webServer.stop()
|
||||||
|
// })
|
||||||
|
// launch {
|
||||||
|
webServer.start().block()
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,16 @@ fun createFactorioMap() = Maps {
|
||||||
) {
|
) {
|
||||||
attribution = "kafkatorio"
|
attribution = "kafkatorio"
|
||||||
tms = true
|
tms = true
|
||||||
minZoom = 1
|
minZoom = -2
|
||||||
// maxZoom = 1
|
maxZoom = 1
|
||||||
maxNativeZoom = 1
|
maxNativeZoom = 1
|
||||||
|
minNativeZoom = 1
|
||||||
}.addTo(this)
|
}.addTo(this)
|
||||||
|
|
||||||
|
L.scale {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
with(options) {
|
with(options) {
|
||||||
crs = CRS.Simple
|
crs = CRS.Simple
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue