From 245a3f314ca6fd25d96648ec7f7d2225bbd9db7a Mon Sep 17 00:00:00 2001 From: RHY3756547 Date: Thu, 22 Mar 2018 01:30:10 +0000 Subject: [PATCH] Reverse engineered SPA format A gift from me to the Mario Kart DS community (i think these are used in Pokemon too). Might work on ingame particle emitters now that I'm the only person ever to have tried reading these stupid files. --- code/engine/ingameRes.js | 1 + code/formats/spa.js | 479 +++++++++++++++++++++++++++++++++++++++ index.html | 2 +- 3 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 code/formats/spa.js diff --git a/code/engine/ingameRes.js b/code/engine/ingameRes.js index 1269aea..d5edb0a 100644 --- a/code/engine/ingameRes.js +++ b/code/engine/ingameRes.js @@ -43,6 +43,7 @@ window.IngameRes = function(rom) { var characters = []; var karts = []; + var test = new spa(r.MainEffect.getFile("RaceEffect.spa")); loadItems(); loadTires(); diff --git a/code/formats/spa.js b/code/formats/spa.js new file mode 100644 index 0000000..d091d31 --- /dev/null +++ b/code/formats/spa.js @@ -0,0 +1,479 @@ +// +// spa.js +//-------------------- +// Reads spa files. Based off of code from MJDS Course Modifier, which was very incomplete but at least got the textures. +// Reverse engineered most of the emitter stuff. +// by RHY3756547 +// + +window.spa = function(input) { + var t = this; + this.load = load; + + var colourBuffer; + + function load(input) { + colourBuffer = new Uint32Array(4); + var view = new DataView(input); + var header = null; + var offset = 0; + + var stamp = readChar(view, 0x0)+readChar(view, 0x1)+readChar(view, 0x2)+readChar(view, 0x3); + if (stamp != " APS") throw "SPA invalid. Expected ' APS', found "+stamp; + offset += 4; + + var version = readChar(view, offset)+readChar(view, offset+1)+readChar(view, offset+2)+readChar(view, offset+3); + offset += 4; + + var particleCount = view.getUint16(offset, true); + var particleTexCount = view.getUint16(offset+2, true); + var unknown = view.getUint32(offset+4, true); + var unknown2 = view.getUint32(offset+8, true); + var unknown3 = view.getUint32(offset+12, true); + + var firstTexOffset = view.getUint32(offset+16, true); + var pad = view.getUint32(offset+20, true); + + offset += 24; + if (version == "12_1") { + this.particles = []; + for (let i=0; i1 means more than one particle will appear. + obj.areaSpread = view.getInt32(off+0x14, true)/4096; //x and z + //^ particle count? + obj.unknown3 = view.getInt32(off+0x18, true)/4096; //unknown (does not change anything for grass) + + //not sure what this vector is for. grass it's (0, 1, 0), smoke it's (-0.706787109375, 0, -0.707275390625) billboard alignment vector? (it's a bit crazy for powerslide) + obj.vector = [view.getInt16(off+0x1C, true)/4096, view.getInt16(off+0x1E, true)/4096, view.getInt16(off+0x20, true)/4096]; + obj.color = view.getUint16(off+0x22, true); //15 bit, usually 32767 for white. + obj.randomxz = view.getUint32(off+0x24, true); //random xz velocity intensity + obj.velocity = view.getUint32(off+0x28, true); //initial velocity related + obj.size = view.getUint32(off+0x2C, true)/4096; //size + obj.aspect = view.getUint16(off+0x30, true) / 4096; //aspect + + //frame delay before activation (x2) + //rotational velocity from (x2) + //rotational velocity to (x2) + obj.delay = view.getUint16(off+0x32, true); + obj.rotVelFrom = view.getInt16(off+0x34, true); + obj.rotVelTo = view.getInt16(off+0x36, true); + + obj.unknown13 = view.getUint16(off+0x38, true); //??? (0) + obj.unknown14 = view.getUint16(off+0x3A, true); //??? (4B) + obj.emitterLifetime = view.getUint16(off+0x3C, true); //stop emitting particles after this many frames + obj.duration = view.getUint16(off+0x3E, true); + + obj.varScale = view.getUint8(off+0x40, true); + obj.varDuration = view.getUint8(off+0x42, true); + obj.varUnk1 = view.getUint8(off+0x44, true); //usually like 1-8 + obj.varUnk2 = view.getUint8(off+0x46, true); //usually like 128 (hahaa) + + obj.frequency = view.getUint8(off+0x44, true); //create particle every n frames + obj.opacity = view.getUint8(off+0x45, true); //opacity (0-1F) + obj.yOffIntensity = view.getUint8(off+0x46, true); //y offset intensity (seems to include updraft and gravity. 124 for smoke, 120 for grass. 128 is probably 1x) + obj.textureId = view.getUint8(off+0x47, true); + obj.unknown21 = view.getUint32(off+0x48, true); //negative number makes grass disappear (1 for grass, smoke) + obj.unknown22 = view.getUint32(off+0x4C, true); //some numbers make grass disappear (0x458d00 for grass, 0x74725f60 for smoke) + obj.xScaleDelta = view.getUint16(off+0x50, true); //x scale delta for some reason. usually 0 + obj.yScaleDelta = view.getUint16(off+0x52, true); //y scale delta for some reason. usually 0 + obj.unknown25 = view.getUint32(off+0x54, true); //FFFFFFFF makes run at half framerate. idk? usually 0 + off += 0x58; + + if ((obj.flag & ParticleFlags.ScaleAnim) != 0) + { + obj.scaleAnim = []; + //1.000 (doesn't seem to do anything important, but occasionally is between start and end) + //start scale + //end scale + //???? (seems to affect the interpolation. cubic params?) + //flags (1: random scale for one frame? everything above it might be cubic params) + //???? (0x4B) + + obj.scaleAnim = { + unkBase: view.getUint16(off, true)/4096, + scaleFrom: view.getUint16(off+2, true)/4096, + scaleTo: view.getUint16(off+4, true)/4096, + param: view.getUint16(off+6, true), + flagParam: view.getUint16(off+8, true), + unk4b: view.getUint16(off+10, true), + }; + off += 12; + } + if ((obj.flag & ParticleFlags.ColorAnimation) != 0) + { + obj.ColorAnimation = { + colorFrom: view.getUint16(off, true), //color from + colorTo: view.getUint16(off+2, true), //color to (seems to be same as base color) + framePct: view.getUint16(off+4, true), //frame pct to become color to (FFFF means always from, 8000 is about the middle) + unknown: view.getUint16(off+6, true), //unknown, 00FF for fire? + flags: view.getUint32(off+8, true), //flags (1: binary select color, 4: smooth blend) + }; + off += 12; + } + if ((obj.flag & ParticleFlags.OpacityAnimation) != 0) + { + //opacity + + //intensity x2 (0FFF to 0000. smoke is 0bff. 1000 breaks it, i'm assuming it pushes opacity from 1f to 20 (overflow to 0)) + //random flicker + //unknown (negative byte breaks it) + //startfade x2 + //cubic param? x2 + obj.opacityAnim = { + intensity: view.getUint16(off, true), + random: view.getUint8(off+2, true), + unk: view.getUint8(off+3, true), + startFade: view.getUint16(off+4, true), //0-FFFF. seems to be the pct of duration where the anim starts. + param: view.getUint16(off+6, true), + } + off += 8; + } + if ((obj.flag & ParticleFlags.TextureAnimation) != 0) + { + var textures = []; + for (let i=0; i<8; i++) textures[i] = view.getUint8(off+i); + obj.texAnim = { + textures: textures, + frames: view.getUint8(off+8), + unknown1: view.getUint8(off+9), //128 - duration of particle. 37 - blue spark? (7 frames for 7 duration effect) + unknown2: view.getUint16(off+10, true), //1 - random frame? + } + off += 12; + } + if ((obj.flag & ParticleFlags.Bit16) != 0) + { + obj.Bit16 = []; + for (let i=0; i<20; i++) obj.Bit16[i] = view.getUint8(off+i); + off += 20; + } + if ((obj.flag & ParticleFlags.Gravity) != 0) + { + + //gravity + //x wind + //gravity (signed 16, -1 is down, leaves are FFEA (-22/4096)) + //z wind + //pad? + obj.gravity = [ + view.getInt16(off, true)/4096, + view.getInt16(off+2, true)/4096, + view.getInt16(off+4, true)/4096, + view.getInt16(off+6, true)/4096, //pad, should be ignored by vec3 ops + ]; + + off += 8; + } + if ((obj.flag & ParticleFlags.Bit25) != 0) + { + //seems to be 4 int 16s typically in some kind of pattern. + obj.Bit25 = []; + for (let i=0; i<8; i++) obj.Bit25[i] = view.getUint8(off+i); + off += 8; + } + if ((obj.flag & ParticleFlags.Bit26) != 0) + { + obj.Bit26 = []; + for (let i=0; i<16; i++) obj.Bit26[i] = view.getUint8(off+i); + off += 16; + } + if ((obj.flag & ParticleFlags.Bit27) != 0) + { + obj.Bit27 = []; + for (let i=0; i<4; i++) obj.Bit27[i] = view.getUint8(off+i); + off += 4; + } + if ((obj.flag & ParticleFlags.Bit28) != 0) + { + obj.Bit28 = []; + for (let i=0; i<8; i++) obj.Bit28[i] = view.getUint8(off+i); + off += 8; + } + if ((obj.flag & ParticleFlags.Bit29) != 0) + { + obj.Bit29 = []; + for (let i=0; i<16; i++) obj.Bit29[i] = view.getUint8(off+i); + off += 16; + } + + obj.nextOff = off; + return obj; + } + + function readParticleTexture(view, off) { + var obj = {}; + obj.stamp = readChar(view, off+0x0)+readChar(view, off+0x1)+readChar(view, off+0x2)+readChar(view, off+0x3); + if (obj.stamp != " TPS") throw "SPT invalid (particle texture in SPA). Expected ' TPS', found "+obj.stamp; + + var flags = view.getUint16(off+4, true); + obj.info = { + pal0trans: (flags>>3)&1, //weirdly different format + format: ((flags)&7), + height: 8 << ((flags>>8)&0xF), + width: 8 << ((flags>>4)&0xF), + repeatX: (flags>>12)&1, + repeatY: (flags>>13)&1, + flipX: (flags>>14)&1, + flipY: (flags>>15)&1, + } + obj.flags = flags; + + obj.unknown = view.getUint16(off+6, true); + obj.texDataLength = view.getUint32(off+8, true); + obj.palOff = view.getUint32(off+0xC, true); + obj.palDataLength = view.getUint32(off+0x10, true); + obj.unknown2 = view.getUint32(off+0x14, true); + obj.unknown3 = view.getUint32(off+0x18, true); + obj.unknown4 = view.getUint32(off+0x1C, true); + + obj.texData = view.buffer.slice(off+32, off+32+obj.texDataLength); + off += 32+obj.texDataLength; + obj.palData = view.buffer.slice(off, off+obj.palDataLength); + + obj.nextOff = off+obj.palDataLength; + + //var test = readTexWithPal(obj.info, obj); + //document.body.appendChild(test); + + return obj; + } + + +//modified from NSBTX.js - should probably refactor to use be generic between both + + function readTexWithPal(tex, data) { + var format = tex.format; + var trans = tex.pal0trans; + + if (format == 5) return readCompressedTex(tex, data); //compressed 4x4 texture, different processing entirely + + var off = 0;//tex.texOffset; + var palView = new DataView(data.palData); + var texView = new DataView(data.texData); + var palOff = 0;//pal.palOffset; + + var canvas = document.createElement("canvas"); + canvas.width = tex.width; + canvas.height = tex.height; + var ctx = canvas.getContext("2d"); + var img = ctx.getImageData(0, 0, tex.width, tex.height); + + var total = tex.width*tex.height; + var databuf; + for (var i=0; i>5)*(255/7); + + } else if (format == 2) { //2 bit pal + if (i%4 == 0) databuf = texView.getUint8(off++); + col = readPalColour(palView, palOff, (databuf>>((i%4)*2))&3, trans) + + } else if (format == 3) { //4 bit pal + if (i%2 == 0) { + databuf = texView.getUint8(off++); + col = readPalColour(palView, palOff, databuf&15, trans) + } else { + col = readPalColour(palView, palOff, databuf>>4, trans) + } + + } else if (format == 4) { //8 bit pal + col = readPalColour(palView, palOff, texView.getUint8(off++), trans) + + } else if (format == 6) { //A5I3 encoding. 5 bits alpha 3 bits pal index + var dat = texView.getUint8(off++) + col = readPalColour(palView, palOff, dat&7, trans); + col[3] = (dat>>3)*(255/31); + + } else if (format == 7) { //raw color data + col = texView.getUint16(off, true); + colourBuffer[0] = Math.round(((col&31)/31)*255) + colourBuffer[1] = Math.round((((col>>5)&31)/31)*255) + colourBuffer[2] = Math.round((((col>>10)&31)/31)*255) + colourBuffer[3] = Math.round((col>>15)*255); + col = colourBuffer; + off += 2; + + } else { + console.log("texture format is none, ignoring") + return canvas; + } + img.data.set(col, i*4); + } + ctx.putImageData(img, 0, 0) + return canvas; + } + + function readCompressedTex(tex) { //format 5, 4x4 texels. I'll keep this well documented so it's easy to understand. + throw "compressed tex not supported for particles! (unknowns for tex data offsets and lengths?)"; + var off = 0;//tex.texOffset; + var texView = new DataView(compData); //real texture data - 32 bits per 4x4 block (one byte per 4px horizontal line, each descending 1px) + var compView = new DataView(compInfoData); //view into compression info - informs of pallete and parameters. + var palView = new DataView(data.palData); //view into the texture pallete + var compOff = off/2; //info is 2 bytes per block, so the offset is half that of the tex offset. + var palOff = 0;//pal.palOffset; + var transColor = new Uint8Array([0, 0, 0, 0]); //transparent black + + var canvas = document.createElement("canvas"); + canvas.width = tex.width; + canvas.height = tex.height; + var ctx = canvas.getContext("2d"); + var img = ctx.getImageData(0, 0, tex.width, tex.height); + + var w = tex.width>>2; //iterate over blocks, block w and h is /4. + var h = tex.height>>2; + + for (var y=0; y> 14) & 3); + + var finalPo = palOff+addr*4; + var imgoff = x*4+(y*w*16); + for (var iy=0; iy<4; iy++) { + var dat = texView.getUint8(off++); + for (var ix=0; ix<4; ix++) { //iterate over horiz lines + var part = (dat>>(ix*2))&3; + var col; + + switch (mode) { + case 0: //value 3 is transparent, otherwise pal colour + if (part == 3) col = transColor; + else col = readPalColour(palView, finalPo, part); + break; + case 1: //average mode - colour 2 is average of 1st two, 3 is transparent. 0&1 are normal. + if (part == 3) col = transColor; + else if (part == 2) col = readFractionalPal(palView, finalPo, 0.5); + else col = readPalColour(palView, finalPo, part); + break; + case 2: //pal colour + col = readPalColour(palView, finalPo, part); + break; + case 3: //5/8 3/8 mode - colour 2 is 5/8 of col0 plus 3/8 of col1, 3 is 3/8 of col0 plus 5/8 of col1. 0&1 are normal. + if (part == 3) col = readFractionalPal(palView, finalPo, 3/8); + else if (part == 2) col = readFractionalPal(palView, finalPo, 5/8); + else col = readPalColour(palView, finalPo, part); + break; + } + + img.data.set(col, (imgoff++)*4) + } + imgoff += tex.width-4; + } + compOff += 2; //align off to next block + } + } + + ctx.putImageData(img, 0, 0) + return canvas; + } + + function readPalColour(view, palOff, ind, pal0trans) { + var col = view.getUint16(palOff+ind*2, true); + var f = 255/31; + colourBuffer[0] = Math.round((col&31)*f) + colourBuffer[1] = Math.round(((col>>5)&31)*f) + colourBuffer[2] = Math.round(((col>>10)&31)*f) + colourBuffer[3] = (pal0trans && ind == 0)?0:255; + return colourBuffer; + } + + function readFractionalPal(view, palOff, i) { + var col = view.getUint16(palOff, true); + var col2 = view.getUint16(palOff+2, true); + var ni = 1-i; + var f = 255/31; + colourBuffer[0] = Math.round((col&31)*f*i + (col2&31)*f*ni) + colourBuffer[1] = Math.round(((col>>5)&31)*f*i + ((col2>>5)&31)*f*ni) + colourBuffer[2] = Math.round(((col>>10)&31)*f*i + ((col2>>10)&31)*f*ni) + colourBuffer[3] = 255; + return colourBuffer; + } + + //end NSBTX + + + function readChar(view, offset) { + return String.fromCharCode(view.getUint8(offset)); + } + + if (input != null) { + load(input); + } +} \ No newline at end of file diff --git a/index.html b/index.html index 3faa682..dcf0e9a 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file