package net.torvald.terrarum.spriteassembler import com.badlogic.gdx.files.FileHandle import net.torvald.terrarum.App.printdbg 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.* import kotlin.math.max 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) { 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" } /** * If this class is held by a IngamePlayer, this won't get serialised (will be read from a file on loading); * if this class is held by a non-player, this will get serialised. */ class ADProperties { private var fileFrom = "" @Transient private var adlString = "" private val javaProp = Properties() /** Every key is CAPITALISED */ private val propTable = HashMap>() /** list of bodyparts used by all the skeletons (HEAD, UPPER_TORSO, LOWER_TORSO) */ lateinit var bodyparts: List; private set /** [bodyparts] but in a full file path */ lateinit var bodypartFiles: List; private set /** properties that are being used as skeletons (SKELETON_STAND) */ internal lateinit var skeletons: HashMap; private set /** properties that defines position of joint of the bodypart */ internal val bodypartJoints = HashMap() /** properties that are recognised as animations (ANIM_RUN, ANIM_IDLE) */ internal lateinit var animations: HashMap; private set /** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */ internal lateinit var transforms: HashMap>; 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" const val EXTRA_HEADROOM_X = 32 const val EXTRA_HEADROOM_Y = 16 } 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 + EXTRA_HEADROOM_X frameHeight = frameSizeVec.y + EXTRA_HEADROOM_Y originX = (get("CONFIG").linearSearchBy { it.name == "ORIGINX" }!!.input as Float).toInt() var maxColFinder = -1 var maxRowFinder = -1 val skeletons = HashMap() val animations = HashMap() val animFrames = HashMap() val transforms = HashMap>() // 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] = max(animFrames[animName]!!, frameNumber) } else { animFrames[animName] = frameNumber } maxColFinder = max(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() 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 = max(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.bodyparts = this.bodypartJoints.keys.toList() this.bodypartFiles = this.bodypartJoints.keys.map { toFilename(it) } this.transforms = transforms cols = maxColFinder rows = maxRowFinder } operator fun get(identifier: String): List = propTable[identifier.toUpperCase()]!! val keys get() = propTable.keys fun containsKey(key: String) = propTable.containsKey(key) fun forEach(predicate: (String, List) -> 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] ?: throw NullPointerException("No skeleton with name '$name'") internal fun getTransform(name: String) = transforms[name] ?: throw NullPointerException("No tranform with name '$name'") /** * Removes number suffix from the animation name */ private fun getAnimNameFromFrame(s: String): String { val ret = if (s.matches(reAnimWithNumber)) s.substringBeforeLast('_') else s // printdbg(this, "getAnimNameFromFrame $s -> $ret") return ret } private val reAnimWithNumber = Regex("""ANIM_[A-Z]+_[0-9]+""") private fun List.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) } 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()}" } }