KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt

151 lines
5.4 KiB
Kotlin

package ru.dbotthepony.kstarbound.defs.dungeon
import com.github.benmanes.caffeine.cache.Interner
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector4i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.server.world.ServerWorld
@JsonAdapter(DungeonTile.Adapter::class)
data class DungeonTile(
val brushes: ImmutableList<DungeonBrush>,
val rules: ImmutableList<DungeonRule>,
val index: Int,
val connector: JigsawConnector?,
) {
data class JigsawConnector(
val index: String,
val forward: Boolean,
val direction: DungeonDirection
)
@JsonFactory
data class BasicData(
val brush: ImmutableList<DungeonBrush> = ImmutableList.of(),
val rules: ImmutableList<DungeonRule> = ImmutableList.of(),
val direction: DungeonDirection = DungeonDirection.UNKNOWN,
// original engine supports specifying pixels as strings
// but I don't see it being used anywhere in original game assets
// /shrug lets support anyway
val value: Either<Vector4i, String>,
val connector: Boolean = false,
val connectForwardOnly: Boolean = false,
)
// whenever this tile CAN BE overdrawn by other tile
val allowOverdrawing: Boolean = rules.any { it.allowOverdrawing }
// "modifyPlaces" in original code
val hasBrushes: Boolean get() = brushes.isNotEmpty()
val hasRules: Boolean get() = rules.isNotEmpty()
val usesPlaces: Boolean get() = hasBrushes && !allowOverdrawing
val collidesWithPlaces: Boolean get() = usesPlaces
val requiresOpen: Boolean get() = rules.any { it.requiresOpen }
val requiresLiquid: Boolean get() = rules.any { it.requiresLiquid }
val requiresSolid: Boolean get() = rules.any { it.requiresSolid }
// empty tiles can be cut off from canPlace and place loops
// (since they don't do anything other than padding region coordinates)
val isEmpty: Boolean get() = !hasBrushes && !hasRules
// sucks that we have to join world thread when doing this check
// this hinders parallelized computations by noticeable margin,
// and also makes dungeon placement in world completely serial
// (one dungeon can be in process of generating at time)
// TODO: find a way around this, to make dungeons less restricted by this
// but thats also not a priority, since this check happens quite quickly
// to have any noticeable impact on world's performance
fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
val cell = world.parent.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID)
return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false
return rules.none { !it.checkTileCanPlace(x, y, world) }
}
fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID)
return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false
return rules.none { !it.checkTileCanPlace(x, y, world) }
}
fun place(x: Int, y: Int, phase: DungeonBrush.Phase, world: DungeonWorld) {
brushes.forEach { it.execute(x, y, phase, world) }
}
// weird custom parsing rules but ok
class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() {
private val objects = gson.getAdapter(JsonObject::class.java)
private val data = gson.getAdapter(BasicData::class.java)
private val values = gson.getAdapter<Either<Vector4i, String>>()
private fun parseIndex(value: String): Int {
val split = value.split(',')
require(split.size == 4) { "Invalid color string: $value" }
var index = 0
for (v in split.map { it.toInt() }.asReversed())
index = index.shl(8) or v.and(0xFF)
return index
}
override fun write(out: JsonWriter, value: DungeonTile) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): DungeonTile {
val read = objects.read(`in`)
val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read)
var connectorIndex = rawIndex
if ("connector-value" in read) {
connectorIndex = values.fromJsonTree(read["connector-value"])
}
val index = rawIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) })
if (connector) {
return INTERNER.intern(DungeonTile(brushes, rules, index, JigsawConnector(
index = connectorIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) }).toString(),
forward = connectForwardOnly,
direction = direction
)))
} else {
return INTERNER.intern(DungeonTile(brushes, rules, index, null))
}
}
}
companion object {
val INTERNER: Interner<DungeonTile> = if (Starbound.DEDUP_CELL_STATES) Starbound.interner(5) else Interner { it }
val EMPTY: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(), ImmutableList.of(), 0, null))
val CLEAR: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(DungeonBrush.Clear), ImmutableList.of(), 0, null))
}
}