workinggggg!!!!!!!

- fix map tiles proto colours KTable
- test and tidy up Colour/ColourHex
- reset event-server application
This commit is contained in:
Adam 2022-02-08 11:51:16 +01:00
parent bdb2d9577f
commit 5a59b3bd6d
13 changed files with 211 additions and 97 deletions

View file

@ -1,6 +1,7 @@
package dev.adamko.kafkatorio.events.schema.converters package dev.adamko.kafkatorio.events.schema.converters
import dev.adamko.kafkatorio.events.schema.Colour import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.ColourHex
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -15,39 +16,12 @@ fun Colour.isHexadecimal(): Boolean {
/** True if all values are between `[0..1]`. */ /** True if all values are between `[0..1]`. */
fun Colour.isPercentage(): Boolean = !isHexadecimal() fun Colour.isPercentage(): Boolean = !isHexadecimal()
fun Colour.toHexadecimal() = if (isPercentage()) {
Colour(
(red * 255).coerceIn(0f..255f),
(green * 255).coerceIn(0f..255f),
(blue * 255).coerceIn(0f..255f),
(alpha * 255).coerceIn(0f..255f),
)
} else {
Colour(
red.coerceIn(0f..255f),
green.coerceIn(0f..255f),
blue.coerceIn(0f..255f),
alpha.coerceIn(0f..255f),
)
}
fun Colour.toPercentage() = if (isPercentage()) {
this
} else {
Colour(
(red / 255).coerceIn(0f..1f),
(green / 255).coerceIn(0f..1f),
(blue / 255).coerceIn(0f..1f),
(alpha / 255).coerceIn(0f..1f),
)
}
fun Colour.toHexString(includeAlpha: Boolean = false): String { fun Colour.toHexString(includeAlpha: Boolean = false): String {
val hexConverter: Float.() -> String = if (isHexadecimal()) { val hexConverter: Float.() -> String = when {
Float::decimalToHex isHexadecimal() -> Float::hexadecimalToString
} else { else -> Float::percentageToHexadecimalString
Float::percentageToHex
} }
val length = if (includeAlpha) 9 else 7 val length = if (includeAlpha) 9 else 7
@ -62,8 +36,51 @@ fun Colour.toHexString(includeAlpha: Boolean = false): String {
} }
} }
fun Float.decimalToHex(): String = fun Float.hexadecimalToString(): String =
roundToInt().toString(16).padStart(2, ' ') roundToInt().toString(16).padStart(2, ' ')
fun Float.percentageToHex(): String = fun Float.percentageToHexadecimalString(): String =
times(255).roundToInt().toString(16).padStart(2, ' ') times(255).roundToInt().toString(16).padStart(2, ' ')
private val ubyteMaxFloat = UByte.MAX_VALUE.toFloat()
private val ubyteMinFloat = UByte.MIN_VALUE.toFloat()
private val ubyteRangeFloat = ubyteMinFloat..ubyteMaxFloat
private fun Float.toColourHex() = (this * ubyteMaxFloat)
.coerceIn(ubyteRangeFloat)
.toUInt()
.toUByte()
private fun Float.coerceToColourHex() = coerceIn(ubyteRangeFloat)
.toUInt()
.toUByte()
fun Colour.toHex(): ColourHex {
val converter: Float.() -> UByte = when {
isPercentage() -> Float::toColourHex
else -> Float::coerceToColourHex
}
return ColourHex(
red.converter(),
green.converter(),
blue.converter(),
alpha.converter(),
)
}
private fun UByte.toPercentileFloat() = (this.toFloat() / ubyteMaxFloat).coerceIn(0f..1f)
fun ColourHex.toPercentile(): Colour {
return Colour(
red.toPercentileFloat(),
green.toPercentileFloat(),
blue.toPercentileFloat(),
alpha.toPercentileFloat(),
)
}

View file

