com.soywiz.korge.ext.swf.Swf.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of korge-ext-swf Show documentation
Show all versions of korge-ext-swf Show documentation
Korge: Kotlin cORoutines Game Engine
package com.soywiz.korge.ext.swf
import com.codeazur.as3swf.SWF
import com.codeazur.as3swf.data.GradientType
import com.codeazur.as3swf.data.actions.ActionGotoFrame
import com.codeazur.as3swf.data.actions.ActionPlay
import com.codeazur.as3swf.data.actions.ActionStop
import com.codeazur.as3swf.data.consts.BitmapFormat
import com.codeazur.as3swf.data.consts.GradientInterpolationMode
import com.codeazur.as3swf.data.consts.GradientSpreadMode
import com.codeazur.as3swf.data.consts.LineCapsStyle
import com.codeazur.as3swf.exporters.LoggerShapeExporter
import com.codeazur.as3swf.exporters.ShapeExporter
import com.codeazur.as3swf.tags.*
import com.soywiz.korau.format.AudioFormats
import com.soywiz.korfl.abc.*
import com.soywiz.korge.animate.*
import com.soywiz.korge.resources.Path
import com.soywiz.korge.resources.ResourcesRoot
import com.soywiz.korge.view.Views
import com.soywiz.korim.bitmap.*
import com.soywiz.korim.color.BGRA
import com.soywiz.korim.color.BGRA_5551
import com.soywiz.korim.color.RGB
import com.soywiz.korim.color.RGBA
import com.soywiz.korim.format.readBitmap
import com.soywiz.korim.vector.Context2d
import com.soywiz.korim.vector.GraphicsPath
import com.soywiz.korio.error.ignoreErrors
import com.soywiz.korio.inject.AsyncFactory
import com.soywiz.korio.inject.AsyncFactoryClass
import com.soywiz.korio.stream.openAsync
import com.soywiz.korio.util.Extra
import com.soywiz.korio.util.extract8
import com.soywiz.korio.util.substr
import com.soywiz.korio.util.toIntCeil
import com.soywiz.korio.vfs.VfsFile
import com.soywiz.korma.Matrix2d
import com.soywiz.korma.geom.Rectangle
import kotlin.collections.set
@AsyncFactoryClass(SwfLibraryFactory::class)
class SwfLibrary(val an: AnLibrary)
class SwfLibraryFactory(
val path: Path,
val resourcesRoot: ResourcesRoot,
val views: Views
) : AsyncFactory {
suspend override fun create(): SwfLibrary = SwfLibrary(resourcesRoot[path].readSWF(views))
//suspend override fun create(): SwfLibrary = SwfLibrary(AnimateDeserializer.read(AnimateSerializer.gen(ResourcesVfs[path.path].readSWF(views), compression = 0.0), views))
}
inline val TagPlaceObject.depth0: Int get() = this.depth - 1
inline val TagRemoveObject.depth0: Int get() = this.depth - 1
val SWF.bitmaps by Extra.Property { hashMapOf() }
class MySwfFrameElement(
val depth: Int,
val uid: Int,
val name: String?,
val transform: Matrix2d.Computed,
val alpha: Double
) {
fun toAnSymbolTimelineFrame() = AnSymbolTimelineFrame(uid, transform, name, alpha)
}
class MySwfFrame(val index: Int, maxDepths: Int) {
var name: String? = null
val depths = arrayListOf()
val actions = arrayListOf()
interface Action {
object Stop : Action
object Play : Action
class Goto(val frame0: Int) : Action
class PlaySound(val soundId: Int) : Action
}
val isFirst: Boolean get() = index == 0
val hasName: Boolean get() = name != null
val hasStop: Boolean get() = Action.Stop in actions
val hasGoto: Boolean get() = actions.any { it is Action.Goto }
fun stop() = run { actions += Action.Stop }
fun play() = run { actions += Action.Play }
fun goto(frame: Int) = run { actions += Action.Goto(frame) }
fun gotoAndStop(frame: Int) = run { goto(frame); stop() }
fun gotoAndPlay(frame: Int) = run { goto(frame); play() }
fun playSound(soundId: Int) = run { actions += Action.PlaySound(soundId) }
}
class MySwfTimeline {
val frames = arrayListOf()
}
internal val AnSymbolMovieClip.swfTimeline by Extra.Property { MySwfTimeline() }
internal val AnSymbolMovieClip.labelsToFrame0 by Extra.Property { hashMapOf() }
private class SwfLoaderMethod(val views: Views, val debug: Boolean) {
lateinit var swf: SWF
lateinit var lib: AnLibrary
val classNameToTypes = hashMapOf()
val classNameToTagId = hashMapOf()
val shapesToPopulate = arrayListOf>()
suspend fun load(data: ByteArray): AnLibrary {
swf = SWF().loadBytes(data)
lib = AnLibrary(views, swf.frameRate)
parseMovieClip(swf.tags, AnSymbolMovieClip(0, "MainTimeLine", findLimits(swf.tags)))
for (symbol in symbols) lib.addSymbol(symbol)
processAs3Actions()
generateActualTimelines()
lib.processSymbolNames()
generateTextures()
return lib
}
fun getFrameTime(index0: Int) = (index0 * lib.msPerFrameDouble).toInt() * 1000
suspend private fun generateActualTimelines() {
for (symbol in lib.symbolsById.filterIsInstance()) {
val swfTimeline = symbol.swfTimeline
var currentState = AnSymbolMovieClipState(symbol.limits.totalDepths)
var justAfterStop = true
var stateStartFrame = 0
//println(swfTimeline.frames)
for (frame in swfTimeline.frames) {
//println("Frame:(${frame.index})")
// Create State
if (justAfterStop) {
justAfterStop = false
stateStartFrame = frame.index
currentState = AnSymbolMovieClipState(symbol.limits.totalDepths)
symbol.states["frame${frame.index}"] = AnSymbolMovieClipStateWithStartTime(currentState, 0)
}
val frameInState = frame.index - stateStartFrame
val currentTime = getFrameTime(frameInState)
val isLast = frame.index >= swfTimeline.frames.size - 1
// Register State
if (frame.isFirst) symbol.states["default"] = AnSymbolMovieClipStateWithStartTime(currentState, currentTime)
if (frame.hasName) symbol.states[frame.name!!] = AnSymbolMovieClipStateWithStartTime(currentState, currentTime)
// Compute frame
for (depth in frame.depths) {
currentState.timelines[depth.depth].add(currentTime, depth.toAnSymbolTimelineFrame())
}
// Compute actions
val anActions = arrayListOf()
for (it in frame.actions) {
when (it) {
is MySwfFrame.Action.PlaySound -> {
anActions += AnPlaySoundAction(it.soundId)
}
}
}
if (anActions.isNotEmpty()) currentState.actions.add(currentTime, AnActions(anActions))
if (isLast || frame.hasStop || frame.hasGoto) {
//println(" - $isLast,${frame.hasStop},${frame.hasGoto}")
justAfterStop = true
if (frame.hasStop) {
currentState.loopStartTime = currentTime
}
if (frame.hasGoto) {
val goto = frame.actions.filterIsInstance().first()
currentState.loopStartTime = getFrameTime(goto.frame0 - stateStartFrame)
}
val stateEndFrame = frameInState
currentState.totalTime = getFrameTime(stateEndFrame - stateStartFrame)
}
}
}
}
suspend private fun processAs3Actions() {
for ((className, tagId) in classNameToTagId) {
lib.symbolsById[tagId].name = className
val type = classNameToTypes[className] ?: continue
val symbol = (lib.symbolsById[tagId] as AnSymbolMovieClip?) ?: continue
val abc = type.abc
val labelsToFrame0 = symbol.labelsToFrame0
//println("$tagId :: $className :: $symbol :: $type")
for (trait in type.instanceTraits) {
val simpleName = trait.name.simpleName
//println(" - " + trait.name.simpleName)
if (simpleName.startsWith("frame")) {
val frame = ignoreErrors { simpleName.substr(5).toInt() } ?: continue
val frame0 = frame - 1
val traitMethod = (trait as ABC.TraitMethod?) ?: continue
val methodDesc = abc.methodsDesc[traitMethod.methodIndex]
val body = methodDesc.body ?: continue
//println("FRAME: $frame0")
//println(body.ops)
var lastValue: Any? = null
for (op in body.ops) {
when (op.opcode) {
AbcOpcode.PushByte -> lastValue = (op as AbcIntOperation).value
AbcOpcode.PushShort -> lastValue = (op as AbcIntOperation).value
AbcOpcode.PushInt -> lastValue = (op as AbcIntOperation).value
AbcOpcode.PushUInt -> lastValue = (op as AbcIntOperation).value
AbcOpcode.PushString -> lastValue = (op as AbcStringOperation).value
AbcOpcode.CallPropVoid -> {
val call = (op as AbcMultinameIntOperation)
val callMethodName = call.multiname.simpleName
val frameData = symbol.swfTimeline.frames[frame0]
when (callMethodName) {
"gotoAndPlay", "gotoAndStop" -> {
val gotoFrame0 = when (lastValue) {
is String -> labelsToFrame0[lastValue] ?: 0
is Int -> lastValue - 1
else -> 0
}
if (callMethodName == "gotoAndStop") {
frameData.gotoAndStop(gotoFrame0)
} else {
frameData.gotoAndPlay(gotoFrame0)
}
}
"play" -> frameData.play()
"stop" -> frameData.stop()
else -> {
//println("method: $callMethodName")
}
}
lastValue = null
}
else -> Unit
}
}
}
}
}
}
suspend private fun generateTextures() {
val atlas = shapesToPopulate.map {
it.second.image
}.toAtlas(views)
for ((shape, texture) in shapesToPopulate.map { it.first }.zip(atlas)) shape.textureWithBitmap = texture
}
fun findLimits(tags: Iterable): AnSymbolLimits {
var maxDepth = -1
var totalFrames = 0
val items = hashSetOf>()
// Find limits
for (it in tags) {
when (it) {
is TagPlaceObject -> {
if (it.hasCharacter) {
items += it.depth0 to it.characterId
}
maxDepth = Math.max(maxDepth, it.depth0)
}
is TagShowFrame -> {
totalFrames++
}
}
}
return AnSymbolLimits(maxDepth + 1, totalFrames, items.size, (totalFrames * lib.msPerFrameDouble).toInt())
}
val symbols = arrayListOf()
fun registerBitmap(charId: Int, bmp: Bitmap, name: String? = null) {
swf.bitmaps[charId] = bmp
symbols += AnSymbolBitmap(charId, name, bmp)
//showImageAndWait(bmp)
}
suspend fun parseMovieClip(tags: Iterable, mc: AnSymbolMovieClip) {
symbols += mc
val swfTimeline = mc.swfTimeline
val labelsToFrame0 = mc.labelsToFrame0
val uniqueIds = hashMapOf, Int>()
class DepthInfo(val depth: Int) {
var uid: Int = -1
var charId: Int = -1
var name: String? = null
var alpha: Double = 1.0
var matrix: Matrix2d = Matrix2d()
fun reset() {
uid = -1
charId = -1
name = null
matrix = Matrix2d()
}
fun toFrameElement(): MySwfFrameElement = MySwfFrameElement(
depth = depth,
uid = uid,
name = name,
transform = Matrix2d.Computed(matrix),
alpha = alpha
)
}
val depths = Array(mc.limits.totalDepths) { DepthInfo(it) }
fun getUid(depth: Int): Int {
val charId = depths[depth].charId
return uniqueIds.getOrPut(depth to charId) {
val uid = uniqueIds.size
mc.uidInfo[uid] = AnSymbolUidDef(charId)
uid
}
}
// Add frames and read labels information
for (it in tags) {
val currentFrame = swfTimeline.frames.size
when (it) {
is TagDefineSceneAndFrameLabelData -> {
mc.labelsToFrame0 += it.frameLabels.map { it.name to it.frameNumber - 1 }
}
is TagFrameLabel -> {
mc.labelsToFrame0[it.frameName] = currentFrame
}
is TagShowFrame -> {
swfTimeline.frames += MySwfFrame(currentFrame, mc.limits.totalDepths)
}
}
}
// Populate frame names
for ((name, index) in mc.labelsToFrame0) swfTimeline.frames[index].name = name
var currentFrame = 0
for (it in tags) {
//println("Tag: $it")
val currentTime = getFrameTime(currentFrame)
val swfCurrentFrame by lazy { mc.swfTimeline.frames[currentFrame] }
when (it) {
is TagDefineSceneAndFrameLabelData -> Unit
is TagFrameLabel -> Unit
is TagFileAttributes -> Unit
is TagSetBackgroundColor -> {
lib.bgcolor = decodeSWFColor(it.color)
}
is TagDefineFont -> {
}
is TagDefineFontName -> {
}
is TagDefineFontAlignZones -> {
}
is TagDefineEditText -> {
symbols += AnTextFieldSymbol(it.characterId, null, it.initialText ?: "", it.bounds.rect)
}
is TagCSMTextSettings -> {
}
is TagDoAction -> {
for (action in it.actions) {
when (action) {
is ActionStop -> swfCurrentFrame.stop()
is ActionPlay -> swfCurrentFrame.play()
is ActionGotoFrame -> swfCurrentFrame.goto(action.frame)
}
}
}
is TagSoundStreamHead -> {
}
is TagDefineSound -> {
val soundBytes = it.soundData.cloneToNewByteArray()
val audioData = try {
AudioFormats.decode(soundBytes.openAsync())
} catch (e: Throwable) {
e.printStackTrace()
null
}
symbols += AnSymbolSound(it.characterId, null, audioData)
//LocalVfs("c:/temp/temp.mp3").write()
}
is TagStartSound -> {
swfCurrentFrame.playSound(it.soundId)
}
is TagDefineBits, is TagDefineBitsLossless -> {
var fbmp: Bitmap = Bitmap32(1, 1)
it as IDefinitionTag
when (it) {
is TagDefineBitsJPEG2 -> {
val bitsData = it.bitmapData.cloneToNewByteArray()
val nativeBitmap = bitsData.openAsync().readBitmap()
//println(nativeBitmap)
val bmp = nativeBitmap.toBMP32()
fbmp = bmp
if (it is TagDefineBitsJPEG3) {
val fmaskinfo = it.bitmapAlphaData.cloneToNewFlashByteArray()
fmaskinfo.uncompress("zlib")
val maskinfo = fmaskinfo.cloneToNewByteArray()
//val bmpAlpha = nativeImageFormatProvider.decode(maskinfo)
//showImageAndWait(bmpAlpha)
bmp.writeChannel(BitmapChannel.ALPHA, Bitmap8(bmp.width, bmp.height, maskinfo))
}
//showImageAndWait(bmp)
//println(bmp)
//for (y in 0 until bmp.height) {
// for (x in 0 until bmp.width) System.out.printf("%08X,", bmp[x, y])
// println()
//}
}
is TagDefineBitsLossless -> {
val isRgba = it.hasAlpha
val funcompressedData = it.zlibBitmapData.cloneToNewFlashByteArray()
funcompressedData.uncompress("zlib")
val uncompressedData = funcompressedData.cloneToNewByteArray()
when (it.bitmapFormat) {
BitmapFormat.BIT_8 -> {
val bmp = Bitmap8(it.bitmapWidth, it.bitmapHeight)
fbmp = bmp
}
BitmapFormat.BIT_15 -> {
fbmp = Bitmap32(it.bitmapWidth, it.bitmapHeight, BGRA_5551.decode(uncompressedData))
}
BitmapFormat.BIT_24_32 -> {
val colorFormat = if (isRgba) BGRA else RGB
fbmp = Bitmap32(it.bitmapWidth, it.bitmapHeight, colorFormat.decode(uncompressedData, littleEndian = false))
}
else -> Unit
}
}
}
registerBitmap(it.characterId, fbmp, null)
}
is TagDefineShape -> {
val rasterizer = SWFShapeRasterizer(swf, debug, it)
val symbol = AnSymbolShape(it.characterId, null, rasterizer.bounds, null, rasterizer.path)
symbols += symbol
shapesToPopulate += symbol to rasterizer
}
is TagDoABC -> {
classNameToTypes += it.abc.typesInfo.map { it.name.toString() to it }.toMap()
}
is TagSymbolClass -> {
classNameToTagId += it.symbols.filter { it.name != null }.map { it.name!! to it.tagId }.toMap()
}
is TagDefineSprite -> {
parseMovieClip(it.tags, AnSymbolMovieClip(it.characterId, null, findLimits(it.tags)))
}
is TagPlaceObject -> {
val depth = depths[it.depth0]
if (it.hasCharacter) depth.charId = it.characterId
if (it.hasName) depth.name = it.instanceName
//if (it.hasBlendMode) depth.blendMode = it.blendMode
if (it.hasColorTransform) {
depth.alpha = it.colorTransform!!.aMult
}
if (it.hasMatrix) depth.matrix = it.matrix!!.matrix
depth.uid = getUid(it.depth0)
}
is TagRemoveObject -> {
depths[it.depth0].reset()
}
is TagShowFrame -> {
for (depth in depths) {
swfCurrentFrame.depths += depth.toFrameElement()
}
currentFrame++
}
is TagEnd -> {
}
else -> {
println("Unhandled tag $it")
}
}
}
}
}
object SwfLoader {
suspend fun load(views: Views, data: ByteArray, debug: Boolean = false): AnLibrary = SwfLoaderMethod(views, debug).load(data)
}
fun decodeSWFColor(color: Int, alpha: Double = 1.0) = RGBA.pack(color.extract8(16), color.extract8(8), color.extract8(0), (alpha * 255).toInt())
class SWFShapeRasterizer(val swf: SWF, val debug: Boolean, val shape: TagDefineShape) : ShapeExporter() {
val bounds: Rectangle = shape.shapeBounds.rect
//val bmp = Bitmap32(bounds.width.toIntCeil(), bounds.height.toIntCeil())
private val _image by lazy { NativeImage(Math.max(1, bounds.width.toIntCeil()), Math.max(1, bounds.height.toIntCeil())) }
val image by lazy {
shape.export(if (debug) LoggerShapeExporter(this) else this)
_image
}
val path = GraphicsPath()
var processingFills = false
val ctx by lazy {
_image.getContext2d().apply {
translate(-bounds.x, -bounds.y)
}
}
override fun beginShape() {
//ctx.beginPath()
}
override fun endShape() {
//ctx.closePath()
}
override fun beginFills() {
processingFills = true
ctx.beginPath()
}
override fun endFills() {
processingFills = false
}
override fun beginLines() {
ctx.beginPath()
}
override fun closePath() {
ctx.closePath()
if (processingFills) path.close()
}
override fun endLines() {
ctx.stroke()
}
override fun beginFill(color: Int, alpha: Double) {
ctx.fillStyle = Context2d.Color(decodeSWFColor(color, alpha))
}
fun GradientSpreadMode.toCtx() = when (this) {
GradientSpreadMode.PAD -> Context2d.CycleMethod.NO_CYCLE
GradientSpreadMode.REFLECT -> Context2d.CycleMethod.REFLECT
GradientSpreadMode.REPEAT -> Context2d.CycleMethod.REPEAT
}
override fun beginGradientFill(type: GradientType, colors: List, alphas: List, ratios: List, matrix: Matrix2d, spreadMethod: GradientSpreadMode, interpolationMethod: GradientInterpolationMode, focalPointRatio: Double) {
//matrix.scale(100.0, 100.0)
//this.createBox(width / 1638.4, height / 1638.4, rotation, tx + width / 2, ty + height / 2);
val transform = Matrix2d.Transform().setMatrix(matrix)
val width = transform.scaleX * 1638.4
val height = transform.scaleY * 1638.4
val rotation = transform.rotation
val x = transform.x - width / 2.0
val y = transform.y - height / 2.0
val x0 = x
val y0 = y
val x1 = x + width * Math.cos(rotation)
val y1 = y + height * Math.sin(rotation)
val aratios = ArrayList(ratios.map { it.toDouble() / 255.0 })
val acolors = ArrayList(colors.zip(alphas).map { decodeSWFColor(it.first, it.second) })
when (type) {
GradientType.LINEAR -> {
ctx.fillStyle = Context2d.LinearGradient(x0, y0, x1, y1, aratios, acolors, spreadMethod.toCtx())
}
GradientType.RADIAL -> {
val r0 = 0.0
val r1 = Math.max(width, height)
ctx.fillStyle = Context2d.RadialGradient(x0, y0, r0, x1, y1, r1, aratios, acolors, spreadMethod.toCtx())
}
}
//ctx.fillStyle = Context2d.Color(decodeSWFColor(color, alpha))
//super.beginGradientFill(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio)
}
override fun beginBitmapFill(bitmapId: Int, matrix: Matrix2d, repeat: Boolean, smooth: Boolean) {
val bmp = swf.bitmaps[bitmapId] ?: Bitmap32(1, 1)
ctx.fillStyle = Context2d.BitmapPaint(bmp, matrix, repeat, smooth)
//println(matrix)
//ctx.fillStyle = Context2d.Bitmap()
//super.beginBitmapFill(bitmapId, matrix, repeat, smooth)
}
override fun endFill() {
ctx.fill()
}
override fun lineStyle(thickness: Double, color: Int, alpha: Double, pixelHinting: Boolean, scaleMode: String, startCaps: LineCapsStyle, endCaps: LineCapsStyle, joints: String?, miterLimit: Double) {
ctx.lineWidth = thickness
ctx.strokeStyle = Context2d.Color(decodeSWFColor(color, alpha))
ctx.lineCap = when (startCaps) {
LineCapsStyle.NO -> Context2d.LineCap.BUTT
LineCapsStyle.ROUND -> Context2d.LineCap.ROUND
LineCapsStyle.SQUARE -> Context2d.LineCap.SQUARE
}
}
override fun lineGradientStyle(type: GradientType, colors: List, alphas: List, ratios: List, matrix: Matrix2d, spreadMethod: GradientSpreadMode, interpolationMethod: GradientInterpolationMode, focalPointRatio: Double) {
super.lineGradientStyle(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio)
}
override fun moveTo(x: Double, y: Double) {
ctx.moveTo(x, y)
if (processingFills) path.moveTo(x, y)
}
override fun lineTo(x: Double, y: Double) {
ctx.lineTo(x, y)
if (processingFills) path.lineTo(x, y)
}
override fun curveTo(controlX: Double, controlY: Double, anchorX: Double, anchorY: Double) {
ctx.quadraticCurveTo(controlX, controlY, anchorX, anchorY)
if (processingFills) path.quadTo(controlX, controlY, anchorX, anchorY)
}
}
suspend fun VfsFile.readSWF(views: Views, debug: Boolean = false): AnLibrary = SwfLoader.load(views, this.readAll(), debug = debug)