lang composition

This commit is contained in:
minjaesong
2022-01-11 11:04:11 +09:00
parent 2b3fdbde1f
commit 202123486a
20 changed files with 58 additions and 208 deletions

View File

@@ -0,0 +1,339 @@
package net.torvald.terrarum.spriteassembler
import com.badlogic.gdx.files.FileHandle
import net.torvald.terrarum.linearSearchBy
import net.torvald.terrarum.serialise.Common
import java.io.InputStream
import java.io.Reader
import java.io.StringReader
import java.util.*
internal data class Joint(val name: String, val position: ADPropertyObject.Vector2i) {
override fun toString() = "$name $position"
}
internal data class Skeleton(val name: String, val joints: List<Joint>) {
override fun toString() = "$name=$joints"
}
/**
* @param name You know it
* @param delay Delay between each frame in seconds
* @param row STARTS AT ONE! Row in the final spritesheet, also act as the animation index.
* @param frames number of frames this animation has
* @param skeleton list of joints to be transformed
*/
internal data class Animation(val name: String, val delay: Float, val row: Int, val frames: Int, val skeleton: Skeleton) {
override fun toString() = "$name delay: $delay, row: $row, frames: $frames, skeleton: ${skeleton.name}"
}
/** Later the 'translate' can be changed so that it represents affine transformation (Matrix2d) */
internal data class Transform(val joint: Joint, val translate: ADPropertyObject.Vector2i) {
override fun toString() = "$joint transform: $translate"
}
class ADProperties {
private var fileFrom = ""
@Transient private var adlString = ""
private val javaProp = Properties()
/** Every key is CAPITALISED */
private val propTable = HashMap<String, List<ADPropertyObject>>()
/** list of bodyparts used by all the skeletons (HEAD, UPPER_TORSO, LOWER_TORSO) */
// lateinit var bodyparts: List<String>; private set
lateinit var bodypartFiles: List<String>; private set
/** properties that are being used as skeletons (SKELETON_STAND) */
internal lateinit var skeletons: HashMap<String, Skeleton>; private set
/** properties that defines position of joint of the bodypart */
internal val bodypartJoints = HashMap<String, ADPropertyObject.Vector2i>()
/** properties that are recognised as animations (ANIM_RUN, ANIM)IDLE) */
internal lateinit var animations: HashMap<String, Animation>; private set
/** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */
internal lateinit var transforms: HashMap<String, List<Transform>>; private set
@Transient private val reservedProps = listOf("SPRITESHEET", "EXTENSION", "CONFIG", "BODYPARTS")
@Transient private val animMustContain = listOf("DELAY", "ROW", "SKELETON")
lateinit var baseFilename: String; private set
lateinit var extension: String; private set
var frameWidth: Int = -1; private set
var frameHeight: Int = -1; private set
var originX: Int = -1; private set
internal val origin: ADPropertyObject.Vector2i
get() = ADPropertyObject.Vector2i(originX, 0)
@Transient private val animFrameSuffixRegex = Regex("""_[0-9]+""")
@Transient private val ALL_JOINT = Joint(ALL_JOINT_SELECT_KEY, ADPropertyObject.Vector2i(0, 0))
var rows = -1; private set
var cols = -1; private set
fun getRawADL() = adlString
companion object {
const val ALL_JOINT_SELECT_KEY = "ALL"
}
constructor(gdxFile: FileHandle) {
fileFrom = gdxFile.path()
adlString = gdxFile.readString(Common.CHARSET.name())
continueLoad()
}
constructor(reader: Reader) {
adlString = reader.readText()
continueLoad()
}
constructor(inputStream: InputStream) {
adlString = inputStream.readAllBytes().toString(Common.CHARSET)
continueLoad()
}
private fun continueLoad() {
javaProp.load(StringReader(adlString))
// sanity check
reservedProps.forEach {
try {
javaProp[it]!!
}
catch (e: NullPointerException) {
throw IllegalArgumentException("Prop '$it' not found from ${fileFrom.ifBlank { "'${adlString.substring(0, 1024)}'" }}", e)
}
}
javaProp.keys.forEach { propName ->
val propsStr = javaProp.getProperty(propName as String)
val propsList = propsStr.split(';').map { ADPropertyObject(it) }
propTable[propName.toUpperCase()] = propsList
}
// set reserved values for the animation: filename, extension
baseFilename = get("SPRITESHEET")[0].name
extension = get("EXTENSION")[0].name
val frameSizeVec = get("CONFIG").linearSearchBy { it.name == "SIZE" }!!.input as ADPropertyObject.Vector2i
frameWidth = frameSizeVec.x
frameHeight = frameSizeVec.y
originX = (get("CONFIG").linearSearchBy { it.name == "ORIGINX" }!!.input as Float).toInt()
var maxColFinder = -1
var maxRowFinder = -1
val skeletons = HashMap<String, Skeleton>()
val animations = HashMap<String, Animation>()
val animFrames = HashMap<String, Int>()
val transforms = HashMap<String, List<Transform>>()
// scan every props, write down anim frames for later use
propTable.keys.forEach {
if (animFrameSuffixRegex.containsMatchIn(it)) {
val animName = getAnimNameFromFrame(it)
val frameNumber = getFrameNumberFromName(it)
// if animFrames does not have our entry, add it.
// otherwise, max() against the existing value
if (animFrames.containsKey(animName)) {
animFrames[animName] = maxOf(animFrames[animName]!!, frameNumber)
}
else {
animFrames[animName] = frameNumber
}
maxColFinder = maxOf(maxColFinder, frameNumber)
}
}
// populate skeletons and animations
forEach { s, list ->
// Map-ify. If it has variable == "SKELETON", the 's' is likely an animation
// and thus, uses whatever the "input" used by the SKELETON is a skeleton
val propsHashMap = HashMap<String, Any?>()
list.forEach {
propsHashMap[it.name.toUpperCase()] = it.input
}
// if it is indeed anim, populate animations list
if (propsHashMap.containsKey("SKELETON")) {
val skeletonName = propsHashMap["SKELETON"] as String
val skeletonDef = get(skeletonName)
skeletons[skeletonName] = Skeleton(skeletonName, skeletonDef.toJoints())
animations[s] = Animation(
s,
propsHashMap["DELAY"] as Float,
(propsHashMap["ROW"] as Float).toInt(),
animFrames[s]!!,
Skeleton(skeletonName, skeletonDef.toJoints())
)
maxRowFinder = maxOf(maxRowFinder, animations[s]!!.row)
}
}
// populate transforms
animations.forEach { t, u ->
for (fc in 1..u.frames) {
val frameName = "${t}_$fc"
val prop = get(frameName)
var emptyList = prop.size == 1 && prop[0].name.isEmpty()
val transformList = if (!emptyList) {
List(prop.size) { index ->
val jointNameToSearch = prop[index].name.toUpperCase()
val joint = if (jointNameToSearch == "ALL")
ALL_JOINT
else
u.skeleton.joints.linearSearchBy { it.name == jointNameToSearch }
?: throw NullPointerException("No such joint: $jointNameToSearch")
val translate = prop[index].input as ADPropertyObject.Vector2i
Transform(joint, translate)
}
}
else {
// to make real empty list
List(0) { Transform(ALL_JOINT, ADPropertyObject.Vector2i(0, 0)) }
}
transforms[frameName] = transformList
}
}
get("BODYPARTS").forEach {
try {
this.bodypartJoints[it.name] = (it.input as ADPropertyObject.Vector2i)
}
catch (e: NullPointerException) {
if (it.name.isBlank())
throw IllegalArgumentException("Empty Bodypart name on BODYPARTS; try removing trailing semicolon (';')?")
else
throw IllegalArgumentException("Bodyparts definition for '${it.name}' not found from ${fileFrom.ifBlank { "'${adlString.substring(0, 1024)}'" }}", e)
}
}
this.skeletons = skeletons
this.animations = animations
this.bodypartFiles = this.bodypartJoints.keys.map { toFilename(it) }
this.transforms = transforms
cols = maxColFinder
rows = maxRowFinder
}
operator fun get(identifier: String): List<ADPropertyObject> = propTable[identifier.toUpperCase()]!!
val keys
get() = propTable.keys
fun containsKey(key: String) = propTable.containsKey(key)
fun forEach(predicate: (String, List<ADPropertyObject>) -> Unit) = propTable.forEach(predicate)
fun toFilename(partName: String) =
"${this.baseFilename}${partName.toLowerCase()}${this.extension}"
internal fun getAnimByFrameName(frameName: String) = animations[getAnimNameFromFrame(frameName)]!!
internal fun getFrameNumberFromName(frameName: String) = frameName.substring(frameName.lastIndexOf('_') + 1 until frameName.length).toInt()
internal fun getSkeleton(name: String) = skeletons[name]!!
internal fun getTransform(name: String) = transforms[name]!!
private fun getAnimNameFromFrame(s: String) = s.substring(0 until s.lastIndexOf('_'))
private fun List<ADPropertyObject>.toJoints() = List(this.size) {
Joint(this[it].name.toUpperCase(), this[it].input!! as ADPropertyObject.Vector2i)
}
}
/**
* @param propertyRaw example inputs:
* - ```DELAY 0.15```
* - ```LEG_RIGHT 0,-1```
*
* Created by minjaesong on 2019-01-05.
*/
class ADPropertyObject(propertyRaw: String) {
/** If the input is like ```UPPER_TORSO``` (that is, not a variable-input pair), this holds the string UPPER_TORSO. */
val name: String
val input: Any?
get() = when (type) {
ADPropertyType.IVEC2 -> field!! as Vector2i
ADPropertyType.FLOAT -> field!! as Float
ADPropertyType.STRING_PAIR -> field!! as String
else -> null
}
val type: ADPropertyType
init {
val propPair = propertyRaw.split(variableInputSepRegex)
if (isADvariable(propertyRaw)) {
name = propPair[0]
val inputStr = propPair[1]
if (isADivec2(inputStr)) {
type = ADPropertyType.IVEC2
input = toADivec2(inputStr)
}
else if (isADfloat(inputStr)) {
type = ADPropertyType.FLOAT
input = toADfloat(inputStr)
}
else {
type = ADPropertyType.STRING_PAIR
input = inputStr
}
}
else {
name = propertyRaw
input = null
type = ADPropertyType.NAME_ONLY
}
}
internal companion object {
private val floatRegex = Regex("""-?[0-9]+(\.[0-9]*)?""")
private val ivec2Regex = Regex("""-?[0-9]+,-?[0-9]+""")
private val variableInputSepRegex = Regex(""" +""")
fun isADivec2(s: String) = ivec2Regex.matches(s)
fun isADfloat(s: String) = floatRegex.matches(s) && !ivec2Regex.containsMatchIn(s)
fun toADivec2(s: String) = if (isADivec2(s))
Vector2i(s.substringBefore(',').toInt(), s.substringAfter(',').toInt())
else throw IllegalArgumentException("Input not in ivec2 format: $s")
fun toADfloat(s: String) = if (isADfloat(s))
s.toFloat()
else throw IllegalArgumentException("Input not in ivec2 format: $s")
/** example valid input: ```LEG_RIGHT 0,1``` */
fun isADvariable(property: String) = variableInputSepRegex.containsMatchIn(property)
/** example valid input: ```sprites/test``` */
fun isADstring(property: String) = !isADvariable(property)
}
internal data class Vector2i(var x: Int, var y: Int) {
override fun toString() = "($x, $y)"
operator fun plus(other: Vector2i) = Vector2i(this.x + other.x, this.y + other.y)
operator fun minus(other: Vector2i) = Vector2i(this.x - other.x, this.y - other.y)
fun invertY() = Vector2i(this.x, -this.y)
fun invertX() = Vector2i(-this.x, this.y)
fun invertXY() = Vector2i(-this.x, -this.y)
}
enum class ADPropertyType {
NAME_ONLY, // "sprite/test.tga" to nothing
IVEC2, // "LEG_RIGHT" to (1,-1)
FLOAT, // "DELAY" to 0.15
STRING_PAIR // "SKELETON" to "SKELETON_DEFAULT"
}
override fun toString(): String {
return "$name ${input ?: ""}: ${type.toString().toLowerCase()}"
}
}

