packet size improvements

- change structure resource packets
- make more booleans default to false
- use encode_string - massive decrease in packet size!!
This commit is contained in:
Adam 2022-07-17 23:54:53 +02:00
parent 523f6d5914
commit 085776d42b
14 changed files with 131 additions and 134 deletions

View file

@ -18,41 +18,23 @@ sealed interface FactorioEntityData {
/** Non-specific entity data */ /** Non-specific entity data */
@Serializable @Serializable
@SerialName("kafkatorio.entity.FactorioEntityData") @SerialName("kafkatorio.entity.FactorioEntityDataStandard")
data class Standard( data class Standard(
override val protoId: PrototypeId, override val protoId: PrototypeId,
val status: Status? = null,
val unitNumber: UnitNumber? = null,
override val position: MapEntityPosition, override val position: MapEntityPosition,
val graphicsVariation: UByte? = null, val graphicsVariation: UByte? = null,
val health: Float? = null, val health: Float? = null,
val isActive: Boolean? = null, val isActive: Boolean = false,
val isRotatable: Boolean? = null, val isRotatable: Boolean = false,
val lastUser: UInt? = null, val lastUser: UInt? = null,
) : FactorioEntityData { val status: Status? = null,
constructor( val unitNumber: UnitNumber? = null,
position: MapEntityPosition, ) : FactorioEntityData
protoId: PrototypeId,
element: FactorioEntityUpdateElement.Entity
) : this(
protoId = protoId,
position = position,
status = element.status,
unitNumber = element.unitNumber,
graphicsVariation = element.graphicsVariation,
health = element.health,
isActive = element.isActive,
isRotatable = element.isRotatable,
lastUser = element.lastUser
)
}
@Serializable @Serializable
@SerialName("kafkatorio.entity.FactorioResourceData") @SerialName("kafkatorio.entity.FactorioEntityDataResource")
data class Resource( data class Resource(
override val protoId: PrototypeId, override val protoId: PrototypeId,
@ -60,19 +42,7 @@ sealed interface FactorioEntityData {
val amount: UInt, val amount: UInt,
/** Count of initial resource units contained. `null` if resource is not infinite. */ /** Count of initial resource units contained. `null` if resource is not infinite. */
val initialAmount: UInt? = null, val initialAmount: UInt? = null,
) : FactorioEntityData { ) : FactorioEntityData
constructor(
position: MapEntityPosition,
protoId: PrototypeId,
element: FactorioEntityUpdateElement.Resource
) : this(
protoId = protoId,
position = position,
amount = element.amount,
initialAmount = element.initialAmount,
)
}
/** /**
@ -193,7 +163,7 @@ sealed interface FactorioEntityData {
} }
sealed interface FactorioEntityUpdateDictionary<T : FactorioEntityUpdateElement> sealed interface FactorioEntityUpdateDictionary<T : Any>
: Iterable<Pair<MapEntityPosition, T>> { : Iterable<Pair<MapEntityPosition, T>> {
val entitiesXY: Map<String, Map<String, T>> val entitiesXY: Map<String, Map<String, T>>
@ -203,28 +173,37 @@ sealed interface FactorioEntityUpdateDictionary<T : FactorioEntityUpdateElement>
override fun iterator(): Iterator<Pair<MapEntityPosition, T>> = override fun iterator(): Iterator<Pair<MapEntityPosition, T>> =
iterator { iterator {
entitiesXY.forEach { (x, column) -> val invalid = mutableListOf<String>()
column.forEach { (y, entity) -> entitiesXY.forEach { (xString, column) ->
yield( val x = xString.toDoubleOrNull()
MapEntityPosition(x.toDouble(), y.toDouble()) to entity column.forEach { (yString, entity) ->
) val y = yString.toDoubleOrNull()
if (x == null || y == null) {
invalid += "$xString/$yString"
} else {
yield(MapEntityPosition(x, y) to entity)
} }
} }
} }
if (invalid.isNotEmpty()) {
println("WARNING FactorioEntityUpdateDictionary contained invalid x/y coords: ${invalid.joinToString()}")
}
}
@Serializable @Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateResourceDictionary") @SerialName("kafkatorio.entity.FactorioEntityUpdateResourceAmountDictionary")
@JvmInline @JvmInline
value class Resource( value class ResourceAmounts(
override val entitiesXY: Map<String, Map<String, FactorioEntityUpdateElement.Resource>> override val entitiesXY: Map<String, Map<String, UInt>> = emptyMap()
) : FactorioEntityUpdateDictionary<FactorioEntityUpdateElement.Resource> ) : FactorioEntityUpdateDictionary<UInt>
@Serializable @Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateEntityDictionary") @SerialName("kafkatorio.entity.FactorioEntityUpdateEntityDictionary")
@JvmInline @JvmInline
value class Entity( value class Entity(
override val entitiesXY: Map<String, Map<String, FactorioEntityUpdateElement.Entity>> override val entitiesXY: Map<String, Map<String, FactorioEntityUpdateElement.Standard>>
) : FactorioEntityUpdateDictionary<FactorioEntityUpdateElement.Entity> ) : FactorioEntityUpdateDictionary<FactorioEntityUpdateElement.Standard>
} }
@ -232,24 +211,16 @@ sealed interface FactorioEntityUpdateDictionary<T : FactorioEntityUpdateElement>
sealed interface FactorioEntityUpdateElement { sealed interface FactorioEntityUpdateElement {
@Serializable @Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateResource") @SerialName("kafkatorio.entity.FactorioEntityUpdateElementStandard")
data class Resource( data class Standard(
val amount: UInt,
/** Count of initial resource units contained. `null` if resource is not infinite. */
val initialAmount: UInt? = null,
) : FactorioEntityUpdateElement
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateEntity")
data class Entity(
val status: FactorioEntityData.Status? = null, val status: FactorioEntityData.Status? = null,
val unitNumber: UnitNumber? = null, val unitNumber: UnitNumber? = null,
val graphicsVariation: UByte? = null, val graphicsVariation: UByte? = null,
val health: Float? = null, val health: Float? = null,
val isActive: Boolean? = null, val isActive: Boolean = false,
val isRotatable: Boolean? = null, val isRotatable: Boolean = false,
val lastUser: UInt? = null, val lastUser: UInt? = null,
) : FactorioEntityUpdateElement ) : FactorioEntityUpdateElement

