Тест аниматора, RebindableSprite и ещё приборка в GL

This commit is contained in:
DBotThePony 2023-02-21 22:49:25 +07:00
parent 0a00595520
commit 9da968695e
Signed by: DBot
GPG Key ID: DCC23B5715498507
14 changed files with 183 additions and 118 deletions

View File

@ -6,6 +6,8 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.Animator
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.DynamicItemDefinition import ru.dbotthepony.kstarbound.defs.item.DynamicItemDefinition
import ru.dbotthepony.kstarbound.io.BTreeDB import ru.dbotthepony.kstarbound.io.BTreeDB
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
@ -189,6 +191,16 @@ fun main() {
} }
// println(Starbound.statusEffects["firecharge"]) // println(Starbound.statusEffects["firecharge"])
starbound.pathStack.push("/animations/dust4")
val def = starbound.gson.fromJson(starbound.locate("/animations/dust4/dust4.animation").reader(), AnimationDefinition::class.java)
starbound.pathStack.pop()
val animator = Animator(client.world!!, def)
client.onPostDrawWorld {
animator.render(client.gl.matrixStack)
}
} }
//ent.position += Vector2d(y = 14.0, x = -10.0) //ent.position += Vector2d(y = 14.0, x = -10.0)

View File

@ -424,7 +424,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
private val entityRenderers = HashMap<Entity, EntityRenderer>() private val entityRenderers = HashMap<Entity, EntityRenderer>()
override fun onEntityAdded(entity: Entity) { override fun onEntityAdded(entity: Entity) {
entityRenderers[entity] = EntityRenderer.getRender(state, entity, this) entityRenderers[entity] = EntityRenderer.getRender(world.client, entity, this)
} }
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) { override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {

View File

@ -18,15 +18,12 @@ import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.api.IStruct4f
import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.util.* import java.util.*
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -437,61 +434,30 @@ class GLStateTracker(val locator: ISBFileLocator) {
private val named2DTextures = HashMap<String, GLTexture2D>() private val named2DTextures = HashMap<String, GLTexture2D>()
fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { private var missingTexture: GLTexture2D? = null
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
throw FileNotFoundException("Unable to locate $path")
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
}
}
fun loadNamedTexture(path: String): GLTexture2D {
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
throw FileNotFoundException("Unable to locate $path")
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips()
}
}
private var loadedEmptyTexture = false
private val missingTexturePath = "/assetmissing.png" private val missingTexturePath = "/assetmissing.png"
fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { fun loadTexture(path: String): GLTexture2D {
if (!loadedEmptyTexture) { ensureSameThread()
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
}
return named2DTextures.computeIfAbsent(path) { if (missingTexture == null) {
if (!locator.exists(path)) { missingTexture = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) it.textureMinFilter = GL_NEAREST
return@computeIfAbsent named2DTextures[missingTexturePath]!!
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
}
}
fun loadNamedTextureSafe(path: String): GLTexture2D {
if (!loadedEmptyTexture) {
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips()
}
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
return@computeIfAbsent named2DTextures[missingTexturePath]!!
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips().also {
it.textureMagFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST
} }
} }
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
missingTexture!!
} else {
newTexture(path).upload(locator.readDirect(path)).generateMips().also {
it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST
}
}
}
} }
fun bind(obj: VertexBufferObject): VertexBufferObject { fun bind(obj: VertexBufferObject): VertexBufferObject {

View File

@ -13,7 +13,7 @@ class TextureLoadingException(message: String) : Throwable(message)
data class UVCoord(val u: Float, val v: Float) data class UVCoord(val u: Float, val v: Float)
class GLTexturePropertyTracker(private val flag: Int, var value: Int) { private class GLTexturePropertyTracker(private val flag: Int, private var value: Int) {
operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int { operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int {
return value return value
} }
@ -62,8 +62,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
return height.toFloat() / width.toFloat() return height.toFloat() / width.toFloat()
} }
private var mipsWarning = 2
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR) var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR) var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR)
@ -71,15 +69,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT) var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)
fun bind(): GLTexture2D { fun bind(): GLTexture2D {
if (textureMinFilter != GL_LINEAR && textureMinFilter != GL_NEAREST) {
if (mipsWarning == 1) {
LOGGER.warn("(Likely) Trying to use texture {} before generated it's mips, this probably won't work!", this)
mipsWarning = 0
} else if (mipsWarning == 2) {
mipsWarning = 1
}
}
state.texture2D = this state.texture2D = this
return this return this
} }
@ -87,8 +76,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
fun generateMips(): GLTexture2D { fun generateMips(): GLTexture2D {
state.ensureSameThread() state.ensureSameThread()
glGenerateTextureMipmap(pointer) glGenerateTextureMipmap(pointer)
checkForGLError() checkForGLError("Generating texture mipmaps")
mipsWarning = 0
return this return this
} }