View File

@@ -0,0 +1,119 @@
## Animation Description Language
This is a text version of my drawing of same name. 2018-01-04 CuriousTorvald
Author's node: yet another non-JSON domain-specific language because why not?
## Objective
* Java .properties-compatible
* Case insensitive
## Example code
```
SPRITESHEET=sprites/test
EXTENSION=.tga.gz
ANIM_RUN=DELAY 0.15;ROW 2
ANIM_RUN_BODYPARTS=HEAD;UPPER_TORSO;LOWER_TORSO;ARM_FWD_LEFT;ARM_FWD_RIGHT;LEG_LEFT;LEG_RIGHT
ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0
ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1
ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1
ANIM_IDLE=DELAY 2;ROW 1
ANIM_IDLE_BODYPARTS=HEAD;UPPER_TORSO;LOWER_TORSO;ARM_REST_LEFT;ARM_REST_RIGHT;LEG_LEFT;LEG_RIGHT
ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1
ANIM_CROUCH=DELAY 1;ROW 3
ANIM_CROUCH_BODYPARTS=HEAD;UPPER_TORSO;LOWER_TORSO;ARM_FWD_LEFT;ARM_FWD_RIGHT;LEG_CROUCH_LEFT;LEG_CROUCH_RIGHT
ANIM_CROUCH_1=
...
```
### In-detail
```
ANIM_RUN=DELAY 0.15;ROW 2
```
Each line defines one property. A property is a field-value pair. In this code, field is ```ANIM_RUN```, and the value is ```DELAY 0.15;ROW 2```
The values are further parsed using ```;``` (semicolon with NO spaces attached) as a separator.
```
In this example, ANIM_RUN contains two variables:
DELAY = 0.15
ROW = 2
```
A value of the field is consisted of zero or more variable-input pairs. Variable and the input are separated with one or more connected spaces.
#### Variables
Variables can have only one of the two types: ```float``` and ```ivec2```. Single integer value ('2' in the ROW) are regarded as a float.
Float and Ivec2 are determined by:
* Ivec2: inputs that are matched by the regex ```-?[0-9]+,-?[0-9]+``` (we call this "ivec2 regex")
* Float: inputs that are matched by the regex ```-?[0-9]+(\.[0-9]*)?```, but not even partially matched by the ivec2 regex.
Any argument to the body parts takes ivec2, to move the parts accordingly.
#### Just one exception: SPRITESHEET and EXTENSION
SPRITESHEET and EXTENSION property is not parsed as a property, it's just a single string like the original .properties
### Naming convention of properties
If a field is recognised as an animation (in this case ANIM_RUN), the assembler will look for the fields named like ANIM_RUN_1, ANIM_RUN_2, ... , ANIM_RUN_9, ANIM_RUN_10 and beyond.
### Naming convention of files
If the animation specifies a "body part" (in this example LEG_LEFT and LEG_RIGHT), the assembler will look for a file ```sprites/test_leg_left.tga.gz``` and ```sprites/test_leg_right.tga.gz``` respectively. Filenames are advised to be kept all lowercase.
### Reserved keywords
These values must exist so that the file can be parsed successfully.
#### Root
|Name|Type|Meaning|
|---|---|---|
|SPRITESHEET|properties: NAME_ONLY|Base file name of the images|
|EXTENSION|properties: NAME_ONLY|Extension of the base file|
|CONFIG|properties: 2 variables|Frame size and origin-x position, 0 being left|
#### Animation
Remember that 'variables' are contained within 'properties'
|Name|Type|Meaning|
|---|---|---|
|DELAY|variable: float|Delay between frames, in seconds|
|ROW|variable: float|which row the animation goes in the spritesheet|
|SKELETON|variable: string_pair|Which skeleton this animation uses
#### Transforms
Things like ```LEG_RIGHT -1,0``` within ```ANIM_RUN_3``` are called 'Transform'
|Name|Type|Meaning|
|---|---|---|
|ALL|variable: ivec2|Shifts (translates) everything by set value|
### Notes
* All indices are one-based
## Operation
* Each line describes transformation
* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! (really?)
* The Field is an identifier the game code -- sprite assembler -- recognises.
* The Field of animation's name is the name the game code looks for. Example: ```this.setAnim("ANIM_RUN")```
* Coord system is Y-Up, meaning Y=0 is the bottommost position.

