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, val rules: ImmutableList, 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 = ImmutableList.of(), val rules: ImmutableList = 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, 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() { private val objects = gson.getAdapter(JsonObject::class.java) private val data = gson.getAdapter(BasicData::class.java) private val values = gson.getAdapter>() 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 = 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)) } }