View File

@ -1,24 +1,51 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientWorld import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.vector.nfloat.Vector3f
class Animator( class Animator(
val world: ClientWorld, val world: ClientWorld,
val def: AnimationDefinition val def: AnimationDefinition,
val renderParams: ((String) -> String?)? = null
) { ) {
inline val state get() = world.client.gl
val frameAnimator: FrameAnimator? val frameAnimator: FrameAnimator?
val mainSprite: RebindableSprite?
init { init {
if (def.frames != null && def.animationCycle != null && def.frameNumber != null) { if (def.frames != null && def.animationCycle != null && def.frameNumber != null) {
frameAnimator = FrameAnimator(lastFrame = def.frameNumber - 1, time = world.client.time, animationCycle = def.animationCycle) frameAnimator = FrameAnimator(lastFrame = def.frameNumber - 1, time = world.client.time, animationCycle = def.animationCycle)
mainSprite = RebindableSprite(world.client, def.frames, ::getRenderParam)
} else { } else {
frameAnimator = null frameAnimator = null
mainSprite = null
} }
} }
fun render(stack: Matrix4fStack) { private fun getRenderParam(name: String): String? {
if (name == "frame") {
return frameAnimator?.frameString
}
return renderParams?.invoke(name)
}
fun render(stack: Matrix4fStack) {
frameAnimator?.advance()
val sprite = mainSprite?.update() ?: return
sprite.texture.bind()
stack.push().translateWithMultiplication(Vector3f(world.client.camera.pos))
state.programs.textured.use()
state.programs.textured.transform = stack.last
state.activeTexture = 0
state.programs.textured.texture = 0
state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, 0.0, sprite.transformer)
stack.pop()
} }
} }

View File

@ -17,12 +17,12 @@ class BoundSprite(
/** /**
* Настоящая ширина спрайта, в пикселях * Настоящая ширина спрайта, в пикселях
*/ */
inline val width get() = sprite.width(texture.width) val width = sprite.width(texture.width)
/** /**
* Настоящая высота спрайта, в пикселях * Настоящая высота спрайта, в пикселях
*/ */
inline val height get() = sprite.height(texture.height) val height = sprite.height(texture.height)
override val u0: Float override val u0: Float
override val v0: Float override val v0: Float
@ -41,24 +41,3 @@ class BoundSprite(
fun bind() = texture.bind() fun bind() = texture.bind()
val transformer = QuadTransformers.uv(u0, v0, u1, v1) val transformer = QuadTransformers.uv(u0, v0, u1, v1)
} }
/**
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun ImageReference.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(sprite!!, texture)
}
/**
* Создаёт связку текстуры-атласа, которая загружается через [GLStateTracker.loadNamedTextureSafe] + координгат спрайта на ней
*/
fun ImageReference.bind(state: GLStateTracker): BoundSprite {
return BoundSprite(sprite!!, state.loadNamedTextureSafe(imagePath.value!!))
}
/**
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun AtlasConfiguration.Sprite.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(this, texture)
}

View File

@ -32,6 +32,13 @@ class FrameAnimator(
var frame = 0 var frame = 0
private set private set
/**
* Эффективное преобразование [frame] в строку
*/
val frameString: String get() {
return framenames.getOrNull(frame) ?: frame.toString()
}
private var counter = 0.0 private var counter = 0.0
private var lastRender = time.seconds private var lastRender = time.seconds
@ -73,4 +80,14 @@ class FrameAnimator(
lastRender = time.seconds lastRender = time.seconds
} }
} }
companion object {
private val framenames = ArrayList<String>()
init {
for (i in 0 .. 500) {
framenames.add(i.toString())
}
}
}
} }

View File

