- wicked sick size optimisation of map-tiles update packet

- fix ts-generator recursive value class unwrapping
- move kafka state dir
- try fixing kafka streams for tile generation (simplifying it, and fixing some nullability problems)
This commit is contained in:
Adam 2022-03-02 18:38:51 +01:00
parent c5d4ae8d63
commit b055397d25
14 changed files with 253 additions and 118 deletions

View file

@ -15,7 +15,7 @@ interface FactorioPrototypes extends KafkatorioPacket {
interface MapTilePrototype extends FactorioPrototype { interface MapTilePrototype extends FactorioPrototype {
canBeMined: boolean; canBeMined: boolean;
collisionMasks: string[]; collisionMasks: string[];
layer: int; layer: uint;
mapColour: Colour; mapColour: Colour;
name: string; name: string;
order: string; order: string;

View file

@ -59,3 +59,8 @@ interface EntityIdentifiers {
type: string; type: string;
unitNumber: uint | null; unitNumber: uint | null;
} }
interface MapTileDictionary {
protos: { [key: string]: string };
tilesXY: { [key: string]: { [key: string]: string } };
}

View file

@ -7,7 +7,7 @@ interface FactorioEventUpdateKey {
} }
interface FactorioEventUpdate extends FactorioEventUpdateKey { interface FactorioEventUpdate extends FactorioEventUpdateKey {
eventCounts: { [key: string]: int } | null; eventCounts: { [key: string]: uint } | null;
updateType: FactorioEventUpdateType; updateType: FactorioEventUpdateType;
} }
@ -30,7 +30,7 @@ interface PlayerUpdate extends FactorioEventUpdate, PlayerUpdateKey {
colour: Colour | null; colour: Colour | null;
diedCause: EntityIdentifiers | null; diedCause: EntityIdentifiers | null;
disconnectReason: string | null; disconnectReason: string | null;
eventCounts: { [key: string]: int } | null; eventCounts: { [key: string]: uint } | null;
forceIndex: uint | null; forceIndex: uint | null;
index: uint; index: uint;
isAdmin: boolean | null; isAdmin: boolean | null;
@ -57,12 +57,12 @@ interface EntityUpdateKey extends EntityIdentifiers, FactorioEventUpdateKey {
interface EntityUpdate extends FactorioEventUpdate, EntityIdentifiers, EntityUpdateKey { interface EntityUpdate extends FactorioEventUpdate, EntityIdentifiers, EntityUpdateKey {
chunkPosition: MapChunkPosition | null; chunkPosition: MapChunkPosition | null;
eventCounts: { [key: string]: int } | null; eventCounts: { [key: string]: uint } | null;
graphicsVariation: number | null; graphicsVariation: uint8 | null;
health: float | null; health: float | null;
isActive: boolean | null; isActive: boolean | null;
isRotatable: boolean | null; isRotatable: boolean | null;
lastUser: int | null; lastUser: uint | null;
localisedDescription: string | null; localisedDescription: string | null;
localisedName: string | null; localisedName: string | null;
name: string; name: string;
@ -79,12 +79,12 @@ interface MapChunkUpdateKey extends FactorioEventUpdateKey {
interface MapChunkUpdate extends FactorioEventUpdate, MapChunkUpdateKey { interface MapChunkUpdate extends FactorioEventUpdate, MapChunkUpdateKey {
chunkPosition: MapChunkPosition; chunkPosition: MapChunkPosition;
eventCounts: { [key: string]: int } | null; eventCounts: { [key: string]: uint } | null;
force: uint | null; force: uint | null;
isDeleted: boolean | null; isDeleted: boolean | null;
player: uint | null; player: uint | null;
robot: EntityIdentifiers | null; robot: EntityIdentifiers | null;
surfaceIndex: uint; surfaceIndex: uint;
tiles: MapTile[] | null; tileDictionary: MapTileDictionary | null;
updateType: FactorioEventUpdateType; updateType: FactorioEventUpdateType;
} }

View file

@ -20,7 +20,7 @@ export namespace EventDataCache {
log("Initialising global.DEFAULT_CACHE_DURATION_TICKS") log("Initialising global.DEFAULT_CACHE_DURATION_TICKS")
global.defaultCacheDurationTicks = { global.defaultCacheDurationTicks = {
"PLAYER": 60, // 1 second "PLAYER": 15, // 0.25 seconds
"MAP_CHUNK": 60 * 30, // 30 seconds "MAP_CHUNK": 60 * 30, // 30 seconds
"ENTITY": 60 * 2, // 2 seconds "ENTITY": 60 * 2, // 2 seconds
"default": 60 * 5, // 5 seconds "default": 60 * 5, // 5 seconds

View file

@ -8,6 +8,17 @@ import CacheData = EventDataCache.CacheData;
type MapChunkUpdater = (data: CacheData<"MAP_CHUNK">) => void type MapChunkUpdater = (data: CacheData<"MAP_CHUNK">) => void
const mapProtoNameToKey: { [key: string]: string } = {}
let protoIndex: uint = 0
function getProtoKey(protoName: string): string {
if (mapProtoNameToKey[protoName] == undefined) {
mapProtoNameToKey[protoName] = `${protoIndex++}`
}
return mapProtoNameToKey[protoName]
}
function mapTilesUpdateDebounce( function mapTilesUpdateDebounce(
surface: LuaSurface | undefined, surface: LuaSurface | undefined,
chunkPosition: ChunkPositionTable, chunkPosition: ChunkPositionTable,
@ -27,24 +38,26 @@ function mapTilesUpdateDebounce(
updateType: "MAP_CHUNK", updateType: "MAP_CHUNK",
}, },
data => { data => {
if (data.tiles == undefined) { data.tileDictionary ??= <MapTileDictionary>{
data.tiles = [] tilesXY: {},
protos: {},
} }
for (const tile of tiles) { for (let tile of tiles) {
data.tiles.push(
{ const xString = `${tile.position.x}`
x: tile.position.x, const yString = `${tile.position.y}`
y: tile.position.y, const protoKey = getProtoKey(tile.name)
proto: tile.name,
} data.tileDictionary.protos[protoKey] = tile.name
)
data.tileDictionary.tilesXY[xString] ??= {}
data.tileDictionary.tilesXY[xString][yString] = protoKey
} }
if (data.eventCounts == undefined) { data.eventCounts ??= {}
data.eventCounts = {} data.eventCounts[eventName] ??= 0
} data.eventCounts[eventName]++
data.eventCounts[eventName] = ((data.eventCounts ?? {}) [eventName] ?? 0) + 1
if (updater != undefined) { if (updater != undefined) {
updater(data) updater(data)

View file

@ -1,7 +1,6 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -101,3 +100,41 @@ interface EntityIdentifiers {
val name: String val name: String
val type: String val type: String
} }
/**
* Size-optimised 2D map of x/y tile positions and prototype-name.
*
* Each X/Y coordinate maps to an arbitrary [PrototypeKey] (which is only valid for any specific
* instance). [protos] is specific to each instance of [MapTileDictionary], can be used to convert from a
*/
@Serializable
data class MapTileDictionary(
/** Map an X,Y coordinate a prototype name */
val tilesXY: Map<String, Map<String, PrototypeKey>>,
val protos: Map<PrototypeKey, PrototypeName>
) {
fun toMapTileList(): List<MapTile> = buildList {
tilesXY.forEach { (xString, row) ->
row.forEach { (yString, protoIndex) ->
val x = xString.toIntOrNull()
val y = yString.toIntOrNull()
val protoName = protos[protoIndex]
if (x != null && y != null && protoName != null) {
add(MapTile(x, y, protoName))
}
}
}
}
companion object {
fun MapTileDictionary?.isNullOrEmpty(): Boolean {
return this == null || (tilesXY.isEmpty() && tilesXY.all { it.value.isEmpty() })
}
}
}
@Serializable
@JvmInline
value class PrototypeKey(val index: String)

View file

@ -147,7 +147,7 @@ data class EntityUpdate(
override val eventCounts: Map<String, UInt>? = null, override val eventCounts: Map<String, UInt>? = null,
val chunkPosition: MapChunkPosition? = null, val chunkPosition: MapChunkPosition? = null,
val graphicsVariation: UShort? = null, val graphicsVariation: UByte? = null,
val health: Float? = null, val health: Float? = null,
val isActive: Boolean? = null, val isActive: Boolean? = null,
val isRotatable: Boolean? = null, val isRotatable: Boolean? = null,
@ -180,11 +180,8 @@ data class MapChunkUpdate(
val player: PlayerIndex? = null, val player: PlayerIndex? = null,
val robot: EntityIdentifiers? = null, val robot: EntityIdentifiers? = null,
val force: ForceIndex? = null, val force: ForceIndex? = null,
/** updated tiles - might be partial */ /** updated tiles - might be partial and not all tiles in the chunk */
// note: this must be a List, not a Map<Location, ProtoName> val tileDictionary: MapTileDictionary? = null,
// because JSON can't have non-string keys.
@Contextual
val tiles: List<MapTile>? = null,
val isDeleted: Boolean? = null, val isDeleted: Boolean? = null,
) : FactorioEventUpdate(), MapChunkUpdateKey { ) : FactorioEventUpdate(), MapChunkUpdateKey {
@EncodeDefault @EncodeDefault

View file

@ -1,11 +1,15 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.full.withNullability import kotlin.reflect.full.withNullability
import me.ntrrgc.tsGenerator.ClassTransformer import me.ntrrgc.tsGenerator.ClassTransformer
@ -14,54 +18,77 @@ import me.ntrrgc.tsGenerator.ClassTransformer
*/ */
object ValueClassTransformer : ClassTransformer { object ValueClassTransformer : ClassTransformer {
private val mappedValueClasses: MutableMap<KType, KType> = mutableMapOf(
// don't unwrap unsigned numbers - they're mapped to Factorio types
UInt::class.starProjectedType to UInt::class.starProjectedType,
UByte::class.starProjectedType to UByte::class.starProjectedType,
UShort::class.starProjectedType to UShort::class.starProjectedType,
ULong::class.starProjectedType to ULong::class.starProjectedType,
)
override fun transformPropertyType(type: KType, property: KProperty<*>, klass: KClass<*>): KType { override fun transformPropertyType(type: KType, property: KProperty<*>, klass: KClass<*>): KType {
val unwrappedType =
val classifier = type.classifier ?: return super.transformPropertyType(type, property, klass) unwrapValueClassType(type, property, klass)
?: unwrapArgs(type, property, klass)
val valueClassType = unwrapValueClassType(type) return when (unwrappedType) {
null -> super.transformPropertyType(type, property, klass)
if (valueClassType != null) { else -> super.transformPropertyType(unwrappedType, property, klass)
println("$klass has a value class $type -> $valueClassType")
return valueClassType
} }
}
val hasValueClassTypeParameters = type.arguments private fun unwrapArgs(type: KType, property: KProperty<*>, klass: KClass<*>): KType? {
.mapNotNull { it.type?.classifier }
.filterIsInstance<KClass<*>>()
.any { it.isValue }
if (hasValueClassTypeParameters) { val classifier: KClassifier = type.classifier ?: return type
val args = type.arguments.map { arg -> return if (type.arguments.isNotEmpty()) {
when (val vct = unwrapValueClassType(arg.type)) { println("unwrapping args for $type - ${type.arguments}")
null -> arg
else -> KTypeProjection(arg.variance, vct.withNullability(arg.type!!.isMarkedNullable)) val args = type.arguments.map { arg: KTypeProjection ->
} // recursively map each argument
val argType = arg.type!!
val converted = transformPropertyType(argType, property, klass)
KTypeProjection(arg.variance, converted.withNullability(argType.isMarkedNullable))
} }
println("$klass has a prop with value class type parameters ${type.arguments} -> $args") classifier.createType(
return classifier.createType(
arguments = args, arguments = args,
nullable = type.isMarkedNullable, nullable = type.isMarkedNullable,
annotations = type.annotations, annotations = type.annotations,
) )
} } else {
null
return super.transformPropertyType(type, property, klass)
}
private fun unwrapValueClassType(type: KType?): KType? {
val klass = type?.classifier as? KClass<*> ?: return null
return when {
klass.isValue -> klass
.declaredMemberProperties
.first()
.returnType
.withNullability(type.isMarkedNullable)
else -> null
} }
} }
private fun unwrapValueClassType(type: KType, property: KProperty<*>, klass: KClass<*>): KType? {
val valueKlass: KClass<*> = type.classifier as? KClass<*> ?: return null
return if (isValueClass(type)) {
val mappedType = mappedValueClasses.getOrPut(type.withNullability(false)) {
val result = valueKlass
.declaredMemberProperties
.first()
.returnType
.withNullability(type.isMarkedNullable)
println("unwrapping value class $type -> $result")
// recursively unwrap...
val transformed = transformPropertyType(result, property, klass)
transformed.withNullability(false)
}
mappedType.withNullability(type.isMarkedNullable)
} else {
null
}
}
@OptIn(ExperimentalContracts::class)
private fun isValueClass(type: KType?): Boolean {
contract { returnsNotNull() implies (type != null) }
return (type?.classifier as? KClass<*>)?.isValue == true
}
} }

View file

@ -21,12 +21,9 @@ fun main(args: Array<String>) {
add(FactorioPrototypes::class) add(FactorioPrototypes::class)
add(FactorioConfigurationUpdate::class) add(FactorioConfigurationUpdate::class)
add(FactorioEventUpdatePacket::class) add(FactorioEventUpdatePacket::class)
addAll(FactorioEventUpdate::class.sealedSubclasses) addAll(FactorioEventUpdate::class.sealedSubclasses)
addAll(FactorioEventUpdateKey::class.sealedSubclasses) addAll(FactorioEventUpdateKey::class.sealedSubclasses)
// addAll(FactorioEventUpdateData::class.sealedSubclasses)
}, },
mappings = mapOf( mappings = mapOf(
// builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts` // builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts`
@ -74,7 +71,6 @@ private fun splitDefinitions(definitions: String): Map<String, String> {
.split("\n\n") .split("\n\n")
.groupBy { def -> .groupBy { def ->
when { when {
"interface FactorioObjectData " in def || "interface FactorioObjectData " in def ||

1
modules/events-server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
kafka-state/

View file

@ -66,7 +66,7 @@ tasks.withType<KotlinCompile> {
val kafkaStateDirDelete by tasks.registering(Delete::class) { val kafkaStateDirDelete by tasks.registering(Delete::class) {
group = project.name group = project.name
mustRunAfter(kafkaConsumersDelete) mustRunAfter(kafkaConsumersDelete)
delete(layout.buildDirectory.dir("kafka-state")) delete(layout.projectDirectory.dir("kafka-state"))
} }
val kafkaConsumersDelete by tasks.registering(KafkaConsumerGroupsTask.DeleteAll::class) { val kafkaConsumersDelete by tasks.registering(KafkaConsumerGroupsTask.DeleteAll::class) {

View file

@ -2,6 +2,8 @@ package dev.adamko.kafkatorio.processor
import dev.adamko.kafkatorio.events.schema.ColourHex import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.FactorioEventUpdate
import dev.adamko.kafkatorio.events.schema.FactorioEventUpdatePacket
import dev.adamko.kafkatorio.events.schema.FactorioObjectData import dev.adamko.kafkatorio.events.schema.FactorioObjectData
import dev.adamko.kafkatorio.events.schema.FactorioPrototypes import dev.adamko.kafkatorio.events.schema.FactorioPrototypes
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
@ -33,6 +35,7 @@ import org.apache.kafka.streams.KafkaStreams
import org.apache.kafka.streams.StreamsBuilder import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.StreamsConfig import org.apache.kafka.streams.StreamsConfig
import org.apache.kafka.streams.Topology import org.apache.kafka.streams.Topology
import org.apache.kafka.streams.TopologyDescription
import org.apache.kafka.streams.kstream.KStream import org.apache.kafka.streams.kstream.KStream
import org.apache.kafka.streams.kstream.KTable import org.apache.kafka.streams.kstream.KTable
@ -69,20 +72,29 @@ internal class KafkatorioTopology(
val tileProtoColourDict = tileProtoColourDictionary(protosStream) val tileProtoColourDict = tileProtoColourDictionary(protosStream)
val mapTilesStream: KStream<FactorioServerId, FactorioEvent> = // val mapTilesStream: KStream<FactorioServerId, FactorioEvent> =
// builder.stream(
// "kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}",
// consumedAs("consume.map-tiles", jsonMapper.serde(), jsonMapper.serde())
// )
//
// val mapChunksStream: KStream<FactorioServerId, FactorioEvent> =
// builder.stream(
// "kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
// consumedAs("consume.map-chunks", jsonMapper.serde(), jsonMapper.serde())
// )
val mapChunksUpdatesStream: KStream<FactorioServerId, FactorioEventUpdatePacket> =
builder.stream( builder.stream(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaTiles}", "kafkatorio.${KafkatorioPacket.PacketType.UPDATE}.${FactorioEventUpdate.FactorioEventUpdateType.MAP_CHUNK}",
consumedAs("consume.map-tiles", jsonMapper.serde(), jsonMapper.serde()) consumedAs("consume.map-chunk-updates", jsonMapper.serde(), jsonMapper.serde())
)
val mapChunksStream: KStream<FactorioServerId, FactorioEvent> =
builder.stream(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
consumedAs("consume.map-chunks", jsonMapper.serde(), jsonMapper.serde())
) )
val groupedMapChunkTiles: KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> = val groupedMapChunkTiles: KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> =
groupTilesIntoChunksWithColours( groupTilesIntoChunksWithColours(
mapTilesStream, // mapTilesStream,
mapChunksStream, // mapChunksStream,
mapChunksUpdatesStream,
tileProtoColourDict, tileProtoColourDict,
) )
@ -90,7 +102,11 @@ internal class KafkatorioTopology(
.toStream("stream-grouped-map-tiles") .toStream("stream-grouped-map-tiles")
.to( .to(
TOPIC_GROUPED_MAP_CHUNKS, TOPIC_GROUPED_MAP_CHUNKS,
producedAs("produce.grouped-map-chunks", kxsBinary.serde(), kxsBinary.serde()) producedAs(
"produce.grouped-map-chunks",
kxsBinary.serde<ServerMapChunkId?>(),
kxsBinary.serde<ServerMapChunkTiles<ColourHex>?>()
)
) )
@ -140,10 +156,19 @@ internal class KafkatorioTopology(
val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as? String val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as? String
props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId) props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId)
val streams = KafkaStreams(topology, props) val streams = KafkaStreams(topology, props)
streams.setUncaughtExceptionHandler(StreamsExceptionHandler()) streams.setUncaughtExceptionHandler(StreamsExceptionHandler())
val description: TopologyDescription = topology.describe()
println(
"""
|----------------
|$appId
|$description
|----------------
""".trimMargin()
)
coroutineContext.job.invokeOnCompletion { coroutineContext.job.invokeOnCompletion {
println("closing $id KafkaStreams") println("closing $id KafkaStreams")
streams.close(Duration.ofSeconds(1)) streams.close(Duration.ofSeconds(1))

View file

@ -1,10 +1,11 @@
package dev.adamko.kafkatorio.processor.topology package dev.adamko.kafkatorio.processor.topology
import dev.adamko.kafkatorio.events.schema.ColourHex import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEventUpdatePacket
import dev.adamko.kafkatorio.events.schema.MapChunk
import dev.adamko.kafkatorio.events.schema.MapChunkPosition import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapChunkUpdate
import dev.adamko.kafkatorio.events.schema.MapTile import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTileDictionary.Companion.isNullOrEmpty
import dev.adamko.kafkatorio.events.schema.MapTilePosition import dev.adamko.kafkatorio.events.schema.MapTilePosition
import dev.adamko.kafkatorio.events.schema.MapTiles import dev.adamko.kafkatorio.events.schema.MapTiles
import dev.adamko.kafkatorio.events.schema.SurfaceIndex import dev.adamko.kafkatorio.events.schema.SurfaceIndex
@ -13,10 +14,8 @@ import dev.adamko.kafkatorio.processor.serdes.kxsBinary
import dev.adamko.kotka.extensions.groupedAs 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.streams.filter
import dev.adamko.kotka.extensions.streams.flatMap import dev.adamko.kotka.extensions.streams.flatMap
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.reduce import dev.adamko.kotka.extensions.streams.reduce
import dev.adamko.kotka.extensions.tables.join import dev.adamko.kotka.extensions.tables.join
import dev.adamko.kotka.kxs.serde import dev.adamko.kotka.kxs.serde
@ -44,39 +43,70 @@ data class ServerMapChunkTiles<Data>(
fun groupTilesIntoChunksWithColours( fun groupTilesIntoChunksWithColours(
mapTilePacketsStream: KStream<FactorioServerId, FactorioEvent>, // mapTilePacketsStream: KStream<FactorioServerId, FactorioEvent>,
mapChunksPacketsStream: KStream<FactorioServerId, FactorioEvent>, // mapChunksPacketsStream: KStream<FactorioServerId, FactorioEvent>,
mapChunkUpdatePacketsStream: KStream<FactorioServerId, FactorioEventUpdatePacket>,
tileProtoColourDict: KTable<FactorioServerId, TileColourDict>, tileProtoColourDict: KTable<FactorioServerId, TileColourDict>,
): KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> { ): KTable<ServerMapChunkId, ServerMapChunkTiles<ColourHex>> {
val mapChunkTilesStream: KStream<FactorioServerId, MapTiles> = // val mapChunkTilesStream: KStream<FactorioServerId, MapTiles> =
mapChunksPacketsStream // mapChunksPacketsStream
.filter { _, packet: FactorioEvent? -> // .filter { _, packet: FactorioEvent? ->
packet != null // when (val data = packet?.data) {
&& packet.data is MapChunk // is MapChunk -> data.tiles.tiles.isNotEmpty()
&& !((packet.data as? MapChunk)?.tiles?.tiles.isNullOrEmpty()) // else -> false
// }
// }
// .mapValues("map-chunk-packets.convert-to-map-tiles") { _, packet: FactorioEvent ->
// (packet.data as MapChunk).tiles
// }
//
//
// val mapTilesStream: KStream<FactorioServerId, MapTiles> =
// mapTilePacketsStream
// .filter { _, packet: FactorioEvent? ->
// when (val data = packet?.data) {
// is MapTiles -> data.tiles.isNotEmpty()
// else -> false
// }
// }
// .mapValues("map-tiles-packets.convert-to-map-tiles") { _, packet: FactorioEvent ->
// packet.data as MapTiles
// }
val mapChunkUpdatesStream: KStream<FactorioServerId, MapTiles> =
mapChunkUpdatePacketsStream
.filter { _, packet: FactorioEventUpdatePacket? ->
when (val data = packet?.update) {
is MapChunkUpdate -> !data.tileDictionary.isNullOrEmpty()
else -> false
}
} }
.mapValues("map-chunk-packets.convert-to-map-tiles") { _, packet: FactorioEvent -> .mapValues("map-chunk-update-packets.convert-to-map-tiles") { _, packet: FactorioEventUpdatePacket ->
(packet.data as MapChunk).tiles
val mapChunkUpdate: MapChunkUpdate = requireNotNull(packet.update as? MapChunkUpdate) {
"invalid UpdatePacket type $packet"
}
val tiles: List<MapTile> = requireNotNull(mapChunkUpdate.tileDictionary?.toMapTileList()) {
"invalid MapChunkUpdate packet, tiles must not be null"
}
require(tiles.isNotEmpty()) {
"invalid MapChunkUpdate packet, tiles must not be empty"
}
MapTiles(mapChunkUpdate.surfaceIndex, tiles)
} }
val mapTilesStream: KStream<FactorioServerId, MapTiles> =
mapTilePacketsStream
.filter { _, packet: FactorioEvent? ->
packet != null
&& packet.data is MapTiles
&& !((packet.data as? MapTiles)?.tiles.isNullOrEmpty())
}
.mapValues("map-tiles-packets.convert-to-map-tiles") { _, packet: FactorioEvent ->
packet.data as MapTiles
}
// group tiles by server & surface & chunk // group tiles by server & surface & chunk
val chunkedTilesTable: KTable<ServerMapChunkId, ServerMapChunkTiles<TileProtoHashCode>> = val chunkedTilesTable: KTable<ServerMapChunkId, ServerMapChunkTiles<TileProtoHashCode>> =
mapTilesStream mapChunkUpdatesStream
.merge("merge-map-chunk-tiles-with-map-tiles", mapChunkTilesStream) // mapTilesStream
.filter { _, value -> value != null && value.tiles.isNotEmpty() } // .merge("merge-mapTilesStream-with-mapChunkTilesStream", mapChunkTilesStream)
// .merge("merge-mapTilesStream-with-mapChunkUpdatesStream", mapChunkUpdatesStream)
// .filter { _, value -> !value?.tiles.isNullOrEmpty() }
.flatMap("server-map-data.tiles.flatMapByChunk") { key: FactorioServerId, mapTiles: MapTiles -> .flatMap("server-map-data.tiles.flatMapByChunk") { key: FactorioServerId, mapTiles: MapTiles ->
val standardChunkSize = ChunkSize.MAX val standardChunkSize = ChunkSize.MAX
@ -102,11 +132,15 @@ fun groupTilesIntoChunksWithColours(
chunkId to chunkTiles chunkId to chunkTiles
} }
} }
.filter("server-map-data.tiles.filter-not-empty") { _, value -> // .filter("server-map-data.tiles.filter-not-empty") { _, value ->
value.map.isNotEmpty() // value.map.isNotEmpty()
} // }
.groupByKey( .groupByKey(
groupedAs("server-map-data.tiles.group", kxsBinary.serde(), kxsBinary.serde()) groupedAs(
"server-map-data.tiles.group",
kxsBinary.serde<ServerMapChunkId>(),
kxsBinary.serde<ServerMapChunkTiles<TileProtoHashCode>>()
)
) )
.reduce( .reduce(
"server-map-data.tiles.reduce", "server-map-data.tiles.reduce",

View file

@ -1,6 +1,6 @@
package dev.adamko.kafkatorio.processor.topology package dev.adamko.kafkatorio.processor.topology
import dev.adamko.kafkatorio.events.schema.FactorioObjectData import dev.adamko.kafkatorio.events.schema.FactorioEventUpdate
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.processor.WebsocketServer import dev.adamko.kafkatorio.processor.WebsocketServer
import dev.adamko.kafkatorio.processor.serdes.jsonMapper import dev.adamko.kafkatorio.processor.serdes.jsonMapper
@ -15,8 +15,8 @@ fun playerUpdatesToWsServer(
builder: StreamsBuilder = StreamsBuilder(), builder: StreamsBuilder = StreamsBuilder(),
) { ) {
builder.stream<FactorioServerId, KafkatorioPacket>( builder.stream<FactorioServerId, KafkatorioPacket>(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.LuaPlayer}", "kafkatorio.${KafkatorioPacket.PacketType.UPDATE}.${FactorioEventUpdate.FactorioEventUpdateType.PLAYER}",
consumedAs("stream-player-updates-for-ws-server", jsonMapper.serde(), jsonMapper.serde()) consumedAs("EventUpdate.Player-updates-for-ws-server", jsonMapper.serde(), jsonMapper.serde()),
) )
.foreach { _, value -> .foreach { _, value ->
// println("sending ${value.packetType} packet to websocket") // println("sending ${value.packetType} packet to websocket")