150 lines
5.4 KiB
Kotlin
150 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 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 = Starbound.ELEMENTS_ADAPTER.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))
|
|
}
|
|
}
|
|
|