234 lines
7.3 KiB
Kotlin
234 lines
7.3 KiB
Kotlin
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
|
||
}
|
||
}
|
||
}
|