@ -1,7 +1,6 @@
package dev.adamko.kafkatorio.events.schema.converters package dev.adamko.kafkatorio.events.schema.converters
import dev.adamko.kafkatorio.events.schema.MAP_CHUNK_SIZE import dev.adamko.kafkatorio.events.schema.MAP_CHUNK_SIZE
import dev.adamko.kafkatorio.events.schema.MapBoundingBox
import dev.adamko.kafkatorio.events.schema.MapChunkPosition import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapEntityPosition import dev.adamko.kafkatorio.events.schema.MapEntityPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition import dev.adamko.kafkatorio.events.schema.MapTilePosition
@ -17,7 +16,7 @@ operator fun MapTilePosition.plus(addend: Int) =
MapTilePosition(x + addend, y + addend) MapTilePosition(x + addend, y + addend)
fun MapTilePosition.toMapChunkPosition( chunkSize: Int = MAP_CHUNK_SIZE) = fun MapTilePosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition( MapChunkPosition(
floor(x.toDouble() / chunkSize.toDouble()).toInt(), floor(x.toDouble() / chunkSize.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.toDouble()).toInt(), floor(y.toDouble() / chunkSize.toDouble()).toInt(),
@ -27,7 +26,7 @@ fun MapTilePosition.toMapChunkPosition( chunkSize: Int = MAP_CHUNK_SIZE) =
fun MapEntityPosition.toMapTilePosition() = fun MapEntityPosition.toMapTilePosition() =
MapChunkPosition(floor(x).toInt(), floor(y).toInt()) MapChunkPosition(floor(x).toInt(), floor(y).toInt())
fun MapEntityPosition.toMapChunkPosition( chunkSize: Int = MAP_CHUNK_SIZE) = fun MapEntityPosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition( MapChunkPosition(
floor(x / chunkSize.toDouble()).toInt(), floor(x / chunkSize.toDouble()).toInt(),
floor(y / chunkSize.toDouble()).toInt(), floor(y / chunkSize.toDouble()).toInt(),
@ -44,8 +43,8 @@ operator fun MapChunkPosition.minus(subtrahend: Int) =
MapChunkPosition(x - subtrahend, y - subtrahend) MapChunkPosition(x - subtrahend, y - subtrahend)
fun MapChunkPosition.leftTopTile( chunkSize: Int = MAP_CHUNK_SIZE): MapTilePosition = fun MapChunkPosition.leftTopTile(chunkSize: Int = MAP_CHUNK_SIZE): MapTilePosition =
MapTilePosition( MapTilePosition(
x * chunkSize, x * chunkSize,
y * chunkSize, y * chunkSize,
) )

View file

@ -1,6 +1,5 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.events.schema.converters.toHexadecimal
import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -69,20 +68,10 @@ data class Colour(
val blue: Float = 0f, val blue: Float = 0f,
@EncodeDefault @EncodeDefault
val alpha: Float = 1f, 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] */ /** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
@Serializable @Serializable
data class ColourHex( data class ColourHex(
@EncodeDefault @EncodeDefault

View file

@ -0,0 +1,86 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.events.schema.converters.toHex
import dev.adamko.kafkatorio.events.schema.converters.toPercentile
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.floats.shouldBeWithinPercentageOf
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.property.checkAll
import kotlinx.serialization.decodeFromString
class ColourHexTest : BehaviorSpec({
Given("colour json") {
//Language=JSON
val colourJson = """
{
"red": 0.1,
"green": 0.4,
"blue": 0.7,
"alpha": 0.6
}
""".trimIndent()
val colour: Colour = jsonMapperKafkatorio.decodeFromString(colourJson)
Then("expect colour is decoded") {
colour shouldBe Colour(
0.1f,
0.4f,
0.7f,
0.6f,
)
}
Then("expect Colour can be converted to ColourHex and back") {
val colourHex = colour.toHex()
colourHex shouldBe ColourHex(
25u,
102u,
178u,
153u,
)
colourHex.toPercentile() should { back ->
withClue(back) {
val tolerance = 2.6 // 2.6%, because 255/100 = 2.55
withClue("red") { back.red.shouldBeWithinPercentageOf(0.1f, tolerance) }
withClue("green") { back.green.shouldBeWithinPercentageOf(0.4f, tolerance) }
withClue("blue") { back.blue.shouldBeWithinPercentageOf(0.7f, tolerance) }
withClue("alpha") { back.alpha.shouldBeWithinPercentageOf(0.6f, tolerance) }
}
}
}
}
Given("any colour json") {
Then("expect can always convert") {
checkAll<UByte, UByte, UByte, UByte> { r, g, b, a ->
//Language=JSON
val colourJson = """
{
"red": ${r.toFloat() / UByte.MAX_VALUE.toFloat()},
"green": ${g.toFloat() / UByte.MAX_VALUE.toFloat()},
"blue": ${b.toFloat() / UByte.MAX_VALUE.toFloat()},
"alpha": ${a.toFloat() / UByte.MAX_VALUE.toFloat()}
}
""".trimIndent()
val colour: Colour = jsonMapperKafkatorio.decodeFromString(colourJson)
val colourHex = colour.toHex()
colourHex shouldBe ColourHex(r, g, b, a)
}
}
}
})

View file

@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.util.parseSpaceSeparatedArgs
plugins { plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm` dev.adamko.kafkatorio.lang.`kotlin-jvm`
@ -64,3 +65,19 @@ tasks.withType<KotlinCompile> {
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
) )
} }
val kafkatorioEventsServerKafkaForceReset by tasks.registering(Exec::class) {
group = project.name
val cmd = """
/kafka/bin/kafka-streams-application-reset.sh --application-id kafkatorio-events-processor --input-topics factorio-server-log --force
""".trimIndent()
logging.captureStandardOutput(LogLevel.LIFECYCLE)
commandLine = parseSpaceSeparatedArgs(""" docker exec -d kafka $cmd """)
}
tasks.run.configure {
dependsOn(kafkatorioEventsServerKafkaForceReset)
}

View file

@ -12,11 +12,11 @@ import org.apache.kafka.clients.admin.TopicListing
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun main() { //fun main() {
CreateTopics.create() // KafkaAdmin.createKafkatorioTopics()
} //}
object CreateTopics { object KafkaAdmin {
private val kafkaAdmin: Admin = private val kafkaAdmin: Admin =
Admin.create( Admin.create(
@ -26,7 +26,7 @@ object CreateTopics {
) )
) )
fun create() { fun createKafkatorioTopics() {
val currentTopics = currentTopics().map { it.name() } val currentTopics = currentTopics().map { it.name() }

View file

@ -16,6 +16,7 @@ class KafkatorioTopology(
private val builder: StreamsBuilder = StreamsBuilder(), private val builder: StreamsBuilder = StreamsBuilder(),
) { ) {
companion object { companion object {
const val sourceTopic = "factorio-server-log" const val sourceTopic = "factorio-server-log"
} }
@ -23,11 +24,8 @@ class KafkatorioTopology(
fun build() { fun build() {
val packets = factorioServerPacketStream(builder) val packets = factorioServerPacketStream(builder)
splitFactorioServerPacketStream(packets) splitFactorioServerPacketStream(packets)
playerUpdatesToWsServer(websocketServer, builder) playerUpdatesToWsServer(websocketServer, builder)

View file

@ -1,7 +1,7 @@
package dev.adamko.kafkatorio.processor package dev.adamko.kafkatorio.processor
import dev.adamko.kafkatorio.admin.CreateTopics import dev.adamko.kafkatorio.admin.KafkaAdmin
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.runBlocking import kotlinx.coroutines.runBlocking
@ -9,11 +9,13 @@ import org.http4k.server.PolyHandler
import org.http4k.server.Undertow import org.http4k.server.Undertow
import org.http4k.server.asServer import org.http4k.server.asServer
val appProps = ApplicationProperties() val appProps = ApplicationProperties()
fun main(): Unit = runBlocking { fun main(): Unit = runBlocking {
CreateTopics.create() KafkaAdmin.createKafkatorioTopics()
val wsServer = WebsocketServer() val wsServer = WebsocketServer()
val tileServer = WebMapTileServer() val tileServer = WebMapTileServer()

View file

@ -76,11 +76,6 @@ fun groupTilesIntoChunksWithColours(
val zoomLevel = ZoomLevel.ZOOM_0 val zoomLevel = ZoomLevel.ZOOM_0
//
// ZoomLevel
// .values
// .mapNotNull { zoomLevel ->
mapTileData mapTileData
?.tiles ?.tiles
?.groupBy { tile -> ?.groupBy { tile ->
@ -101,7 +96,7 @@ fun groupTilesIntoChunksWithColours(
chunkId to chunkTiles chunkId to chunkTiles
} ?: listOf() } ?: listOf()
// }.flatten()
} }
.groupByKey( .groupByKey(
groupedAs("server-map-data.tiles.group", kxsBinary.serde(), kxsBinary.serde()) groupedAs("server-map-data.tiles.group", kxsBinary.serde(), kxsBinary.serde())
@ -121,15 +116,20 @@ fun groupTilesIntoChunksWithColours(
foreignKeyExtractor = { chunkTiles: ServerMapChunkTiles<TileProtoHashCode> -> chunkTiles.chunkId.serverId } foreignKeyExtractor = { chunkTiles: ServerMapChunkTiles<TileProtoHashCode> -> chunkTiles.chunkId.serverId }
) { tiles: ServerMapChunkTiles<TileProtoHashCode>, colourDict: TileColourDict -> ) { tiles: ServerMapChunkTiles<TileProtoHashCode>, colourDict: TileColourDict ->
ServerMapChunkTiles( val missingProtos = mutableSetOf<TileProtoHashCode>()
tiles.chunkId,
tiles.map.mapValues { (_, code) -> val tileColours = tiles.map.mapValues { (_, code) ->
colourDict.map.getOrElse(code) { colourDict.map.getOrElse(code) {
println("missing prototype $code") missingProtos.add(code)
ColourHex.TRANSPARENT ColourHex.TRANSPARENT
}
} }
) }
if (missingProtos.isNotEmpty()) {
println("missing ${missingProtos.size} tile prototypes: ${missingProtos.joinToString { "${it.code}" }}")
}
ServerMapChunkTiles(tiles.chunkId, tileColours)
} }
return colourisedChunkTable return colourisedChunkTable

View file

@ -36,6 +36,7 @@ fun factorioServerPacketStream(
} }
} }
fun splitFactorioServerPacketStream( fun splitFactorioServerPacketStream(
factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket> factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket>
) { ) {

View file

@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.processor.topology
import com.sksamuel.scrimage.ImmutableImage import com.sksamuel.scrimage.ImmutableImage
import com.sksamuel.scrimage.ScaleMethod import com.sksamuel.scrimage.ScaleMethod
import com.sksamuel.scrimage.color.RGBColor
import com.sksamuel.scrimage.nio.PngWriter import com.sksamuel.scrimage.nio.PngWriter
import dev.adamko.kafkatorio.events.schema.ColourHex import dev.adamko.kafkatorio.events.schema.ColourHex
import dev.adamko.kafkatorio.events.schema.MapChunkPosition import dev.adamko.kafkatorio.events.schema.MapChunkPosition
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -63,7 +63,7 @@ fun saveMapTiles(
chunkedTiles chunkedTiles
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
.debounce(5.seconds) .debounce(1.seconds)
.map { .map {
println("createMapTileImage ${it.chunkId}") println("createMapTileImage ${it.chunkId}")
createMapTileImage(it) createMapTileImage(it)
@ -164,6 +164,9 @@ private fun createMapTileImage(
} }
private val TRANSPARENT_AWT = ColourHex.TRANSPARENT.toRgbColor().awt()
private fun createMapTileImage( private fun createMapTileImage(
chunkPosition: MapChunkPosition, chunkPosition: MapChunkPosition,
chunkColours: Map<MapTilePosition, ColourHex>, chunkColours: Map<MapTilePosition, ColourHex>,
@ -173,7 +176,7 @@ private fun createMapTileImage(
val chunkImage = ImmutableImage.filled( val chunkImage = ImmutableImage.filled(
z.chunkSize, z.chunkSize,
z.chunkSize, z.chunkSize,
ColourHex.TRANSPARENT.toRgbColor().awt(), TRANSPARENT_AWT,
BufferedImage.TYPE_INT_ARGB BufferedImage.TYPE_INT_ARGB
) )
@ -196,3 +199,13 @@ private fun createMapTileImage(
return chunkImage.scaleTo(WEB_MAP_TILE_SIZE_PX, WEB_MAP_TILE_SIZE_PX, ScaleMethod.Progressive) return chunkImage.scaleTo(WEB_MAP_TILE_SIZE_PX, WEB_MAP_TILE_SIZE_PX, ScaleMethod.Progressive)
} }
private fun ColourHex.toRgbColor(): RGBColor {
return RGBColor(
red.toInt(),
green.toInt(),
blue.toInt(),
alpha.toInt(),
)
}

View file

@ -1,8 +1,5 @@
package dev.adamko.kafkatorio.processor.topology 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.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -24,16 +21,6 @@ value class FactorioServerId(val id: String) {
} }
fun ColourHex.toRgbColor(): RGBColor {
return RGBColor(
red.toInt(),
green.toInt(),
blue.toInt(),
alpha.toInt(),
)
}
enum class ZoomLevel( enum class ZoomLevel(
val level: Int val level: Int
) { ) {

View file

@ -5,8 +5,10 @@ import dev.adamko.kafkatorio.events.schema.FactorioPrototypes
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.MapTile import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTilePrototype import dev.adamko.kafkatorio.events.schema.MapTilePrototype
import dev.adamko.kafkatorio.events.schema.converters.toHex
import dev.adamko.kafkatorio.processor.serdes.jsonMapper import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import dev.adamko.kotka.extensions.materializedAs import dev.adamko.kotka.extensions.materializedAs
import dev.adamko.kotka.extensions.streams.filter
import dev.adamko.kotka.extensions.streams.mapValues import dev.adamko.kotka.extensions.streams.mapValues
import dev.adamko.kotka.extensions.streams.toTable import dev.adamko.kotka.extensions.streams.toTable
import dev.adamko.kotka.kxs.serde import dev.adamko.kotka.kxs.serde
@ -22,9 +24,10 @@ value class PrototypeName(val name: String) {
} }
/** Use the hashcode of Prototypes names as keys - more efficient storage */
@JvmInline @JvmInline
@Serializable @Serializable
value class TileProtoHashCode(val code: Int) { value class TileProtoHashCode private constructor(val code: Int) {
constructor(prototype: MapTilePrototype) : this(prototype.name.hashCode()) constructor(prototype: MapTilePrototype) : this(prototype.name.hashCode())
constructor(tile: MapTile) : this(tile.prototypeName.hashCode()) constructor(tile: MapTile) : this(tile.prototypeName.hashCode())
} }
@ -32,19 +35,17 @@ value class TileProtoHashCode(val code: Int) {
@JvmInline @JvmInline
@Serializable @Serializable
value class TileColourDict(val map: Map<TileProtoHashCode, ColourHex>) { 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]). */ /** Get the latest map-tile colours per server. */
fun tileProtoColourDictionary( fun tileProtoColourDictionary(
factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket> factorioServerPacketStream: KStream<FactorioServerId, KafkatorioPacket>
): KTable<FactorioServerId, TileColourDict> { ): KTable<FactorioServerId, TileColourDict> {
// get all the prototypes // get all the prototypes
return factorioServerPacketStream return factorioServerPacketStream
.mapValues("server-map-data.tile-prototypes.mapValues") { _, packet -> .mapValues("server-map-data.tile-prototypes.mapValues") { _, packet: KafkatorioPacket ->
val map = when (packet) { val map = when (packet) {
is FactorioPrototypes -> packet is FactorioPrototypes -> packet
.prototypes .prototypes
@ -53,13 +54,17 @@ fun tileProtoColourDictionary(
else -> mapOf() else -> mapOf()
} }
TileColourDict(map) TileColourDict(map)
}.filter("server-map-data.tile-prototypes.filterMapTileProtos") { _, dict: TileColourDict ->
dict.map.isNotEmpty()
}.peek { serverId, dict: TileColourDict ->
println("server $serverId has TileColourDict[${dict.map.size}]: ${dict.map.entries.joinToString()}")
} }
.toTable( .toTable(
"server-map-data.tile-prototypes", "server-map-data.tile-prototypes",
materializedAs( materializedAs(
"server-map-data.tile-prototypes.store", "server-map-data.tile-prototypes.store",
jsonMapper.serde(), jsonMapper.serde(),
jsonMapper.serde() jsonMapper.serde(),
) )
) )