KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AtlasDefinition.kt

234 lines
7.3 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, Sprite>,
) {
/**
* Первый спрайт, по своему имени (естественная сортировка)
*/
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<String, AtlasDefinition>()
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<String, Sprite>()
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<Sprite>()
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
}
}
}