@ -0,0 +1,50 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.image.ImageReference
class RebindableSprite(
val client: StarboundClient,
ref: ImageReference,
val renderParams: ((String) -> String?)? = null
) {
var sprite: BoundSprite? = null
private set
var ref: ImageReference = ref
private set
init {
val unbound = ref.sprite
if (unbound != null) {
sprite = BoundSprite(unbound, client.gl.loadTexture(ref.imagePath.value!!))
}
}
/**
* Обновляет [ref] и [sprite] по значениям, которые выдаст [renderParams]
*
* Возвращает новое значение [sprite]
*/
fun update(): BoundSprite? {
client.gl.ensureSameThread()
if (renderParams == null)
return sprite
val newRef = ref.with(renderParams)
if (newRef !== ref) {
ref = newRef
sprite = null
val unbound = newRef.sprite
if (unbound != null) {
sprite = BoundSprite(unbound, client.gl.loadTexture(newRef.imagePath.value!!))
}
}
return sprite
}
}

View File

@ -186,7 +186,7 @@ private class ModifierEqualityTester(val definition: MaterialModifier) : Equalit
class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
val state get() = renderers.state val state get() = renderers.state
val texture = state.loadNamedTexture(def.renderParameters.texture.imagePath.value!!).also { val texture = state.loadTexture(def.renderParameters.texture.imagePath.value!!).also {
it.textureMagFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST
} }
@ -255,9 +255,9 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
for (renderPiece in matchPiece.pieces) { for (renderPiece in matchPiece.pieces) {
if (renderPiece.piece.texture != null) { if (renderPiece.piece.texture != null) {
val program = if (background) { val program = if (background) {
renderers.background(state.loadNamedTexture(renderPiece.piece.texture!!)) renderers.background(state.loadTexture(renderPiece.piece.texture!!))
} else { } else {
renderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) renderers.foreground(state.loadTexture(renderPiece.piece.texture!!))
} }
tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier) tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier)

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render.entity
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.matrix.Matrix4fStack
@ -13,7 +14,8 @@ import java.io.Closeable
* *
* Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход) * Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход)
*/ */
open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable { open class EntityRenderer(val client: StarboundClient, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
inline val state: GLStateTracker get() = client.gl
open val renderPos: Vector2d get() = entity.position open val renderPos: Vector2d get() = entity.position
open fun render(stack: Matrix4fStack) { open fun render(stack: Matrix4fStack) {
@ -39,20 +41,20 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
*/ */
const val Z_LEVEL_ENTITIES = 30000 const val Z_LEVEL_ENTITIES = 30000
private val renderers = Reference2ObjectOpenHashMap<Class<*>, (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>() private val renderers = Reference2ObjectOpenHashMap<Class<*>, (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) { fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
check(renderers.put(clazz, renderer as (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" } check(renderers.put(clazz, renderer as (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" }
} }
inline fun <reified T : Entity> registerRenderer(noinline renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) { inline fun <reified T : Entity> registerRenderer(noinline renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
registerRenderer(T::class.java, renderer) registerRenderer(T::class.java, renderer)
} }
fun getRender(state: GLStateTracker, entity: Entity, chunk: ClientChunk? = null): EntityRenderer { fun getRender(client: StarboundClient, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
val factory = renderers[entity::class.java] ?: return EntityRenderer(state, entity, chunk) val factory = renderers[entity::class.java] ?: return EntityRenderer(client, entity, chunk)
return factory.invoke(state, entity, chunk) return factory.invoke(client, entity, chunk)
} }
init { init {

View File

@ -2,14 +2,14 @@ package ru.dbotthepony.kstarbound.client.render.entity
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.bind import ru.dbotthepony.kstarbound.client.render.RebindableSprite
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.matrix.Matrix4fStack
class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) { class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) {
private val def = entity.def private val def = entity.def
private val textures = def.inventoryIcon?.stream()?.map { it.image.bind(state) }?.toList() ?: listOf() private val textures = def.inventoryIcon?.stream()?.map { RebindableSprite(client, it.image) }?.toList() ?: listOf()
override fun render(stack: Matrix4fStack) { override fun render(stack: Matrix4fStack) {
if (textures.isEmpty()) if (textures.isEmpty())
@ -20,10 +20,10 @@ class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk
state.activeTexture = 0 state.activeTexture = 0
state.programs.textured.texture = 0 state.programs.textured.texture = 0
for (texture in textures) { for (unbound in textures) {
texture.bind() val sprite = unbound.update() ?: continue
sprite.texture.bind()
state.flat2DTexturedQuads.singleSprite(texture.width / PIXELS_IN_STARBOUND_UNITf, texture.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, texture.transformer) state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, sprite.transformer)
} }
} }
} }

View File

@ -58,16 +58,25 @@ class AtlasConfiguration private constructor(
} }
} }
/**
* @return [Sprite] если он существует с данным [name], или `null`
*/
operator fun get(name: String): Sprite? { operator fun get(name: String): Sprite? {
return sprites[name] return sprites[name]
} }
/**
* @return [Sprite] если он существует с данным [name], или `null`
*/
operator fun get(name: Int): Sprite? { operator fun get(name: Int): Sprite? {
return intIndexed[name] return intIndexed[name]
} }
/**
* @return [Sprite] если он существует с данным [name], или первый спрайт в атласе
*/
fun any(name: String): Sprite { fun any(name: String): Sprite {
return get(name) ?: first return get(name) ?: any()
} }
private val any by lazy { get("root") ?: get("0") ?: get("default") ?: first } private val any by lazy { get("root") ?: get("0") ?: get("default") ?: first }

View File

@ -13,7 +13,7 @@ import ru.dbotthepony.kstarbound.util.PathStack
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
/** /**
* @see [AtlasConfiguration.Companion.get] * @see [AtlasConfiguration.Registry.get]
*/ */
class ImageReference private constructor( class ImageReference private constructor(
val raw: DirectAssetReference, val raw: DirectAssetReference,
@ -26,8 +26,9 @@ class ImageReference private constructor(
* Спрайт, на которое ссылается данный референс, или `null` если: * Спрайт, на которое ссылается данный референс, или `null` если:
* * [atlas] равен `null` * * [atlas] равен `null`
* * [spritePath] является шаблоном и определены не все значения * * [spritePath] является шаблоном и определены не все значения
* * [spritePath] не является правильным именем спрайта внутри [atlas] (смотрим [AtlasConfiguration.get])
*/ */
val sprite by lazy { val sprite by lazy(LazyThreadSafetyMode.NONE) {
if (atlas == null) if (atlas == null)
null null
else if (spritePath == null) else if (spritePath == null)
@ -38,7 +39,15 @@ class ImageReference private constructor(
fun with(values: (String) -> String?): ImageReference { fun with(values: (String) -> String?): ImageReference {
val imagePath = this.imagePath.with(values) val imagePath = this.imagePath.with(values)
val spritePath = this.spritePath?.with(values) var spritePath = this.spritePath?.with(values)
if (spritePath == null) {
val frame = values.invoke("frame")
if (frame != null) {
spritePath = SBPattern.FRAME.with(values)
}
}
if (imagePath != this.imagePath || spritePath != this.spritePath) { if (imagePath != this.imagePath || spritePath != this.spritePath) {
if (imagePath != this.imagePath) { if (imagePath != this.imagePath) {

View File

@ -52,8 +52,11 @@ class SBPattern private constructor(
} }
fun resolve(values: (String) -> String?): String? { fun resolve(values: (String) -> String?): String? {
if (names.isEmpty()) if (names.isEmpty()) {
return raw return raw
} else if (pieces.size == 1) {
return pieces[0].resolve(values, params::get)
}
val buffer = ArrayList<String>(pieces.size) val buffer = ArrayList<String>(pieces.size)
@ -116,6 +119,9 @@ class SBPattern private constructor(
@JvmField @JvmField
val EMPTY = raw("") val EMPTY = raw("")
@JvmField
val FRAME = of("<frame>")
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == SBPattern::class.java) { if (type.rawType == SBPattern::class.java) {
return object : TypeAdapter<SBPattern>() { return object : TypeAdapter<SBPattern>() {
@ -126,7 +132,7 @@ class SBPattern private constructor(
} }
override fun read(`in`: JsonReader): SBPattern? { override fun read(`in`: JsonReader): SBPattern? {
return interner.intern(of(strings.read(`in`) ?: return null)) return of(strings.read(`in`) ?: return null)
} }
} as TypeAdapter<T> } as TypeAdapter<T>
} }
@ -159,7 +165,7 @@ class SBPattern private constructor(
throw IllegalArgumentException("Malformed pattern string: $raw") throw IllegalArgumentException("Malformed pattern string: $raw")
} }
pieces.add(Piece(name = raw.substring(open + 1, closing - 1))) pieces.add(Piece(name = raw.substring(open + 1, closing)))
i = closing + 1 i = closing + 1
} }
} }