View File

@@ -0,0 +1,274 @@
package net.torvald.terrarum.spriteassembler
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.utils.GdxRuntimeException
import net.torvald.terrarum.App
import net.torvald.terrarum.ItemCodex
import net.torvald.terrarum.ReferencingRanges
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.linearSearch
import net.torvald.terrarum.savegame.ByteArray64InputStream
import net.torvald.terrarum.savegame.ByteArray64Reader
import net.torvald.terrarum.savegame.SimpleFileSystem
import net.torvald.terrarum.serialise.Common
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.*
/**
* Assembles the single frame of the animation, outputs GDX Pixmap.
*
* The entire rendering is done by using pixmap. That is, no GPU access.
*
* Created by minjaesong on 2019-01-06.
*/
object AssembleSheetPixmap {
const val MUGSHOT_PIXMAP_W = 48
const val MUGSHOT_PIXMAP_H = 48
/**
* The name of the Bodypart here may or may not be case-sensitive (depends on your actual filesystem -- NTFS, APFS, Ext4, ...)
*/
fun getAssetsDirFileGetter(properties: ADProperties): (String) -> InputStream? = { partName: String ->
val file = Gdx.files.internal("assets/${properties.toFilename(partName)}")
if (file.exists()) file.read() else null
}
/**
* The name of the Bodypart is CASE-SENSITIVE!
*/
fun getVirtualDiskFileGetter(bodypartMapping: Properties, disk: SimpleFileSystem): (String) -> InputStream? = { partName: String ->
bodypartMapping.getProperty(partName).let {
if (it != null)
ByteArray64InputStream(disk.getFile(bodypartMapping.getProperty(partName).toLong())!!.bytes)
else
null
}
}
private fun drawAndGetCanvas(properties: ADProperties, fileGetter: (String) -> InputStream?, injectedItem: GameItem?): Pixmap {
val canvas = Pixmap(properties.cols * properties.frameWidth, properties.rows * properties.frameHeight, Pixmap.Format.RGBA8888)
canvas.blending = Pixmap.Blending.SourceOver
// actually draw
properties.transforms.forEach { t, _ ->
drawThisFrame(t, canvas, properties, fileGetter, injectedItem)
}
return canvas
}
fun fromAssetsDir(properties: ADProperties, injectedItem: GameItem?) = drawAndGetCanvas(properties, getAssetsDirFileGetter(properties), injectedItem)
fun fromVirtualDisk(disk: SimpleFileSystem, entrynum: Long, properties: ADProperties, injectedItem: GameItem?): Pixmap {
val bodypartMapping = Properties()
bodypartMapping.load(ByteArray64Reader(disk.getFile(entrynum)!!.bytes, Common.CHARSET))
return drawAndGetCanvas(properties, getVirtualDiskFileGetter(bodypartMapping, disk), injectedItem)
}
fun getPartPixmap(getFile: (String) -> InputStream?, partName: String): Pixmap? {
getFile(partName)?.let {
val bytes = it.readAllBytes()
return Pixmap(bytes, 0, bytes.size)
}
return null
}
fun getMugshotFromAssetsDir(properties: ADProperties): TextureRegion? {
// TODO assemble from HAIR_FORE (optional), HAIR (optional) then HEAD (mandatory)
val getter = getAssetsDirFileGetter(properties)
val headPixmap = getPartPixmap(getter, "HEAD")
val hairPixmap = getPartPixmap(getter, "HAIR")
val hair2Pixmap = getPartPixmap(getter, "HAIR_FORE")
if (headPixmap == null) throw FileNotFoundException("Bodyparts file of HEAD is not found!")
return composeMugshot(properties, headPixmap, hairPixmap, hair2Pixmap)
}
fun getMugshotFromVirtualDisk(disk: SimpleFileSystem, entrynum: Long, properties: ADProperties): TextureRegion? {
// TODO assemble from HAIR_FORE (optional), HAIR (optional) then HEAD (mandatory)
val bodypartMapping = Properties()
bodypartMapping.load(ByteArray64Reader(disk.getFile(entrynum)!!.bytes, Common.CHARSET))
val getter = getVirtualDiskFileGetter(bodypartMapping, disk)
val headPixmap = getPartPixmap(getter, "HEAD")
val hairPixmap = getPartPixmap(getter, "HAIR")
val hair2Pixmap = getPartPixmap(getter, "HAIR_FORE")
if (headPixmap == null) throw FileNotFoundException("Bodyparts file of HEAD is not found!")
return composeMugshot(properties, headPixmap, hairPixmap, hair2Pixmap)
}
private fun composeMugshot(properties: ADProperties, head: Pixmap, hair: Pixmap?, hair2: Pixmap?): TextureRegion {
val canvas = Pixmap(MUGSHOT_PIXMAP_W, MUGSHOT_PIXMAP_H, Pixmap.Format.RGBA8888)
val drawX = (canvas.width - head.width) / 2
val drawY = (canvas.height - head.height) / 2
val headOffset = properties.bodypartJoints["HEAD"]!!
// TODO shift drawing pos using the properties BODYPARTS
canvas.drawPixmap(head, drawX, drawY)
hair?.let {
val offset = properties.bodypartJoints["HAIR"]!! - headOffset
canvas.drawPixmap(it, drawX - offset.x, drawY - offset.y)
}
hair2?.let {
val offset = properties.bodypartJoints["HAIR_FORE"]!! - headOffset
canvas.drawPixmap(it, drawX - offset.x, drawY - offset.y)
}
val tr = TextureRegion(Texture(canvas))
canvas.dispose()
head.dispose()
hair?.dispose()
hair2?.dispose()
return tr
}
private fun drawThisFrame(frameName: String,
canvas: Pixmap,
properties: ADProperties,
fileGetter: (String) -> InputStream?,
injectedItem: GameItem?
) {
val theAnim = properties.getAnimByFrameName(frameName)
val skeleton = theAnim.skeleton.joints.reversed()
val transforms = properties.getTransform(frameName)
val bodypartOrigins = properties.bodypartJoints
val bodypartImages = properties.bodypartJoints.keys.map { partname ->
fileGetter(partname).let { file ->
if (file == null) partname to null
else {
try {
val bytes = file.readAllBytes()
partname to Pixmap(bytes, 0, bytes.size)
}
catch (e: GdxRuntimeException) {
partname to null
}
}
}
}.toMap()
val transformList = AssembleFrameBase.makeTransformList(skeleton, transforms)
val animRow = theAnim.row
val animFrame = properties.getFrameNumberFromName(frameName)
// AppLoader.printdbg(this, "Frame to draw: $frameName (R$animRow C$animFrame)")
drawFrame(animRow, animFrame, canvas, properties, bodypartOrigins, bodypartImages, transformList, injectedItem)
bodypartImages.values.forEach { it?.dispose() }
}
private fun drawFrame(row: Int, column: Int,
canvas: Pixmap,
props: ADProperties,
bodypartOrigins: HashMap<String, ADPropertyObject.Vector2i>,
bodypartImages: Map<String, Pixmap?>,
transformList: List<Pair<String, ADPropertyObject.Vector2i>>,
injectedItem: GameItem?
) {
val tmpFrame = Pixmap(props.frameWidth, props.frameHeight, Pixmap.Format.RGBA8888)
transformList.forEach { (name, bodypartPos) ->
if (name == "HELD_ITEM" && injectedItem != null) {
// printdbg(this, "ID of the held item: ${injectedItem.originalID}")
ItemCodex.getItemImage(injectedItem)?.let { textureRegion ->
// printdbg(this, "and it did have a textureregion")
val texdata = textureRegion.texture.textureData
val textureBackedByPixmap = texdata.isPrepared // texture backed by pixmap is always prepared without ordering it to prepare
if (!textureBackedByPixmap) texdata.prepare()
val imageSheet = if
(injectedItem.originalID.startsWith("${ReferencingRanges.PREFIX_DYNAMICITEM}:") ||
injectedItem.originalID.startsWith("item@") ||
injectedItem.originalID.startsWith("wire@"))
texdata.consumePixmap()
// super dirty and ugly hack because for some reason it just won't work
else if (injectedItem.originalID.startsWith("wall@"))
App.tileMaker.itemWallPixmap
else
App.tileMaker.itemTerrainPixmap
val drawPos = props.origin + bodypartPos
val pu = (textureRegion.u * texdata.width).toInt()
val pv = (textureRegion.v * texdata.height).toInt()
val imageWidth = textureRegion.regionWidth
val imageHeight = textureRegion.regionHeight
// printdbg(this, "uv: ($pu,$pv) uv2: ($pu2,$pv2) dim: ($imageWidth,$imageHeight) atlasdim: (${texdata.width},${texdata.height})")
tmpFrame.drawPixmap(imageSheet, drawPos.x, props.frameHeight - drawPos.y - 1 - imageHeight, pu, pv, imageWidth, imageHeight)
if (!textureBackedByPixmap) imageSheet.dispose()
}
}
else {
bodypartImages[name]?.let { image ->
val imgCentre = bodypartOrigins[name]!!.invertX()
val drawPos = props.origin + bodypartPos + imgCentre
tmpFrame.drawPixmap(image, drawPos.x, props.frameHeight - drawPos.y - 1)
}
}
}
canvas.drawPixmap(
tmpFrame,
(column - 1) * props.frameWidth,
(row - 1) * props.frameHeight
)
tmpFrame.dispose()
}
}
internal object AssembleFrameBase {
/**
* Returns joints list with tranform applied.
* @param skeleton list of joints
* @param transform ordered list of transforms should be applied. First come first serve.
* @return List of pairs that contains joint name on left, final transform value on right
*/
fun makeTransformList(joints: List<Joint>, transforms: List<Transform>): List<Pair<String, ADPropertyObject.Vector2i>> {
// make our mutable list
val out = ArrayList<Pair<String, ADPropertyObject.Vector2i>>()
joints.forEach {
out.add(it.name to it.position)
}
// process transform queue
transforms.forEach { transform ->
if (transform.joint.name == ADProperties.ALL_JOINT_SELECT_KEY) {
// transform applies to all joints
for (c in out.indices) {
out[c] = out[c].first to (out[c].second + transform.translate)
}
}
else {
val i = out.linearSearch { it.first == transform.joint.name }!!
// transform applies to one specific joint in the list (one specific joint is a search result)
out[i] = out[i].first to (out[i].second + transform.translate)
}
}
return out.toList()
}
fun getCentreOf(pixmap: Pixmap) = ADPropertyObject.Vector2i(pixmap.width / 2, pixmap.height / 2)
}