View file

@ -24,7 +24,7 @@ sealed interface FactorioPrototype {
val collisionMasks: LuaJsonList<String>, val collisionMasks: LuaJsonList<String>,
val order: String, val order: String,
/** Can the tile be mined for resources? */ /** Can the tile be mined for resources? */
val canBeMined: Boolean, val canBeMined: Boolean = false,
) : FactorioPrototype ) : FactorioPrototype
@ -63,7 +63,7 @@ sealed interface FactorioPrototype {
@Serializable @Serializable
@SerialName("kafkatorio.prototype.EntityMiningProperties") @SerialName("kafkatorio.prototype.EntityMiningProperties")
data class MiningProperties( data class MiningProperties(
val canBeMined: Boolean, val canBeMined: Boolean = false,
// @Contextual // @Contextual
val products: LuaJsonList<MinedProduct>?, val products: LuaJsonList<MinedProduct>?,
) )

View file

@ -23,8 +23,8 @@ data class ConfigurationUpdate(
val factorioData: ConfigurationUpdateGameData, val factorioData: ConfigurationUpdateGameData,
// @Contextual // @Contextual
val allMods: LuaJsonList<ConfigurationUpdateModData>, val allMods: LuaJsonList<ConfigurationUpdateModData>,
val modStartupSettingsChange: Boolean, val modStartupSettingsChange: Boolean = false,
val migrationApplied: Boolean, val migrationApplied: Boolean = false,
) : KafkatorioInstantPacketData() { ) : KafkatorioInstantPacketData() {
@Serializable @Serializable

View file

@ -7,6 +7,7 @@ import dev.adamko.kafkatorio.schema.common.EntityIdentifiersData
import dev.adamko.kafkatorio.schema.common.EventName import dev.adamko.kafkatorio.schema.common.EventName
import dev.adamko.kafkatorio.schema.common.FactorioEntityData import dev.adamko.kafkatorio.schema.common.FactorioEntityData
import dev.adamko.kafkatorio.schema.common.FactorioEntityUpdateDictionary import dev.adamko.kafkatorio.schema.common.FactorioEntityUpdateDictionary
import dev.adamko.kafkatorio.schema.common.FactorioEntityUpdateDictionary.ResourceAmounts
import dev.adamko.kafkatorio.schema.common.ForceIndex import dev.adamko.kafkatorio.schema.common.ForceIndex
import dev.adamko.kafkatorio.schema.common.MapChunkPosition import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapEntityPosition import dev.adamko.kafkatorio.schema.common.MapEntityPosition
@ -135,7 +136,7 @@ data class PlayerUpdate(
val kickedReason: String? = null, val kickedReason: String? = null,
val disconnectReason: String? = null, val disconnectReason: String? = null,
/** `true` when a player is removed (deleted) from the game */ /** `true` when a player is removed (deleted) from the game */
val isRemoved: Boolean? = null, val isRemoved: Boolean = false,
) : KafkatorioKeyedPacketData() ) : KafkatorioKeyedPacketData()
@ -166,7 +167,14 @@ data class MapChunkEntityUpdate(
FactorioEntityData.Standard( FactorioEntityData.Standard(
protoId = key.protoId, protoId = key.protoId,
position = position, position = position,
element = entity,
status = entity.status,
unitNumber = entity.unitNumber,
graphicsVariation = entity.graphicsVariation,
health = entity.health,
isActive = entity.isActive,
isRotatable = entity.isRotatable,
lastUser = entity.lastUser
) )
} }
} }
@ -191,15 +199,20 @@ data class MapChunkResourceUpdate(
override val events: Map<EventName, LuaJsonList<Tick>>? = null, override val events: Map<EventName, LuaJsonList<Tick>>? = null,
val resourcesXY: FactorioEntityUpdateDictionary.Resource, // Optimise JSON size by storing amounts separately.
val amounts: ResourceAmounts = ResourceAmounts(),
// Initial amount is only available for infinite resources, and infinite resources (oil) are more
// uncommon, so there are unlikely to be many per chunk
val initialAmounts: ResourceAmounts = ResourceAmounts(),
) : KafkatorioKeyedPacketData() { ) : KafkatorioKeyedPacketData() {
fun resources(): List<FactorioEntityData.Resource> = fun resources(): List<FactorioEntityData.Resource> =
resourcesXY.map { (position, resource) -> amounts.map { (position, amount) ->
FactorioEntityData.Resource( FactorioEntityData.Resource(
protoId = key.protoId, protoId = key.protoId,
position = position, position = position,
element = resource, amount = amount,
initialAmount = initialAmounts[position],
) )
} }
} }

