package ru.dbotthepony.kstarbound.defs.animation import com.google.common.collect.ImmutableMap import com.google.gson.JsonArray import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonReader import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.GLTexture2D import ru.dbotthepony.kstarbound.io.json.stream import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector4i import java.util.concurrent.ConcurrentHashMap /** * Атлас спрайтов, собранный вручную артистом * * В файлах игры именуется frames */ class AtlasDefinition private constructor( val name: String, /** * Спрайты данного атласа, включая спрайты под псевдонимами */ val sprites: ImmutableMap, ) { /** * Первый спрайт, по своему имени (естественная сортировка) */ val first: Sprite = sprites[sprites.keys.stream().sorted().findFirst().orElseThrow { NoSuchElementException("No a single key present in $name") }] ?: throw NoSuchElementException("IMPOSSIBRU in $name") operator fun get(name: String): Sprite? { return sprites[name] } fun any(name: String): Sprite { return get(name) ?: first } fun any(): Sprite { return get("root") ?: get("0") ?: first } class Sprite( /** * Имя данного спрайта, на которое прототипы могут ссылаться * * К примеру, если имя будет указано как * * **`active.2`** * * а сам атлас называется * * **`mything.png`** * * то прототипы могут ссылаться на данный спрайт как * * **`mything.png:active.2`** */ val name: String, /** * Позиция и размеры данного спрайта, в пикселях */ val position: Vector4i, ) { /** * Вычисляет uv координаты данного спрайта на заданном полотне */ fun compute(width: Int, height: Int): UVCoordinates { return UVCoordinates( u0 = position.x.toFloat() / width.toFloat(), v0 = position.y.toFloat() / height.toFloat(), u1 = position.z.toFloat() / width.toFloat(), v1 = position.w.toFloat() / height.toFloat(), ) } /** * Вычисляет uv координаты данного спрайта на заданном полотне */ fun compute(dimensions: Vector2i): UVCoordinates { return compute(dimensions.x, dimensions.y) } /** * Вычисляет uv координаты данного спрайта на заданном полотне */ fun compute(texture: GLTexture2D): UVCoordinates { return compute(texture.width, texture.height) } override fun toString(): String { return "FrameGrid.Sprite[name=$name, position=$position]" } } companion object { val EMPTY = AtlasDefinition("null", ImmutableMap.of("root", Sprite("root", Vector4i(0, 0, 1, 1)))) private val cache = ConcurrentHashMap() private fun generateFakeNames(dimensions: Vector2i): JsonArray { return JsonArray(dimensions.y).also { var stripElem = 0 for (stripNum in 0 until dimensions.y) { val strip = JsonArray(dimensions.x) for (i in 0 until dimensions.x) { strip.add(stripElem.toString()) stripElem++ } it.add(strip) } } } private fun parseFrames(input: JsonReader, name: String): AtlasDefinition { val read = TypeAdapters.JSON_ELEMENT.read(input) if (read !is JsonObject) { throw JsonSyntaxException("Expected to have object as top most element, $read given") } val frameGrid = read["frameGrid"] val sprites = HashMap() if (frameGrid is JsonObject) { val size = Starbound.gson.fromJson(frameGrid["size"] ?: throw JsonSyntaxException("Missing frameGrid.size"), Vector2i::class.java) val dimensions = Starbound.gson.fromJson(frameGrid["dimensions"] ?: throw JsonSyntaxException("Missing frameGrid.dimensions"), Vector2i::class.java) require(size.x >= 0) { "Invalid size.x: ${size.x}" } require(size.y >= 0) { "Invalid size.y: ${size.y}" } require(dimensions.x >= 0) { "Invalid dimensions.x: ${dimensions.x}" } require(dimensions.y >= 0) { "Invalid dimensions.y: ${dimensions.y}" } val names = (frameGrid["names"] as? JsonArray ?: generateFakeNames(dimensions)) .stream().map { (it as? JsonArray)?.stream()?.map { if (it == JsonNull.INSTANCE) null else it.asString }?.toList() }.toList() val spriteList = ArrayList() for ((y, strip) in names.withIndex()) { // разрешаем вставлять null как ленту кадров, что означает что мы должны пропустить её полностью if (strip == null) continue for ((x, spriteName) in strip.withIndex()) { // если кадр не имеет имени... if (spriteName == null) continue spriteList.add(Sprite(spriteName, Vector4i(x * size.x, y * size.y, (x + 1) * size.x, (y + 1) * size.y))) } } for (sprite in spriteList) sprites[sprite.name] = sprite } else if (frameGrid != null) { throw JsonSyntaxException("Unexpected frameGrid element: $frameGrid") } else { val frameList = read["frameList"] ?: throw JsonSyntaxException("Frame grid must have either frameGrid or frameList object defined") if (frameList !is JsonObject) { throw JsonSyntaxException("frameList is not an object") } for ((spriteName, coords) in frameList.entrySet()) { sprites[spriteName] = Sprite(spriteName, Starbound.gson.fromJson(coords, Vector4i::class.java)) } } val aliases = read["aliases"] if (aliases != null) { if (aliases !is JsonObject) throw JsonSyntaxException("aliases expected to be a Json object, $aliases given") for ((k, v) in aliases.entrySet()) sprites[k] = sprites[v.asString] ?: throw JsonSyntaxException("$k want to refer to sprite $v, but it does not exist") } return AtlasDefinition(name, ImmutableMap.copyOf(sprites)) } private fun recursiveGet(name: String, folder: String): AtlasDefinition? { var current = folder while (current != "/" && current != "") { val get = cache.computeIfAbsent("$current/$name") { val file = Starbound.locate("$it.frames") if (file.exists) { try { return@computeIfAbsent parseFrames(JsonReader(file.reader()), "$it.frames") } catch (err: Throwable) { throw JsonSyntaxException("Reading frame grid $it.frames") } } return@computeIfAbsent EMPTY } if (get !== EMPTY) { return get } current = current.substringBeforeLast('/') } return null } fun get(path: String): AtlasDefinition { require(path[0] == '/') { "$path is not an absolute path" } val folder = path.substringBeforeLast('/').lowercase() val filename = path.substringAfterLast('/').substringBefore('.').lowercase() val direct = recursiveGet(filename, folder) if (direct != null) return direct val default = recursiveGet("default", folder) if (default != null) return default return EMPTY } } }