View File

@@ -0,0 +1,331 @@
package net.torvald.terrarum.spriteassembler
import com.badlogic.gdx.Game
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.EMDASH
import net.torvald.gdx.graphics.PixmapIO2
import net.torvald.terrarum.gdxClearAndSetBlend
import net.torvald.terrarum.inUse
import java.awt.BorderLayout
import java.awt.Font
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.StringReader
import java.util.*
import javax.swing.*
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
/**
* Created by minjaesong on 2019-01-05.
*/
class SpriteAssemblerApp(val gdxWindow: SpriteAssemblerPreview) : JFrame() {
private val panelProperties = JTree()
private val panelAnimationsList = JList<String>()
private val panelBodypartsList = JList<String>()
private val panelImageFilesList = JList<String>()
private val panelSkeletonsList = JList<String>()
private val panelTransformsList = JList<String>()
private val panelStatList = JList<String>()
private val panelCode = JTextPane()
private val statBar = JTextArea("Null.")
private lateinit var adProperties: ADProperties
private val lang = Properties()
/**
* ¤ is used as a \n marker
*/
private val translations = """
WARNING_CONTINUE=Continue?
WARNING_YOUR_DATA_WILL_GONE=Existing edits will be lost.
OPERATION_CANCELLED=Operation cancelled.
NO_SUCH_FILE=No such file exists, operation cancelled.
NEW_ROWS=Enter the number of rows to initialise the new CSV.¤Remember, you can always add or delete rows later.
ADD_ROWS=Enter the number of rows to add:
WRITE_FAIL=Writing to file has failed:
STAT_INIT=Creating a new CSV. You can still open existing file.
STAT_SAVE_TGA_SUCCESSFUL=Spritesheet exported successfully.
STAT_LOAD_SUCCESSFUL=File loaded successfully.
ERROR_INTERNAL=Something went wrong.
ERROR_PARSE_FAIL=Parsing failed
SPRITE_DEF_LOAD_SUCCESSFUL=Sprite definition loaded.
SPRITE_ASSEMBLE_SUCCESSFUL=Sprite assembled.
PROPERTIES_GO_HERE=Properties will be shown here.
""".trimIndent()
private var panelCodeInit = true
init {
// setup application properties //
try {
lang.load(StringReader(translations))
}
catch (e: Throwable) {
}
panelCode.font = Font(Font.MONOSPACED, Font.PLAIN, 12)
panelCode.text = """Terrarum Sprite Assembler
|Copyright 2019${EMDASH} CuriousTorvald (minjaesong)
|
|This program is free software: you can redistribute it and/or modify
|it under the terms of the GNU General Public License as published by
|the Free Software Foundation, either version 3 of the License, or
|(at your option) any later version.
|
|This program is distributed in the hope that it will be useful,
|but WITHOUT ANY WARRANTY; without even the implied warranty of
|MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|GNU General Public License for more details.
|
|You should have received a copy of the GNU General Public License
|along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|Paste your Animation Description here and press 'Update'!""".trimMargin()
panelCode.addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
if (panelCodeInit) {
panelCodeInit = false
panelCode.text = ""
}
}
})
panelAnimationsList.model = DefaultListModel()
panelBodypartsList.model = DefaultListModel()
panelImageFilesList.model = DefaultListModel()
panelSkeletonsList.model = DefaultListModel()
panelTransformsList.model = DefaultListModel()
panelStatList.model = DefaultListModel()
val panelPartsList = JTabbedPane(JTabbedPane.TOP)
panelPartsList.add("Animations", JScrollPane(panelAnimationsList))
panelPartsList.add("Bodyparts", JScrollPane(panelBodypartsList))
panelPartsList.add("Images", JScrollPane(panelImageFilesList))
panelPartsList.add("Skeletons", JScrollPane(panelSkeletonsList))
panelPartsList.add("Transforms", JScrollPane(panelTransformsList))
panelPartsList.add("Stats", JScrollPane(panelStatList))
panelProperties.model = DefaultTreeModel(DefaultMutableTreeNode(lang.getProperty("PROPERTIES_GO_HERE")))
val panelDataView = JSplitPane(JSplitPane.VERTICAL_SPLIT, JScrollPane(panelProperties), panelPartsList)
panelDataView.resizeWeight = 0.4
// to disable text wrap
//val panelCodeNoWrap = JPanel(BorderLayout())
//panelCodeNoWrap.add(panelCode)
val panelMain = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, JScrollPane(panelCode), panelDataView)
panelMain.resizeWeight = 0.6
val menu = JMenuBar()
menu.add(JMenu("Update")).addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
try {
adProperties = ADProperties(StringReader(panelCode.text))
statBar.text = lang.getProperty("SPRITE_DEF_LOAD_SUCCESSFUL")
val propRoot = DefaultMutableTreeNode("Properties")
adProperties.forEach { s, list ->
// build tree node for the properties display
val propNode = DefaultMutableTreeNode(s)
propRoot.add(propNode)
list.forEach {
propNode.add(DefaultMutableTreeNode(it.toString()))
}
}
panelProperties.model = DefaultTreeModel(propRoot)
// clean the data views
panelAnimationsList.model = DefaultListModel()
panelBodypartsList.model = DefaultListModel()
panelImageFilesList.model = DefaultListModel()
panelSkeletonsList.model = DefaultListModel()
panelTransformsList.model = DefaultListModel()
panelStatList.model = DefaultListModel()
// populate animations view
adProperties.animations.forEach {
(panelAnimationsList.model as DefaultListModel).addElement("${it.value}")
}
// populate bodyparts view
adProperties.bodypartJoints.toSortedMap().forEach { part ->
(panelBodypartsList.model as DefaultListModel).addElement("${part.key}: ${part.value}")
}
// populate image file list view
adProperties.bodypartFiles.forEach { partName ->
(panelImageFilesList.model as DefaultListModel).addElement(partName)
}
// populate skeletons view
adProperties.skeletons.forEach {
(panelSkeletonsList.model as DefaultListModel).addElement("${it.value}")
}
// populate transforms view
adProperties.transforms.forEach {
(panelTransformsList.model as DefaultListModel).addElement("$it")
}
// populate stats
(panelStatList.model as DefaultListModel).addElement("Spritesheet rows: ${adProperties.rows}")
(panelStatList.model as DefaultListModel).addElement("Spritesheet columns: ${adProperties.cols}")
(panelStatList.model as DefaultListModel).addElement("Frame size: ${adProperties.frameWidth}, ${adProperties.frameHeight}")
(panelStatList.model as DefaultListModel).addElement("Origin position: ${adProperties.origin}")
gdxWindow.requestAssemblyTest(adProperties)
statBar.text = lang.getProperty("SPRITE_ASSEMBLE_SUCCESSFUL")
}
catch (fehler: Throwable) {
displayError("ERROR_PARSE_FAIL", fehler)
fehler.printStackTrace()
}
}
})
menu.add(JMenu("Zoom 1x")).addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
gdxWindow.zoom = 1f
}
})
menu.add(JMenu("2x")).addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
gdxWindow.zoom = 2f
}
})
menu.add(JMenu("3x")).addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
gdxWindow.zoom = 3f
}
})
menu.add(JMenu("4x")).addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent?) {
gdxWindow.zoom = 4f
}
})
this.layout = BorderLayout()
this.add(menu, BorderLayout.NORTH)
this.add(panelMain, BorderLayout.CENTER)
this.add(statBar, BorderLayout.SOUTH)
this.title = "Terrarum Sprite Assembler and Viewer"
this.isVisible = true
this.setSize(1120, 768)
this.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
}
private fun displayMessage(messageKey: String) {
JOptionPane.showOptionDialog(
null,
lang.getProperty(messageKey), null,
JOptionPane.DEFAULT_OPTION,
JOptionPane.INFORMATION_MESSAGE, null,
arrayOf("OK", "Cancel"),
"Cancel"
)
}
private fun displayError(messageKey: String, cause: Throwable) {
JOptionPane.showOptionDialog(null,
lang.getProperty(messageKey) + "\n" + cause.toString(), null,
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE, null,
arrayOf("OK", "Cancel"),
"Cancel"
)
}
}
class SpriteAssemblerPreview: Game() {
private lateinit var batch: SpriteBatch
var zoom = 1f
private lateinit var renderTexture: Texture
private var image: Pixmap? = null
set(value) {
renderTexture.dispose()
field?.dispose()
field = value
renderTexture = Texture(field)
}
override fun create() {
Gdx.graphics.setTitle("Sprite Assembler Preview")
batch = SpriteBatch()
renderTexture = Texture(1, 1, Pixmap.Format.RGBA8888)
}
private val bgCol = Color(.62f, .79f, 1f, 1f)
private var doAssemble = false
private lateinit var assembleProp: ADProperties
private var doExport = false
private lateinit var exportPath: String
override fun render() {
if (doAssemble) {
// assembly requires GL context
doAssemble = false
assembleImage(assembleProp)
}
if (doExport && image != null) {
doExport = false
PixmapIO2.writeTGAHappy(Gdx.files.absolute(exportPath), image, false)
}
gdxClearAndSetBlend(bgCol)
batch.inUse {
batch.color = Color.WHITE
batch.draw(renderTexture, 0f, 0f, renderTexture.width * zoom, renderTexture.height * zoom)
}
}
private fun assembleImage(prop: ADProperties) {
image = AssembleSheetPixmap.fromAssetsDir(prop, null)
}
// TODO rename to requestAssembly
fun requestAssemblyTest(prop: ADProperties) {
doAssemble = true
assembleProp = prop
}
fun requestExport(path: String) {
doExport = true
exportPath = path
}
override fun resize(width: Int, height: Int) {
super.resize(width, height)
}
}
fun main(args: Array<String>) {
val appConfig = Lwjgl3ApplicationConfiguration()
appConfig.setWindowedMode(1024, 1024)
appConfig.setIdleFPS(5)
appConfig.setForegroundFPS(5)
appConfig.setResizable(false)
val gdxWindow = SpriteAssemblerPreview()
SpriteAssemblerApp(gdxWindow)
Lwjgl3Application(gdxWindow, appConfig)
}

