diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 50a863a9..0df4979c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -15,6 +15,7 @@ import ru.dbotthepony.kstarbound.api.NonExistingFile import ru.dbotthepony.kstarbound.api.PhysicalFile import ru.dbotthepony.kstarbound.api.explore import ru.dbotthepony.kstarbound.defs.* +import ru.dbotthepony.kstarbound.defs.animation.SpriteReference import ru.dbotthepony.kstarbound.defs.item.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition @@ -32,6 +33,7 @@ import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter +import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABBi @@ -135,12 +137,13 @@ object Starbound { .registerTypeAdapter(stringTypeAdapter) // math - .registerTypeAdapter(AABB::class.java, AABBTypeAdapter) - .registerTypeAdapter(AABBi::class.java, AABBiTypeAdapter) - .registerTypeAdapter(Vector2d::class.java, Vector2dTypeAdapter) - .registerTypeAdapter(Vector2f::class.java, Vector2fTypeAdapter) - .registerTypeAdapter(Vector2i::class.java, Vector2iTypeAdapter) - .registerTypeAdapter(Poly::class.java, PolyTypeAdapter) + .registerTypeAdapter(AABBTypeAdapter) + .registerTypeAdapter(AABBiTypeAdapter) + .registerTypeAdapter(Vector2dTypeAdapter) + .registerTypeAdapter(Vector2fTypeAdapter) + .registerTypeAdapter(Vector2iTypeAdapter) + .registerTypeAdapter(Vector4iTypeAdapter) + .registerTypeAdapter(PolyTypeAdapter) .also(ConfigurableProjectile::registerGson) .also(SkyParameters::registerGson) @@ -154,6 +157,7 @@ object Starbound { .also(LiquidDefinition::registerGson) .also(ItemDefinition::registerGson) .also(ItemRarity::registerGson) + .also(SpriteReference::registerGson) .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt index acf2db15..8322b1cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt @@ -28,6 +28,10 @@ interface IStarboundFile { fun orNull(): IStarboundFile? = if (exists) this else null + operator fun get(name: String): IStarboundFile? { + return children?.get(name) + } + fun computeFullPath(): String { var path = name var parent = parent diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt index 0a909f03..bae774ea 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.client.render.entity -import org.lwjgl.opengl.GL46 import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.gl.GLStateTracker @@ -11,28 +10,31 @@ import ru.dbotthepony.kvector.matrix.Matrix4fStack class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) { private val def = entity.def - private val texture = def.inventoryIcon?.let(state::loadNamedTextureSafe) + private val textures = def.inventoryIcon?.stream()?.map { state.loadNamedTextureSafe(it.image.path) }?.toList() ?: listOf() override fun render(stack: Matrix4fStack) { - if (texture == null) + if (textures.isEmpty()) return state.shaderVertexTexture.use() state.shaderVertexTexture.transform.set(stack.last) state.activeTexture = 0 state.shaderVertexTexture["_texture"] = 0 - texture.bind() - val builder = state.flat2DTexturedQuads.small + for (texture in textures) { + texture.bind() - builder.begin() + val builder = state.flat2DTexturedQuads.small - val width = (texture.width / PIXELS_IN_STARBOUND_UNITf) / 2f - val height = (texture.height / PIXELS_IN_STARBOUND_UNITf) / 2f + builder.begin() - builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, QuadTransformers.uv(0f, 1f, 1f, 0f)) + val width = (texture.width / PIXELS_IN_STARBOUND_UNITf) / 2f + val height = (texture.height / PIXELS_IN_STARBOUND_UNITf) / 2f - builder.upload() - builder.draw() + builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, QuadTransformers.uv(0f, 1f, 1f, 0f)) + + builder.upload() + builder.draw() + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AtlasDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AtlasDefinition.kt new file mode 100644 index 00000000..2c312072 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AtlasDefinition.kt @@ -0,0 +1,233 @@ +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 + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/IUVCoordinates.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/IUVCoordinates.kt new file mode 100644 index 00000000..644af650 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/IUVCoordinates.kt @@ -0,0 +1,26 @@ +package ru.dbotthepony.kstarbound.defs.animation + +import ru.dbotthepony.kvector.api.IStruct4f + +interface IUVCoordinates : IStruct4f { + val u0: Float + val v0: Float + val u1: Float + val v1: Float + + override fun component1(): Float { + return u0 + } + + override fun component2(): Float { + return v0 + } + + override fun component3(): Float { + return u1 + } + + override fun component4(): Float { + return v1 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/SpriteReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/SpriteReference.kt new file mode 100644 index 00000000..32725582 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/SpriteReference.kt @@ -0,0 +1,37 @@ +package ru.dbotthepony.kstarbound.defs.animation + +import com.google.gson.GsonBuilder +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.registerTypeAdapter + +data class SpriteReference( + val path: String, + val sprite: AtlasDefinition.Sprite +) { + companion object : TypeAdapter() { + fun parse(input: String): SpriteReference { + val grid = AtlasDefinition.get(input.substringBefore(':')) + + return when (input.count { it == ':' }) { + 0 -> SpriteReference(input, grid.any()) + 1 -> SpriteReference(input.substringBefore(':'), grid.get(input.substringAfter(':')) ?: throw NoSuchElementException("No such sprite with name ${input.substringAfter(':')} present in frame grid ${grid.name} (atlas ${input.substringBefore(':')})")) + else -> throw IllegalArgumentException("Invalid sprite reference: $input") + } + } + + override fun write(out: JsonWriter, value: SpriteReference) { + out.value(value.path + ":" + value.sprite.name) + } + + override fun read(`in`: JsonReader): SpriteReference { + return parse(Starbound.readingFolderTransformer(`in`.nextString())) + } + + fun registerGson(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(this) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/UVCoordinates.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/UVCoordinates.kt new file mode 100644 index 00000000..e91dd61a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/UVCoordinates.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound.defs.animation + +data class UVCoordinates( + override val u0: Float, + override val v0: Float, + override val u1: Float, + override val v1: Float, +) : IUVCoordinates diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt index 53f6f747..0b44fd16 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt @@ -1,12 +1,10 @@ package ru.dbotthepony.kstarbound.defs.item import com.google.gson.GsonBuilder -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.animation.SpriteReference import ru.dbotthepony.kstarbound.io.json.KConcreteTypeAdapter +import ru.dbotthepony.kstarbound.io.json.ListAdapter import ru.dbotthepony.kstarbound.io.json.asJsonObject import ru.dbotthepony.kstarbound.io.json.asList import ru.dbotthepony.kstarbound.io.json.ifString @@ -37,7 +35,7 @@ data class ItemDefinition( /** * Иконка в инвентаре, относительный и абсолютный пути */ - val inventoryIcon: String? = null, + val inventoryIcon: List? = null, /** * Описание предмета @@ -260,13 +258,21 @@ data class ItemDefinition( val amount: Double = 0.0, ) + data class InventoryIcon( + val image: SpriteReference + ) + companion object { + val INVENTORY_ICON_ADAPTER = KConcreteTypeAdapter.Builder(InventoryIcon::class) + .auto(InventoryIcon::image) + .build() + val ADAPTER = KConcreteTypeAdapter.Builder(ItemDefinition::class) .auto(ItemDefinition::itemName) .auto(ItemDefinition::price) .auto(ItemDefinition::rarity) .auto(ItemDefinition::category) - .auto(ItemDefinition::inventoryIcon, Starbound::readingFolderTransformerNullable) + .add(ItemDefinition::inventoryIcon, ListAdapter(INVENTORY_ICON_ADAPTER).ifString { listOf(InventoryIcon(SpriteReference.parse(Starbound.readingFolderTransformer(it)))) }.nullSafe()) .auto(ItemDefinition::description) .auto(ItemDefinition::shortdescription) @@ -342,6 +348,7 @@ data class ItemDefinition( gsonBuilder.registerTypeAdapter(FOSSIL_ADAPTER) gsonBuilder.registerTypeAdapter(ARMOR_FRAMES_ADAPTER) gsonBuilder.registerTypeAdapter(STATUS_EFFECT_ADAPTER) + gsonBuilder.registerTypeAdapter(INVENTORY_ICON_ADAPTER) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/JsonArraySpliterator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/JsonArraySpliterator.kt new file mode 100644 index 00000000..cd53f684 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/JsonArraySpliterator.kt @@ -0,0 +1,30 @@ +package ru.dbotthepony.kstarbound.io.json + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import it.unimi.dsi.fastutil.objects.ObjectSpliterator +import it.unimi.dsi.fastutil.objects.ObjectSpliterators +import java.util.stream.Stream +import java.util.stream.StreamSupport + +class JsonArraySpliterator(private val obj: JsonArray, offset: Int = 0, private val maxPos: Int = obj.size()) : ObjectSpliterators.AbstractIndexBasedSpliterator(offset) { + init { + require(offset >= 0) { "Invalid offset $offset" } + require(offset + maxPos <= obj.size()) { "$offset -> $maxPos while having only size of ${obj.size()}!" } + } + + override fun get(location: Int): JsonElement { + return obj[location] + } + + override fun getMaxPos(): Int { + return maxPos + } + + override fun makeForSplit(pos: Int, maxPos: Int): ObjectSpliterator { + return JsonArraySpliterator(obj, pos, maxPos) + } +} + +fun JsonArray.elementSpliterator() = JsonArraySpliterator(this) +fun JsonArray.stream(): Stream = StreamSupport.stream(elementSpliterator(), false) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/VectorJson.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/VectorJson.kt index 9e5f7aff..cd526183 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/VectorJson.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/VectorJson.kt @@ -6,6 +6,31 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.nfloat.Vector2f import ru.dbotthepony.kvector.vector.nint.Vector2i +import ru.dbotthepony.kvector.vector.nint.Vector4i + +object Vector4iTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Vector4i) { + `out`.beginArray() + `out`.value(value.x) + `out`.value(value.y) + `out`.value(value.z) + `out`.value(value.w) + `out`.endArray() + } + + override fun read(`in`: JsonReader): Vector4i { + `in`.beginArray() + + val x = `in`.nextInt() + val y = `in`.nextInt() + val z = `in`.nextInt() + val w = `in`.nextInt() + + `in`.endArray() + + return Vector4i(x, y, z, w) + } +} object Vector2iTypeAdapter : TypeAdapter() { override fun write(out: JsonWriter, value: Vector2i) {