View file

@ -99,7 +99,7 @@ class MySourceCodeGenerator(
(element.typeRef as? TsTypeRef.Literal)?.element is TsLiteral.Custom (element.typeRef as? TsTypeRef.Literal)?.element is TsLiteral.Custom
|| element.id.name == "Tick" || element.id.name == "Tick"
|| element.id.name == "FactorioEntityUpdateEntityDictionary" || element.id.name == "FactorioEntityUpdateEntityDictionary"
|| element.id.name == "FactorioEntityUpdateResourceDictionary" || element.id.name == "FactorioEntityUpdateResourceAmountDictionary"
) { ) {
val aliasedRef = generateTypeReference(element.typeRef) val aliasedRef = generateTypeReference(element.typeRef)
""" """

View file

@ -15,7 +15,8 @@ function KafkatorioPacketEmitter.prototype.emitKeyedPacket(self, data)
end end
function KafkatorioPacketEmitter.emitPacket(self, packet) function KafkatorioPacketEmitter.emitPacket(self, packet)
local data = game.table_to_json(packet) local data = game.table_to_json(packet)
localised_print("KafkatorioPacket: " .. data) local encoded = game.encode_string(data)
print("KafkatorioPacket encoded::: " .. tostring(encoded))
end end
local PacketEmitter = __TS__New(____exports.KafkatorioPacketEmitter) local PacketEmitter = __TS__New(____exports.KafkatorioPacketEmitter)
____exports.default = PacketEmitter ____exports.default = PacketEmitter

View file

@ -42,13 +42,6 @@ do
end end
return masks return masks
end end
function Converters.convertResourceEntity(entity)
if entity.type ~= "resource" then
return nil
else
return {amount = entity.amount, initialAmount = entity.initial_amount}
end
end
local function convertEntityStatus(status) local function convertEntityStatus(status)
if status == nil then if status == nil then
return nil return nil

View file

@ -142,16 +142,23 @@ function EntityUpdatesHandler.throttleResourcesUpdate(self, eventName, eventTick
local ____data_events_eventName_17 = data.events[eventName] local ____data_events_eventName_17 = data.events[eventName]
____data_events_eventName_17[#____data_events_eventName_17 + 1] = eventTick ____data_events_eventName_17[#____data_events_eventName_17 + 1] = eventTick
for ____, entity in ipairs(resourceEntities) do for ____, entity in ipairs(resourceEntities) do
local resourceUpdate = Converters.convertResourceEntity(entity) if data.amounts == nil then
if resourceUpdate ~= nil then data.amounts = {}
if data.resourcesXY == nil then
data.resourcesXY = {}
end end
local ____data_resourcesXY_18, ____tostring_result_19 = data.resourcesXY, tostring(entity.position.x) local ____data_amounts_18, ____tostring_result_19 = data.amounts, tostring(entity.position.x)
if ____data_resourcesXY_18[____tostring_result_19] == nil then if ____data_amounts_18[____tostring_result_19] == nil then
____data_resourcesXY_18[____tostring_result_19] = {} ____data_amounts_18[____tostring_result_19] = {}
end end
data.resourcesXY[tostring(entity.position.x)][tostring(entity.position.y)] = resourceUpdate data.amounts[tostring(entity.position.x)][tostring(entity.position.y)] = entity.amount
if entity.initial_amount ~= nil then
if data.initialAmounts == nil then
data.initialAmounts = {}
end
local ____data_initialAmounts_20, ____tostring_result_21 = data.initialAmounts, tostring(entity.position.x)
if ____data_initialAmounts_20[____tostring_result_21] == nil then
____data_initialAmounts_20[____tostring_result_21] = {}
end
data.initialAmounts[tostring(entity.position.x)][tostring(entity.position.y)] = entity.initial_amount
end end
end end
end end

View file

@ -40,8 +40,8 @@ export namespace KafkatorioPacketData {
type: KafkatorioPacketData.Type.ConfigurationUpdate; type: KafkatorioPacketData.Type.ConfigurationUpdate;
factorioData: ConfigurationUpdateGameData; factorioData: ConfigurationUpdateGameData;
allMods: ConfigurationUpdateModData[]; allMods: ConfigurationUpdateModData[];
modStartupSettingsChange: boolean; modStartupSettingsChange?: boolean;
migrationApplied: boolean; migrationApplied?: boolean;
} }
export interface ConsoleChatUpdate { export interface ConsoleChatUpdate {
@ -94,7 +94,8 @@ export namespace KafkatorioPacketData {
type: KafkatorioPacketData.Type.MapChunkResourceUpdate; type: KafkatorioPacketData.Type.MapChunkResourceUpdate;
key: MapChunkResourceUpdateKey; key: MapChunkResourceUpdateKey;
events?: { [key: EventName]: Tick[] } | null; events?: { [key: EventName]: Tick[] } | null;
resourcesXY: FactorioEntityUpdateResourceDictionary; amounts?: FactorioEntityUpdateResourceAmountDictionary;
initialAmounts?: FactorioEntityUpdateResourceAmountDictionary;
} }
export interface MapChunkTileUpdate { export interface MapChunkTileUpdate {
@ -132,7 +133,7 @@ export namespace KafkatorioPacketData {
bannedReason?: string | null; bannedReason?: string | null;
kickedReason?: string | null; kickedReason?: string | null;
disconnectReason?: string | null; disconnectReason?: string | null;
isRemoved?: boolean | null; isRemoved?: boolean;
} }
export interface Error { export interface Error {
@ -171,7 +172,7 @@ export interface MapChunkEntityUpdateKey {
chunkPosition: MapChunkPosition; chunkPosition: MapChunkPosition;
} }
export type FactorioEntityUpdateEntityDictionary = { [key: string]: { [key: string]: FactorioEntityUpdateEntity } }; export type FactorioEntityUpdateEntityDictionary = { [key: string]: { [key: string]: FactorioEntityUpdateElementStandard } };
export interface MapChunkResourceUpdateKey { export interface MapChunkResourceUpdateKey {
protoId: PrototypeId; protoId: PrototypeId;
@ -179,7 +180,7 @@ export interface MapChunkResourceUpdateKey {
chunkPosition: MapChunkPosition; chunkPosition: MapChunkPosition;
} }
export type FactorioEntityUpdateResourceDictionary = { [key: string]: { [key: string]: FactorioEntityUpdateResource } }; export type FactorioEntityUpdateResourceAmountDictionary = { [key: string]: { [key: string]: UInt } };
export interface MapChunkTileUpdateKey { export interface MapChunkTileUpdateKey {
chunkPosition: MapChunkPosition; chunkPosition: MapChunkPosition;
@ -251,7 +252,7 @@ export namespace FactorioPrototype {
layer: UInt; layer: UInt;
collisionMasks: string[]; collisionMasks: string[];
order: string; order: string;
canBeMined: boolean; canBeMined?: boolean;
} }
} }
@ -274,7 +275,7 @@ export interface EntityItemGroup {
} }
export interface EntityMiningProperties { export interface EntityMiningProperties {
canBeMined: boolean; canBeMined?: boolean;
products: MinedProduct[] | null; products: MinedProduct[] | null;
} }
@ -293,21 +294,16 @@ export enum ChunkSize {
export type PrototypeKey = Int & { __PrototypeKey__: void }; export type PrototypeKey = Int & { __PrototypeKey__: void };
export interface FactorioEntityUpdateEntity { export interface FactorioEntityUpdateElementStandard {
status?: FactorioEntityStatus | null; status?: FactorioEntityStatus | null;
unitNumber?: UnitNumber | null; unitNumber?: UnitNumber | null;
graphicsVariation?: UByte | null; graphicsVariation?: UByte | null;
health?: Float | null; health?: Float | null;
isActive?: boolean | null; isActive?: boolean;
isRotatable?: boolean | null; isRotatable?: boolean;
lastUser?: UInt | null; lastUser?: UInt | null;
} }
export interface FactorioEntityUpdateResource {
amount: UInt;
initialAmount?: UInt | null;
}
export type MinedProduct = export type MinedProduct =
| MinedProduct.MinedProductFluid | MinedProduct.MinedProductFluid
| MinedProduct.MinedProductItem; | MinedProduct.MinedProductItem;

View file

@ -26,8 +26,10 @@ export class KafkatorioPacketEmitter {
/** Emit a serialised KafkatorioPacket */ /** Emit a serialised KafkatorioPacket */
private static emitPacket<T extends KafkatorioPacket>(packet: T) { private static emitPacket<T extends KafkatorioPacket>(packet: T) {
let data = game.table_to_json(packet) const data = game.table_to_json(packet)
localised_print(`KafkatorioPacket: ${data}`) // print(`KafkatorioPacket::: ${data}`)
const encoded = game.encode_string(data)
print(`KafkatorioPacket encoded::: ${encoded}`)
// rcon.print(`KafkatorioPacket: ${data}`) // rcon.print(`KafkatorioPacket: ${data}`)
} }

View file

@ -5,7 +5,6 @@ import {
EntityMiningProperties, EntityMiningProperties,
EventName, EventName,
FactorioEntityStatus, FactorioEntityStatus,
FactorioEntityUpdateResource,
KafkatorioPacketData, KafkatorioPacketData,
MapBoundingBox, MapBoundingBox,
MapChunkPosition, MapChunkPosition,
@ -76,20 +75,6 @@ export namespace Converters {
} }
export function convertResourceEntity(
entity: LuaEntity
): FactorioEntityUpdateResource | null {
if (entity.type != "resource") {
return null
} else {
return {
amount: entity.amount,
initialAmount: entity.initial_amount,
}
}
}
function convertEntityStatus( function convertEntityStatus(
status: defines.entity_status | undefined status: defines.entity_status | undefined
): FactorioEntityStatus | null { ): FactorioEntityStatus | null {

View file

@ -1,6 +1,6 @@
import { import {
EventName, EventName,
FactorioEntityUpdateEntity, FactorioEntityUpdateElementStandard,
KafkatorioPacketData, KafkatorioPacketData,
MapChunkPosition, MapChunkPosition,
PrototypeId, PrototypeId,
@ -94,7 +94,7 @@ export class EntityUpdatesHandler {
chunkPosition: Converters.mapPositionToChunkPosition(entity.position) chunkPosition: Converters.mapPositionToChunkPosition(entity.position)
} }
const entityUpdate: FactorioEntityUpdateEntity = { const entityUpdate: FactorioEntityUpdateElementStandard = {
unitNumber: entity.unit_number, unitNumber: entity.unit_number,
graphicsVariation: entity.graphics_variation, graphicsVariation: entity.graphics_variation,
health: entity.health, health: entity.health,
@ -140,7 +140,7 @@ export class EntityUpdatesHandler {
for (const [protoId, resourceEntities] of pairs(entitiesByProtoId)) { for (const [protoId, resourceEntities] of pairs(entitiesByProtoId)) {
// create an update for each prototype ID // create a MapChunkResourceUpdate for each prototype ID
const entityUpdateKey: KafkatorioPacketData.MapChunkResourceUpdate["key"] = { const entityUpdateKey: KafkatorioPacketData.MapChunkResourceUpdate["key"] = {
protoId: protoId, protoId: protoId,
@ -158,18 +158,20 @@ export class EntityUpdatesHandler {
data.events[eventName].push(eventTick) data.events[eventName].push(eventTick)
for (const entity of resourceEntities) { for (const entity of resourceEntities) {
const resourceUpdate = Converters.convertResourceEntity(entity)
if (resourceUpdate != null) { data.amounts ??= {}
data.resourcesXY ??= {} data.amounts[`${entity.position.x}`] ??= {}
data.resourcesXY[`${entity.position.x}`] ??= {} data.amounts[`${entity.position.x}`][`${entity.position.y}`] = entity.amount
data.resourcesXY[`${entity.position.x}`][`${entity.position.y}`] = resourceUpdate
if (entity.initial_amount != null) {
data.initialAmounts ??= {}
data.initialAmounts[`${entity.position.x}`] ??= {}
data.initialAmounts[`${entity.position.x}`][`${entity.position.y}`] = entity.initial_amount
} }
} }
} }
) )
} }
} }

View file

@ -167,6 +167,7 @@ internal class KafkatorioTopology(
} }
/** Receive log messages from the syslog socket, and send them to a Kafka topic */
private fun syslogProducer( private fun syslogProducer(
id: String = "syslog-producer" id: String = "syslog-producer"
) { ) {
@ -187,10 +188,14 @@ internal class KafkatorioTopology(
} }
syslogServer.messages syslogServer.messages
.filter { it.message?.startsWith("KafkatorioPacket:") == true } .filter {
(it.message ?: "").run {
startsWith("KafkatorioPacket:::") || startsWith("KafkatorioPacket encoded:::")
}
}
.onEach { syslogMsg -> .onEach { syslogMsg ->
val kafkatorioMessage = syslogMsg.message?.substringAfter("KafkatorioPacket:") val kafkatorioMessage = syslogMsg.message?.substringAfter(":::")
val record = ProducerRecord<String, String>( val record = ProducerRecord<String, String>(
TOPIC_SRC_SERVER_LOG, TOPIC_SRC_SERVER_LOG,

View file

@ -14,7 +14,13 @@ import dev.adamko.kotka.extensions.producedAs
import dev.adamko.kotka.extensions.streams.map import dev.adamko.kotka.extensions.streams.map
import dev.adamko.kotka.extensions.streams.to import dev.adamko.kotka.extensions.streams.to
import dev.adamko.kotka.kxs.serde import dev.adamko.kotka.kxs.serde
import java.util.zip.Inflater
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.buffer
import okio.inflate
import org.apache.kafka.common.serialization.Serdes import org.apache.kafka.common.serialization.Serdes
import org.apache.kafka.streams.StreamsBuilder import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.KStream import org.apache.kafka.streams.kstream.KStream
@ -33,7 +39,8 @@ fun factorioServerPacketStream(
) )
).map("decode-packets") { serverId: String, value: String -> ).map("decode-packets") { serverId: String, value: String ->
val packet = runCatching { val packet = runCatching {
jsonMapper.decodeFromString<KafkatorioPacket>(value) val decoded = decodeFactorioEncodedString(value)
jsonMapper.decodeFromString<KafkatorioPacket>(decoded)
}.getOrElse { e -> }.getOrElse { e ->
KafkatorioPacket( KafkatorioPacket(
modVersion = "unknown", modVersion = "unknown",
@ -69,3 +76,18 @@ fun splitFactorioServerPacketStream(
value.data.topicName value.data.topicName
} }
} }
/**
* Factorio `game.encode_string` zlib encodes a string, then base64's it. Here we do the reverse.
*/
private fun decodeFactorioEncodedString(
source: String
): String {
val bytes: ByteString = source.decodeBase64() ?: error("failed to base64 decode $source")
return Buffer().write(bytes)
.inflate(Inflater())
.buffer()
.readString(Charsets.UTF_8)
}