map tiles are now saved!

This commit is contained in:
Adam 2022-01-19 16:53:24 +01:00
parent 8533be0af8
commit a7a095ecbf
14 changed files with 220 additions and 151 deletions

View file

@ -105,19 +105,10 @@ export namespace Converters {
} }
} }
// export function convertMapTilePositionToMapChunkPosition( export function convertBoundingBox(bb: BoundingBoxRead): MapBoundingBox {
// tilePosition: TilePositionTable
// ): ChunkPositionTable {
// return {
// x: tilePosition.x / 32,
// y: tilePosition.y / 32,
// }
// }
export function convertBoundingBox(bb: BoundingBoxTable): MapBoundingBox {
return { return {
topLeft: mapTilePosition(bb.left_top as PositionTable), topLeft: mapTilePosition(bb.left_top),
bottomRight: mapTilePosition(bb.right_bottom as PositionTable), bottomRight: mapTilePosition(bb.right_bottom),
} }
} }

View file

@ -57,14 +57,11 @@ export function handleChunkUpdate(
eventType: string, eventType: string,
surfaceIndex: uint, surfaceIndex: uint,
position: ChunkPositionTable, position: ChunkPositionTable,
area: BoundingBoxTable, area: BoundingBoxRead,
) { ) {
let surface = game.get_surface(surfaceIndex) let surface = game.get_surface(surfaceIndex)
// let x = (position as ChunkPositionTable).x * CHUNK_SIZE
// let y = (position as ChunkPositionTable).y * CHUNK_SIZE
let tiles: LuaTile[] = surface?.find_tiles_filtered({ let tiles: LuaTile[] = surface?.find_tiles_filtered({
area: area, area: area,
// position: {x: x, y: y}, // position: {x: x, y: y},
@ -81,7 +78,7 @@ export function handleChunkUpdate(
objectName: "MapChunk", objectName: "MapChunk",
tiles: createMapTiles(surfaceIndex, convertedTiles), tiles: createMapTiles(surfaceIndex, convertedTiles),
position: Converters.mapChunkPosition(position), position: Converters.mapChunkPosition(position),
area: Converters.convertBoundingBox(area) area: Converters.convertBoundingBox(area),
} }
emitEvent(mapChunk, tick, eventType) emitEvent(mapChunk, tick, eventType)

View file

@ -9,6 +9,7 @@ import {
import on_player_built_tile = defines.events.on_player_built_tile; import on_player_built_tile = defines.events.on_player_built_tile;
import on_robot_built_tile = defines.events.on_robot_built_tile; import on_robot_built_tile = defines.events.on_robot_built_tile;
const mapEventIdToName = new LuaTable<defines.Events, keyof typeof defines.events>() const mapEventIdToName = new LuaTable<defines.Events, keyof typeof defines.events>()
for (const [k, v] of pairs(defines.events)) { for (const [k, v] of pairs(defines.events)) {
mapEventIdToName.set(v, k) mapEventIdToName.set(v, k)

View file

@ -0,0 +1,64 @@
package dev.adamko.kafkatorio.events.schema.converters
import dev.adamko.kafkatorio.events.schema.Colour
import kotlin.math.roundToInt
/** True if any value is greater than 1, so the values are hexadecimal. */
fun Colour.isHexadecimal(): Boolean {
return red > 1f
|| green > 1f
|| blue > 1f
|| alpha > 1f
}
/** True if all values are between `[0..1]`. */
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 {
this
}
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 {
val hexConverter: Float.() -> String = if (isHexadecimal()) {
Float::decimalToHex
} else {
Float::percentageToHex
}
val length = if (includeAlpha) 9 else 7
return buildString(length) {
append('#')
append(red.hexConverter())
append(green.hexConverter())
append(blue.hexConverter())
if (includeAlpha) {
append(alpha.hexConverter())
}
}
}
fun Float.decimalToHex(): String =
roundToInt().toString(16).padStart(2, ' ')
fun Float.percentageToHex(): String =
times(255).roundToInt().toString(16).padStart(2, ' ')

View file

@ -0,0 +1,58 @@
package dev.adamko.kafkatorio.events.schema.converters
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.MapEntityPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import kotlin.math.floor
operator fun MapTilePosition.times(factor: Int) =
MapTilePosition(x * factor, y * factor)
operator fun MapTilePosition.div(divisor: Int) =
MapTilePosition(x / divisor, y / divisor)
operator fun MapTilePosition.plus(addend: Int) =
MapTilePosition(x + addend, y + addend)
fun MapTilePosition.toMapChunkPosition() =
MapChunkPosition(
floor(x.toDouble() / MAP_CHUNK_SIZE.toDouble()).toInt(),
floor(y.toDouble() / MAP_CHUNK_SIZE.toDouble()).toInt(),
)
fun MapEntityPosition.toMapTilePosition() =
MapChunkPosition(floor(x).toInt(), floor(y).toInt())
fun MapEntityPosition.toMapChunkPosition() =
MapChunkPosition(
floor(x / MAP_CHUNK_SIZE.toDouble()).toInt(),
floor(y / MAP_CHUNK_SIZE.toDouble()).toInt(),
)
operator fun MapChunkPosition.times(factor: Int) =
MapChunkPosition(x * factor, y * factor)
operator fun MapChunkPosition.plus(addend: Int) =
MapChunkPosition(x + addend, y + addend)
operator fun MapChunkPosition.minus(subtrahend: Int) =
MapChunkPosition(x - subtrahend, y - subtrahend)
val MapChunkPosition.leftTopTile: MapTilePosition
get() = MapTilePosition(
x * MAP_CHUNK_SIZE,
y * MAP_CHUNK_SIZE,
)
val MapChunkPosition.rightBottomTile: MapTilePosition
get() = leftTopTile + (MAP_CHUNK_SIZE - 1)
val MapChunkPosition.boundingBox: MapBoundingBox
get() = MapBoundingBox(leftTopTile, rightBottomTile)

View file

@ -1,9 +1,9 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import kotlin.math.roundToInt
import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32 const val MAP_CHUNK_SIZE = 32
/** /**
@ -16,14 +16,8 @@ const val MAP_CHUNK_SIZE = 32
data class MapEntityPosition( data class MapEntityPosition(
val x: Double, val x: Double,
val y: Double, val y: Double,
) {
fun toMapChunkPosition() = MapChunkPosition(
(x / MAP_CHUNK_SIZE).toInt(),
(y / MAP_CHUNK_SIZE).toInt(),
) )
}
/** Coordinates of a chunk in a [SurfaceData] where each integer `x`/`y` represents a different /** Coordinates of a chunk in a [SurfaceData] where each integer `x`/`y` represents a different
* chunk. * chunk.
@ -35,19 +29,8 @@ data class MapEntityPosition(
data class MapChunkPosition( data class MapChunkPosition(
val x: Int, val x: Int,
val y: Int, val y: Int,
) {
fun toMapEntityPosition() = MapEntityPosition(
x * MAP_CHUNK_SIZE.toDouble(),
y * MAP_CHUNK_SIZE.toDouble(),
) )
fun toMapTilePosition() = MapChunkPosition(
x * MAP_CHUNK_SIZE,
y * MAP_CHUNK_SIZE,
)
}
/** /**
* Coordinates of a tile in a chunk on a [SurfaceData] where each integer `x`/`y` represents a * Coordinates of a tile in a chunk on a [SurfaceData] where each integer `x`/`y` represents a
@ -59,16 +42,8 @@ data class MapChunkPosition(
data class MapTilePosition( data class MapTilePosition(
val x: Int, val x: Int,
val y: Int, val y: Int,
) {
constructor(x: Number, y: Number) : this(x.toInt(), y.toInt())
fun toMapChunkPosition() = MapChunkPosition(
x / MAP_CHUNK_SIZE,
y / MAP_CHUNK_SIZE,
) )
}
@Serializable @Serializable
data class MapBoundingBox( data class MapBoundingBox(
@ -93,39 +68,4 @@ data class Colour(
val blue: Float = 0f, val blue: Float = 0f,
@EncodeDefault @EncodeDefault
val alpha: Float = 1f, val alpha: Float = 1f,
) {
/** True if any value is greater than 1, so the values are hexadecimal. */
fun isHexadecimal(): Boolean {
return red > 1f
|| green > 1f
|| blue > 1f
|| alpha > 1f
}
/** True if all values are between `[0..1]`. */
fun isPercentage(): Boolean = !isHexadecimal()
fun asHexadecimal() = if (isPercentage()) {
Colour(
(red * 255).coerceIn(0f..255f),
(green * 255).coerceIn(0f..255f),
(blue * 255).coerceIn(0f..255f),
(alpha * 255).coerceIn(0f..255f),
) )
} else {
this
}
fun asPercentage() = 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),
)
}
}

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

@ -0,0 +1 @@
**/kafkatorio-web-map/**

View file

@ -37,7 +37,7 @@ dependencies {
implementation("com.sksamuel.scrimage:scrimage-core:4.0.24") implementation("com.sksamuel.scrimage:scrimage-core:4.0.24")
val kotkaVersion = "0.0.5" val kotkaVersion = "0.0.8"
implementation("com.github.adamko-dev.kotka-streams:kotka-streams:$kotkaVersion") { implementation("com.github.adamko-dev.kotka-streams:kotka-streams:$kotkaVersion") {
isChanging = true isChanging = true
} }

View file

@ -29,8 +29,8 @@ object CreateTopics {
KafkatorioPacket.PacketType.values().forEach { packetType -> KafkatorioPacket.PacketType.values().forEach { packetType ->
val kafkatorioTopics = buildList { val kafkatorioTopics = buildSet {
add(KafkatorioTopology.sourceTopic) // add(KafkatorioTopology.sourceTopic)
when (packetType) { when (packetType) {
KafkatorioPacket.PacketType.EVENT -> KafkatorioPacket.PacketType.EVENT ->
FactorioObjectData.ObjectName.values().forEach { objectName -> FactorioObjectData.ObjectName.values().forEach { objectName ->
@ -72,7 +72,7 @@ object CreateTopics {
replicationFactor: Short = 1, replicationFactor: Short = 1,
topicNames: () -> Collection<String>, topicNames: () -> Collection<String>,
): CreateTopicsResult = ): CreateTopicsResult =
createTopics(topicNames().map { createTopics(topicNames().distinct().map {
NewTopic(it, numPartitions, replicationFactor) NewTopic(it, numPartitions, replicationFactor)
}) })
} }

View file

@ -15,25 +15,29 @@ import dev.adamko.kafkatorio.events.schema.MapTile
import dev.adamko.kafkatorio.events.schema.MapTilePosition 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.rightBottomTile
import dev.adamko.kafkatorio.events.schema.converters.toHexadecimal
import dev.adamko.kafkatorio.events.schema.converters.toMapChunkPosition
import dev.adamko.kotka.extensions.consumedAs import dev.adamko.kotka.extensions.consumedAs
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.producedAs import dev.adamko.kotka.extensions.producedAs
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.flatMap
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.to import dev.adamko.kotka.extensions.streams.to
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
import dev.adamko.kotka.topicdata.mapTopicRecords
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.File import java.io.File
import java.time.Duration import java.time.Duration
import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sign
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -76,7 +80,7 @@ class KafkatorioTopology(
streams.setUncaughtExceptionHandler(StreamsExceptionHandler()) streams.setUncaughtExceptionHandler(StreamsExceptionHandler())
Runtime.getRuntime().addShutdownHook(Thread { streams.close(Duration.ofSeconds(1)) }) Runtime.getRuntime().addShutdownHook(Thread { streams.close(Duration.ofSeconds(10)) })
// streams.cleanUp() // streams.cleanUp()
@ -139,6 +143,10 @@ class KafkatorioTopology(
} }
private fun tilePrototypesTable(): KTable<String, MapTilePrototype> { private fun tilePrototypesTable(): KTable<String, MapTilePrototype> {
// val tilePrototypesStreamName = "kafkatorio.stream.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>()) Consumed.with(Serdes.String(), jsonMapper.serde<FactorioPrototypes>())
@ -158,6 +166,24 @@ class KafkatorioTopology(
jsonMapper.serde() jsonMapper.serde()
) )
) )
// .to(
// tilePrototypesStreamName,
// )
// return builder.globalTable(
// tilePrototypesStreamName,
// consumedAs(
// "send-tile-protos-to-gkt-store",
// jsonMapper.serde(),
// jsonMapper.serde()
// ),
// materializedAs(
// storeName = tilePrototypesStoreName,
// keySerde = jsonMapper.serde(),
// valueSerde = jsonMapper.serde(),
// )
// )
} }
@Serializable @Serializable
@ -168,14 +194,23 @@ class KafkatorioTopology(
@Serializable @Serializable
data class MapChunkData( data class MapChunkData(
val position: MapChunkPosition, val chunkPosition: MapChunkDataPosition,
val surfaceIndex: Int,
val tiles: Set<MapTile>, val tiles: Set<MapTile>,
) : TopicRecord<MapChunkDataPosition> { )
override val topicKey = MapChunkDataPosition(position, surfaceIndex) // : TopicRecord<MapChunkDataPosition>
{
constructor(key: MapChunkDataPosition, tiles: Set<MapTile>) : // @kotlinx.serialization.Transient
this(key.position, key.surfaceIndex, tiles) // override val topicKey by ::chunkPosition
// @kotlinx.serialization.Transient
// val surfaceIndex: Int by chunkPosition::surfaceIndex
// @kotlinx.serialization.Transient
// val leftTopTile = chunkPosition.position.leftTopTile
// @kotlinx.serialization.Transient
// val rightBottom = chunkPosition.position.rightBottomTile
// @kotlinx.serialization.Transient
// val leftBottom = MapTilePosition(leftTopTile.x, rightBottom.x)
} }
private fun mapChunksStream() { private fun mapChunksStream() {
@ -193,6 +228,7 @@ class KafkatorioTopology(
println("MapChunk update $key, tiles count: ${value.tiles.size}") println("MapChunk update $key, tiles count: ${value.tiles.size}")
} }
val chunkTilesUpdateStream: KStream<String, MapTiles> = builder.stream( val chunkTilesUpdateStream: KStream<String, MapTiles> = builder.stream(
"kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}", "kafkatorio.${KafkatorioPacket.PacketType.EVENT}.${FactorioObjectData.ObjectName.MapChunk}",
Consumed.with(Serdes.String(), jsonMapper.serde<FactorioEvent>()) Consumed.with(Serdes.String(), jsonMapper.serde<FactorioEvent>())
@ -238,12 +274,12 @@ class KafkatorioTopology(
val chunksTable: KTable<MapChunkDataPosition, MapChunkData> = val chunksTable: KTable<MapChunkDataPosition, MapChunkData> =
allTileUpdatesStream allTileUpdatesStream
.mapTopicRecords("map-tile-updates-to-MapChunkData") { _: TileUpdateRecordKey, value: TileUpdateRecord -> .map("map-tile-updates-to-MapChunkData") { _: TileUpdateRecordKey, value: TileUpdateRecord ->
val key = MapChunkDataPosition( val key = MapChunkDataPosition(
value.tilePosition.toMapChunkPosition(), value.tilePosition.toMapChunkPosition(),
value.surfaceIndex, value.surfaceIndex,
) )
MapChunkData(key, setOf(value.tile)) key to MapChunkData(key, setOf(value.tile))
} }
// group all tiles by chunk position // group all tiles by chunk position
.groupByKey( .groupByKey(
@ -259,19 +295,18 @@ class KafkatorioTopology(
runCatching { runCatching {
saveMapTilesPng(key, value) saveMapTilesPng(key, value)
}.onFailure { e -> }.onFailure { e ->
println("error saving map tile png ${value.position}") println("error saving map tile png chunk:${value.chunkPosition}")
e.printStackTrace() e.printStackTrace()
throw e throw e
} }
} }
} }
@Suppress("KotlinConstantConditions")
private fun saveMapTilesPng(key: MapChunkDataPosition, chunk: MapChunkData) { private fun saveMapTilesPng(key: MapChunkDataPosition, chunk: MapChunkData) {
val (xChunk, yChunk) = chunk.position.toMapTilePosition()
val xOffset = when (xChunk.sign >= 0) { val chunkOriginX = chunk.chunkPosition.position.leftTopTile.x
true -> {} val chunkOriginY = chunk.chunkPosition.position.rightBottomTile.y
false -> {}
}
val chunkImage = val chunkImage =
ImmutableImage.filled( ImmutableImage.filled(
@ -283,25 +318,28 @@ class KafkatorioTopology(
chunk.tiles.forEach { tile -> chunk.tiles.forEach { tile ->
val prototypeColour = tilePrototypesStore
.get(tile.prototypeName)
?.value()
?.mapColour
?.toHexadecimal()
val prototypeColour = tilePrototypesStore.get(tile.prototypeName) val rgbColour = when (prototypeColour) {
.value() null -> RGBColor(11, 11, 11, 0)
.mapColour else -> RGBColor(
.asHexadecimal()
val rgbColour = RGBColor(
prototypeColour.red.roundToInt(), prototypeColour.red.roundToInt(),
prototypeColour.green.roundToInt(), prototypeColour.green.roundToInt(),
prototypeColour.blue.roundToInt(), prototypeColour.blue.roundToInt(),
prototypeColour.alpha.roundToInt(), prototypeColour.alpha.roundToInt(),
) )
}
val x = (tile.position.x - xChunk) * xChunk.sign val pixelX = abs(abs(tile.position.x) - abs(chunkOriginX))
val y = (tile.position.y - yChunk) * yChunk.sign val pixelY = abs(abs(tile.position.y) - abs(chunkOriginY))
chunkImage.setColor( chunkImage.setColor(
tile.position.x, pixelX,
tile.position.y, pixelY,
rgbColour rgbColour
) )
@ -309,10 +347,10 @@ class KafkatorioTopology(
val zoom = 1u val zoom = 1u
val file = val file =
File("kafkatorio-web-map/${key.surfaceIndex}/$zoom/${key.position.x}/${key.position.y}.png") File("kafkatorio-web-map/s${key.surfaceIndex}/z$zoom/x${key.position.x}/y${key.position.y}.png")
require(file.mkdirs()) { if (file.parentFile.mkdirs()) {
"error creating map tile directory ${file.absolutePath}" println("created new map tile parentFile directory ${file.absolutePath}")
} }
println("saving map tile $file") println("saving map tile $file")

View file

@ -0,0 +1,5 @@
<root>
<item name='org.apache.kafka.streams.state.ReadOnlyKeyValueStore V get(K)'>
<annotation name='org.jetbrains.annotations.Nullable'/>
</item>
</root>

View file

@ -6,31 +6,4 @@ import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.Maps import io.kvision.maps.Maps
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun Colour.toHexString(includeAlpha: Boolean = false): String {
val hexConverter: Float.() -> String = if (isDecimal()) {
Float::decimalToHex
} else {
Float::percentageToHex
}
val length = if (includeAlpha) 9 else 7
return buildString(length) {
append('#')
append(red.hexConverter())
append(green.hexConverter())
append(blue.hexConverter())
if (includeAlpha) {
append(alpha.hexConverter())
}
}
}
fun Float.decimalToHex(): String =
roundToInt().toString(16).padStart(2, ' ')
fun Float.percentageToHex(): String =
times(255).roundToInt().toString(16).padStart(2, ' ')
fun PlayerData.latLng(): LatLng = Maps.L.latLng((position.y * -1) to position.x) fun PlayerData.latLng(): LatLng = Maps.L.latLng((position.y * -1) to position.x)

View file

@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.PlayerData import dev.adamko.kafkatorio.events.schema.PlayerData
import dev.adamko.kafkatorio.events.schema.converters.toHexString
import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
import io.kvision.maps.Maps import io.kvision.maps.Maps