diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGenerator.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGenerator.kt index 5e799dfcab..5898a8de12 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGenerator.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGenerator.kt @@ -26,24 +26,34 @@ class TypedResourcesGenerator { fun generateForFolders(resourcesFolder: SFile): String { return Indenter { - line("import korlibs.image.atlas.Atlas") - line("import korlibs.io.file.VfsFile") - line("import korlibs.io.file.std.resourcesVfs") - line("import korlibs.image.atlas.readAtlas") - line("import korlibs.audio.sound.readSound") - line("import korlibs.image.format.readBitmap") + line("import korlibs.audio.sound.*") + line("import korlibs.io.file.*") + line("import korlibs.io.file.std.*") + line("import korlibs.image.bitmap.*") + line("import korlibs.image.atlas.*") + line("import korlibs.image.font.*") + line("import korlibs.image.format.*") line("") line("// AUTO-GENERATED FILE! DO NOT MODIFY!") line("") line("@Retention(AnnotationRetention.BINARY) annotation class ResourceVfsPath(val path: String)") line("inline class TypedVfsFile(val __file: VfsFile)") - line("inline class TypedVfsFileBitmap(val __file: VfsFile) { suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap() }") - line("inline class TypedVfsFileSound(val __file: VfsFile) { suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound() }") + line("inline class TypedVfsFileTTF(val __file: VfsFile) {") + line(" suspend fun read(): korlibs.image.font.TtfFont = this.__file.readTtfFont()") + line("}") + line("inline class TypedVfsFileBitmap(val __file: VfsFile) {") + line(" suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap()") + line(" suspend fun readSlice(atlas: MutableAtlasUnit? = null, name: String? = null): BmpSlice = this.__file.readBitmapSlice(name, atlas)") + line("}") + line("inline class TypedVfsFileSound(val __file: VfsFile) {") + line(" suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound()") + line("}") line("interface TypedAtlas") - data class AtlasInfo(val file: SFile, val className: String) + data class ExtraInfo(val file: SFile, val className: String) - val atlases = arrayListOf() + val atlases = arrayListOf() + val ases = arrayListOf() val exploredFolders = LinkedHashSet() val foldersToExplore = ArrayDeque() @@ -78,11 +88,17 @@ class TypedResourcesGenerator { val type: String? = when (extension) { "png", "jpg" -> "TypedVfsFileBitmap" "mp3", "wav" -> "TypedVfsFileSound" + "ttf", "otf" -> "TypedVfsFileTTF" + "ase" -> { + val className = "Ase${fullVarName.textCase().pascalCase()}" + ases += ExtraInfo(file, className) + "$className.TypedAse" + } "atlas" -> { if (isDirectory) { extraSuffix += ".json" val className = "Atlas${fullVarName.textCase().pascalCase()}" - atlases += AtlasInfo(file, className) + atlases += ExtraInfo(file, className) "$className.TypedAtlas" } else { "TypedVfsFile" @@ -123,6 +139,51 @@ class TypedResourcesGenerator { } } } + + for (ase in ases) { + line("") + line("inline class ${ase.className}(val data: korlibs.image.format.ImageDataContainer)") { + line("inline class TypedAse(val __file: VfsFile) { suspend fun read(atlas: korlibs.image.atlas.MutableAtlasUnit? = null): ${ase.className} = ${ase.className}(this.__file.readImageDataContainer(korlibs.image.format.ASE.toProps(), atlas)) }") + val aseFile = ase.file + + try { + val info = ASEInfo.getAseInfo(ase.file.readBytes()) + + line("enum class TypedAnimation(val animationName: String)") { + for (tag in info.tags) { + line("${tag.tagName.nameToVariable().uppercase()}(${tag.tagName.quoted}),") + } + line(";") + line("companion object") { + line("val list: List = values().toList()") + for (tag in info.tags) { + line("val ${tag.tagName.nameToVariable().lowercase()}: TypedAnimation get() = TypedAnimation.${tag.tagName.nameToVariable().uppercase()}") + } + } + } + + line("inline class TypedImageData(val data: ImageData)") { + line("val animations: TypedAnimation.Companion get() = TypedAnimation") + } + + line("val animations: TypedAnimation.Companion get() = TypedAnimation") + line("val default: TypedImageData get() = TypedImageData(data.default)") + for (slice in info.slices) { + val varName = slice.sliceName.nameToVariable() + line("val `$varName`: TypedImageData get() = TypedImageData(data[${slice.sliceName.quoted}]!!)") + } + // @TODO: We could + + //println("wizardFemale=${wizardFemale.imageDatasByName.keys}") + //println("wizardFemale.animations=${wizardFemale.imageDatas.first().animationsByName.keys}") + } catch (e: Throwable) { + System.err.println("FILE: aseFile=$aseFile") + e.printStackTrace() + + throw e // @TODO: Remove this + } + } + } } } } diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/ASEInfo.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/ASEInfo.kt new file mode 100644 index 0000000000..62aca6ace1 --- /dev/null +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/ASEInfo.kt @@ -0,0 +1,129 @@ +package korlibs.korge.gradle.util + +data class ASEInfo( + val slices: List = emptyList(), + val tags: List = emptyList(), +) { + data class AseSlice( + val sliceName: String, + val hasNinePatch: Boolean, + val hasPivotInfo: Boolean, + ) + + data class AseTag( + val fromFrame: Int, + val toFrame: Int, + val direction: Int, + val tagColor: Int, + val tagName: String + ) + + companion object { + fun getAseInfo(file: SFile): ASEInfo { + return getAseInfo(file.readBytes()) + } + + fun getAseInfo(data: ByteArray): ASEInfo { + return getAseInfo(ByteArraySimpleInputStream(ByteArraySlice(data))) + } + + fun getAseInfo(s: ByteArraySimpleInputStream): ASEInfo { + if (s.length == 0) return ASEInfo() + + val slices = arrayListOf() + val tags = arrayListOf() + + val fileSize = s.readS32LE() + if (s.length < fileSize) error("File too short") + val headerMagic = s.readU16LE() + if (headerMagic != 0xA5E0) error("Not an Aseprite file : headerMagic=$headerMagic") + val numFrames = s.readU16LE() + val imageWidth = s.readU16LE() + val imageHeight = s.readU16LE() + val bitsPerPixel = s.readU16LE() + val bytesPerPixel = bitsPerPixel / 8 + val flags = s.readU32LE() + val speed = s.readU16LE() + s.skip(4) + s.skip(4) + val transparentIndex = s.readU8() + s.skip(3) + val numColors = s.readU16LE() + val pixelWidth = s.readU8() + val pixelHeight = s.readU8() + val gridX = s.readS16LE() + val gridY = s.readS16LE() + val gridWidth = s.readU16LE() + val gridHeight = s.readU16LE() + s.skip(84) + + //println("ASE fileSize=$fileSize, headerMagic=$headerMagic, numFrames=$numFrames, $imageWidth x $imageHeight, bitsPerPixel=$bitsPerPixel, numColors=$numColors, gridWidth=$gridWidth, gridHeight=$gridHeight") + + for (frameIndex in 0 until numFrames) { + //println("FRAME: $frameIndex") + val bytesInFrame = s.readS32LE() + val fs = s.readStream(bytesInFrame - 4) + val frameMagic = fs.readU16LE() + //println(" bytesInFrame=$bytesInFrame, frameMagic=$frameMagic") + if (frameMagic != 0xF1FA) error("Invalid ASE sprite file or error parsing : frameMagic=$frameMagic") + fs.readU16LE() + val frameDuration = fs.readU16LE() + fs.skip(2) + val numChunks = fs.readS32LE() + + //println(" - $numChunks") + + for (nc in 0 until numChunks) { + val chunkSize = fs.readS32LE() + val chunkType = fs.readU16LE() + val cs = fs.readStream(chunkSize - 6) + + //println(" chunkType=$chunkType, chunkSize=$chunkSize") + + when (chunkType) { + 0x2022 -> { // SLICE KEYS + val numSliceKeys = cs.readS32LE() + val sliceFlags = cs.readS32LE() + cs.skip(4) + val sliceName = cs.readAseString() + val hasNinePatch = sliceFlags.hasBitSet(0) + val hasPivotInfo = sliceFlags.hasBitSet(1) + val aslice = AseSlice(sliceName, hasNinePatch, hasPivotInfo) + slices += aslice + } + 0x2018 -> { // TAGS + // Tags + val numTags = cs.readU16LE() + cs.skip(8) + //println(" tags: numTags=$numTags") + + for (tag in 0 until numTags) { + val fromFrame = cs.readU16LE() + val toFrame = cs.readU16LE() + val direction = cs.readU8() + cs.skip(8) + val tagColor = cs.readS32LE() + val tagName = cs.readAseString() + val atag = AseTag(fromFrame, toFrame, direction, tagColor, tagName) + tags += atag + //println(" tag[$tag]=$atag") + } + } + // Unsupported tag + else -> { + + } + } + } + } + + return ASEInfo( + slices = slices, + tags = tags, + ) + } + + fun ByteArraySimpleInputStream.readAseString(): String = readBytes(readU16LE()).toString(Charsets.UTF_8) + public infix fun Int.hasBitSet(index: Int): Boolean = ((this ushr index) and 1) != 0 + } +} diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/InputStreamExt.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/InputStreamExt.kt new file mode 100644 index 0000000000..4909769268 --- /dev/null +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/InputStreamExt.kt @@ -0,0 +1,96 @@ +package korlibs.korge.gradle.util + +import java.io.* + +class ByteArraySlice(val ba: ByteArray, val pos: Int = 0, val size: Int = ba.size - pos) { + fun sliceRange(range: IntRange): ByteArraySlice { + return ByteArraySlice(ba, pos + range.first, range.last - range.first - 1) + } + val length: Int get() = size + operator fun get(index: Int): Byte = ba[pos + index] + fun sliceArray(range: IntRange): ByteArray { + return ba.sliceArray((pos + range.first) .. (pos + range.last)) + } + + override fun toString(): String= "ByteArraySlice[$size]" +} + +class ByteArraySimpleInputStream( + val data: ByteArraySlice, + var pos: Int = 0 +) { + val available: Int get() = length - pos + val length: Int get() = data.size + + fun read(): Int { + if (pos >= length) return -1 + return data[pos++].toInt() and 0xFF + } + + fun readU8(): Int { + val v = this.read() + if (v < 0) error("Can't read byte at $pos in $data") + return v + } + + fun readU16LE(): Int { + val v0 = readU8() + val v1 = readU8() + return (v0 shl 0) or (v1 shl 8) + } + + fun readS16LE(): Int = (readU16LE() shl 16) shr 16 + + fun skip(count: Int): Int { + val oldPos = pos + pos += count + return oldPos + } + + fun readS32LE(): Int { + val v0 = readU8() + val v1 = readU8() + val v2 = readU8() + val v3 = readU8() + return (v0 shl 0) or (v1 shl 8) or (v2 shl 16) or (v3 shl 24) + } + + fun readU32LE(): Long { + return readS32LE().toLong() and 0xFFFFFFFFL + } + + fun readStream(count: Int): ByteArraySimpleInputStream { + val pos = skip(count) + return ByteArraySimpleInputStream(data.sliceRange(pos until (pos + count)), 0) + } + + fun readBytes(count: Int): ByteArray { + val start = skip(count) + return data.sliceArray(start until (start + count)) + } +} + +fun InputStream.readU8(): Int { + val v = this.read() + if (v < 0) error("Can't read byte") + return v +} + +fun InputStream.readU16LE(): Int { + val v0 = readU8() + val v1 = readU8() + return (v0 shl 0) or (v1 shl 8) +} + +fun InputStream.readS32LE(): Int { + val v0 = readU8() + val v1 = readU8() + val v2 = readU8() + val v3 = readU8() + return (v0 shl 0) or (v1 shl 8) or (v2 shl 16) or (v3 shl 24) +} + +fun InputStream.readU32LE(): Long { + return readS32LE().toLong() and 0xFFFFFFFFL +} + diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SFile.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SFile.kt index 8f9a87994e..5ad8b6cba9 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SFile.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SFile.kt @@ -12,6 +12,10 @@ interface SFile { fun exists(): Boolean fun write(text: String) fun read(): String + + fun writeBytes(bytes: ByteArray) + fun readBytes(): ByteArray + fun list(): List fun child(name: String): SFile } @@ -29,6 +33,8 @@ operator fun SFile.get(path: String): SFile? { } class LocalSFile(val file: File, val base: File) : SFile { + override fun toString(): String = "LocalSFile($file)" + constructor(file: File) : this(file, file) override val path: String by lazy { file.relativeTo(base).toString().replace('\\', '/') } override val name: String get() = file.name @@ -36,8 +42,13 @@ class LocalSFile(val file: File, val base: File) : SFile { override fun mkdirs() { file.mkdirs() } override fun isDirectory(): Boolean = file.isDirectory override fun exists(): Boolean = file.exists() + override fun write(text: String) = file.writeText(text) override fun read(): String = file.readText() + + override fun writeBytes(bytes: ByteArray) = file.writeBytes(bytes) + override fun readBytes(): ByteArray = file.readBytes() + override fun child(name: String): SFile = LocalSFile(File(file, name), base) override fun list(): List = (file.listFiles() ?: emptyArray()).map { LocalSFile(it, base) } } @@ -52,6 +63,7 @@ class MemorySFile(override val name: String, override val parent: MemorySFile? = var _isDirectory: Boolean = false var text: String? = null + var bytes: ByteArray? = null override fun mkdirs() { _isDirectory = true @@ -68,12 +80,20 @@ class MemorySFile(override val name: String, override val parent: MemorySFile? = override fun write(text: String) { this.text = text + this.bytes = text.toByteArray() } override fun read(): String { return text ?: error("File $path doesn't exist") } + override fun writeBytes(bytes: ByteArray) { + this.text = "" + this.bytes = bytes + } + + override fun readBytes(): ByteArray = bytes ?: error("File $path doesn't exist") + private val children: ArrayList = arrayListOf() private val childrenByName: LinkedHashMap = LinkedHashMap() override fun child(name: String): SFile { diff --git a/buildSrc/src/test/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGeneratorTest.kt b/buildSrc/src/test/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGeneratorTest.kt index 654a6a1449..52e5e3372a 100644 --- a/buildSrc/src/test/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGeneratorTest.kt +++ b/buildSrc/src/test/kotlin/korlibs/korge/gradle/typedresources/TypedResourcesGeneratorTest.kt @@ -15,25 +15,35 @@ class TypedResourcesGeneratorTest { "gfx/demo.atlas/world.png" to "", "0000/1111/222a.png" to "", "other/file.raw" to "", + "fonts/hello.ttf" to "", + "images/image.ase" to "", ) ) val generatedNormalized = generated.trim().replace("\t", " ") - //println(generatedNormalized) - assertEquals( -""" -import korlibs.image.atlas.Atlas -import korlibs.io.file.VfsFile -import korlibs.io.file.std.resourcesVfs -import korlibs.image.atlas.readAtlas -import korlibs.audio.sound.readSound -import korlibs.image.format.readBitmap + + val expectedNormalized = """ +import korlibs.audio.sound.* +import korlibs.io.file.* +import korlibs.io.file.std.* +import korlibs.image.bitmap.* +import korlibs.image.atlas.* +import korlibs.image.font.* +import korlibs.image.format.* // AUTO-GENERATED FILE! DO NOT MODIFY! @Retention(AnnotationRetention.BINARY) annotation class ResourceVfsPath(val path: String) inline class TypedVfsFile(val __file: VfsFile) -inline class TypedVfsFileBitmap(val __file: VfsFile) { suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap() } -inline class TypedVfsFileSound(val __file: VfsFile) { suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound() } +inline class TypedVfsFileTTF(val __file: VfsFile) { + suspend fun read(): korlibs.image.font.TtfFont = this.__file.readTtfFont() +} +inline class TypedVfsFileBitmap(val __file: VfsFile) { + suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap() + suspend fun readSlice(atlas: MutableAtlasUnit? = null, name: String? = null): BmpSlice = this.__file.readBitmapSlice(name, atlas) +} +inline class TypedVfsFileSound(val __file: VfsFile) { + suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound() +} interface TypedAtlas object KR : __KR.KR @@ -43,8 +53,10 @@ object __KR { interface KR { val __file get() = resourcesVfs[""] @ResourceVfsPath("0000") val `n0000` get() = __KR.KR0000 + @ResourceVfsPath("fonts") val `fonts` get() = __KR.KRFonts @ResourceVfsPath("gfx") val `gfx` get() = __KR.KRGfx @ResourceVfsPath("hello.png") val `hello` get() = TypedVfsFileBitmap(resourcesVfs["hello.png"]) + @ResourceVfsPath("images") val `images` get() = __KR.KRImages @ResourceVfsPath("other") val `other` get() = __KR.KROther @ResourceVfsPath("sfx") val `sfx` get() = __KR.KRSfx } @@ -54,11 +66,21 @@ object __KR { @ResourceVfsPath("0000/1111") val `n1111` get() = __KR.KR00001111 } + object KRFonts { + val __file get() = resourcesVfs["fonts"] + @ResourceVfsPath("fonts/hello.ttf") val `hello` get() = TypedVfsFileTTF(resourcesVfs["fonts/hello.ttf"]) + } + object KRGfx { val __file get() = resourcesVfs["gfx"] @ResourceVfsPath("gfx/demo.atlas.json") val `demo` get() = AtlasGfxDemoAtlas.TypedAtlas(resourcesVfs["gfx/demo.atlas.json"]) } + object KRImages { + val __file get() = resourcesVfs["images"] + @ResourceVfsPath("images/image.ase") val `image` get() = AseImagesImageAse.TypedAse(resourcesVfs["images/image.ase"]) + } + object KROther { val __file get() = resourcesVfs["other"] @ResourceVfsPath("other/file.raw") val `file` get() = TypedVfsFile(resourcesVfs["other/file.raw"]) @@ -80,8 +102,27 @@ inline class AtlasGfxDemoAtlas(val __atlas: korlibs.image.atlas.Atlas) { @ResourceVfsPath("gfx/demo.atlas/hello.png") val `hello` get() = __atlas["hello.png"] @ResourceVfsPath("gfx/demo.atlas/world.png") val `world` get() = __atlas["world.png"] } -""".trimIndent().trim(), - generatedNormalized - ) + +inline class AseImagesImageAse(val data: korlibs.image.format.ImageDataContainer) { + inline class TypedAse(val __file: VfsFile) { suspend fun read(atlas: korlibs.image.atlas.MutableAtlasUnit? = null): AseImagesImageAse = AseImagesImageAse(this.__file.readImageDataContainer(korlibs.image.format.ASE.toProps(), atlas)) } + enum class TypedAnimation(val animationName: String) { + ; + companion object { + val list: List = values().toList() + } + } + inline class TypedImageData(val data: ImageData) { + val animations: TypedAnimation.Companion get() = TypedAnimation + } + val animations: TypedAnimation.Companion get() = TypedAnimation + val default: TypedImageData get() = TypedImageData(data.default) +} +""".trimIndent().trim() + + if (expectedNormalized != generatedNormalized) { + println(generatedNormalized) + } + + assertEquals(expectedNormalized, generatedNormalized) } } diff --git a/e2e-test/src/commonMain/resources/fonts/1.ttf b/e2e-test/src/commonMain/resources/fonts/1.ttf new file mode 100644 index 0000000000..80c85749e3 Binary files /dev/null and b/e2e-test/src/commonMain/resources/fonts/1.ttf differ diff --git a/e2e-test/src/commonMain/resources/gfx/vampire.ase b/e2e-test/src/commonMain/resources/gfx/vampire.ase new file mode 100644 index 0000000000..d0b7dc9635 Binary files /dev/null and b/e2e-test/src/commonMain/resources/gfx/vampire.ase differ diff --git a/e2e-test/src/commonMain/resources/gfx/vampire_slices_fixed.ase b/e2e-test/src/commonMain/resources/gfx/vampire_slices_fixed.ase new file mode 100644 index 0000000000..1507e99835 Binary files /dev/null and b/e2e-test/src/commonMain/resources/gfx/vampire_slices_fixed.ase differ diff --git a/korge/src/commonMain/kotlin/korlibs/korge/view/animation/ImageDataView.kt b/korge/src/commonMain/kotlin/korlibs/korge/view/animation/ImageDataView.kt index f69e37dfa0..0cc2c86467 100644 --- a/korge/src/commonMain/kotlin/korlibs/korge/view/animation/ImageDataView.kt +++ b/korge/src/commonMain/kotlin/korlibs/korge/view/animation/ImageDataView.kt @@ -108,8 +108,21 @@ open class ImageDataView( this.smoothing = smoothing } + /** Play a specific animation */ + fun play(name: String?) { + animation = name + play() + } + @ViewProperty fun play() { animationView.play() } + + /** Stops to a specific animation */ + fun stop(name: String?) { + animation = name + stop() + } + @ViewProperty fun stop() { animationView.stop() } @ViewProperty