View File

@@ -0,0 +1,35 @@
# complete file name is: SPRITESHEET + bodypart name + EXTENSION
SPRITESHEET=mods/basegame/sprites/sprite_assembler_test_assets/test_
EXTENSION=.tga
# defines frame size and origin point. Origin point is given as: (originx, size.y - 1)
CONFIG=SIZE 48,56;ORIGINX 29
# note to self: don't implement skeleton hierarchy: there's too many exceptions
# besides, you have "ALL" key.
! a skeleton also defines what body parts (images) be used.
! you can also write multiline text using reverse solidus; this is a feature of .properties
! skeleton joints are ordered: foremost-drawn object comes first, which means lowermost object IN THIS LIST
! are painted first, and any object that comes before it will paint over it. In other words, this list is
! first reversed then being iterated.
! Joints' original point is defined in the document sprite_joints.psd. It also has visual representations.
# TODO right now accessory points are explicitly defined. Should they be injected in run-time?
SKELETON_STAND=HEADGEAR 0,32;HAIR_FORE 0,32;\
ARM_REST_RIGHT -7,23;HAND_REST_RIGHT -6,11;HELD_ITEM -6,11;\
HAIR 0,32;HEAD 0,32;\
UPPER_TORSO 0,23;LOWER_TORSO 0,15;\
FOOT_RIGHT -2,2;LEG_REST_RIGHT -2,7;\
FOOT_LEFT 2,2;LEG_REST_LEFT 2,7;\
ARM_REST_LEFT 5,24;HAND_REST_LEFT 6,12
# skeleton_stand is used for testing purpose
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_REST_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_REST_LEFT -1,0;FOOT_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_REST_LEFT 0,1;FOOT_LEFT 0,1
ANIM_RUN_3=LEG_REST_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_REST_LEFT 1,1;FOOT_LEFT 1,1
ANIM_RUN_4=ALL 0,1;LEG_REST_RIGHT 0,1;FOOT_RIGHT 0,1;LEG_REST_LEFT 0,-1;FOOT_LEFT 0,-1
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;HAIR 0,-1;HELD_ITEM 0,-1;ARM_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_LEFT 0,-1;HAND_REST_RIGHT 0,-1;HAIR_FORE 0,-1;HEADGEAR 0,-1