diff --git a/gltf-exporter.js b/gltf-exporter.js index f9f314c..a48dcce 100644 --- a/gltf-exporter.js +++ b/gltf-exporter.js @@ -1,346 +1,346 @@ -const icondumper2 = require("./icon.js"); -const iconjs = icondumper2.readers; -const filesystem = require("fs"); -const processObj = require("process"); - -const gltfConstants = { - "FLOAT": 5126, - "ARRAY_BUFFER": 34962, - "LINEAR": 9729, - "NEAREST_MIPMAP_LINEAR": 9986, - "REPEAT": 10497 -}; - -function getCrc32(data) { - let output = -1; - for (let byteIndex of data) { - for (let index = 0; index < 8; index++, byteIndex >>>= 1) { - output = (output >>> 1) ^ (-((output ^ byteIndex) & 1) & 0xEDB88320); - } // 0xEDB88320 is a reverse polynomial that's common in crc32 - } //i know it's some bitwise madness, it works, either way. - return ((~output) >>> 0); -} - -function getAdler32(data) { - let s1 = 1; - let s2 = 0; - for (let index of data) { - s1 = (s1 + index) % 65521; - s2 = (s2 + s1) % 65521; - } - return (s2 << 16) | s1; -} - -function rgb5a1_rgb8(colour) { - let b = ( colour & 0b11111); - let g = ((colour >> 5) & 0b11111); - let r = ((colour >> 10) & 0b11111); - let output = new Number(); - output |= ((r * 8) << 16); - output |= ((g * 8) << 8); - output |= ((b * 8) << 0); - return output; -} - -function imf2gltf(icon = null, filename = "untitled") { - if (icon === null) { - throw "Missing first argument, of which should be a icondumper2 Intermediate Model Format object."; - } - if (icon.hasOwnProperty("numberOfShapes") === false) { - throw "Expected a icondumper2 Intermediate Model Format object."; - } - let shapesArray = new Array(icon.numberOfShapes); - for (let index = 0; index < icon.numberOfShapes; index++) { - shapesArray[index] = new Array(); - } - let verticesArray = new Array(); - let normalsArray = new Array(); - let uvArray = new Array(); - let colourArray = new Array(); - icon.vertices.forEach(function(vertexObject){ - for (let index = 0; index < icon.numberOfShapes; index++) { - shapesArray[index].push(vertexObject.shapes[index].x); - shapesArray[index].push(vertexObject.shapes[index].y); - shapesArray[index].push(vertexObject.shapes[index].z); - } - normalsArray.push(vertexObject.normal.x); - normalsArray.push(vertexObject.normal.y); - normalsArray.push(vertexObject.normal.z); - uvArray.push(vertexObject.uv.u); - uvArray.push(vertexObject.uv.v); - // gamma correction, glTF clients expect lineari(s|z)ed-sRGB, not sRGB. - colourArray.push(Math.pow((vertexObject.color.r/255), 2.2)); - colourArray.push(Math.pow((vertexObject.color.g/255), 2.2)); - colourArray.push(Math.pow((vertexObject.color.b/255), 2.2)); - colourArray.push((vertexObject.color.a > 1) ? (vertexObject.color.a/255): 1); - }); - shapesArray.forEach(function(arr) { - verticesArray = [...verticesArray, ...arr]; - }); - let outputFloatArray = new Float32Array([...verticesArray, ...normalsArray, ...uvArray, ...colourArray]); // 3[nOS], 3, 2, 4# - let gltfOutputArray = new Array(icon.numberOfShapes); - for (let index = 0; index < icon.numberOfShapes; index++) { - const gltfOutput = new Object(); - //setting up GLTF - gltfOutput.scene = 0; - gltfOutput.scenes = [{"name": filename, "nodes": [0]}]; - gltfOutput.nodes = [{"mesh": 0, "name": `${filename}#${index}`, "rotation": [1,0,0,0]}]; - gltfOutput.meshes = [{ - "name": `Mesh (${filename}#${index})`, - "primitives": [{ - "attributes": { - "POSITION": 0, - "NORMAL": 1, - "TEXCOORD_0": 2, - "COLOR_0": 3, - }, - "material": 0 - }] - }]; // no indices because who needs indexing when you're transcoding? - gltfOutput.materials = [{ - "name": `Material (${filename}#${index})`, - "pbrMetallicRoughness": { - "baseColorTexture": {"index":0, "texCoord": 0} - }, - "extensions": { // or we get annoying PBR and specular stuff we don't need - "KHR_materials_unlit": {} - } - }]; - gltfOutput.buffers = [{"uri": `${filename}.bin`, "byteLength": outputFloatArray.byteLength}]; - gltfOutput.bufferViews = [ - { - "buffer": 0, - "byteOffset": (((icon.vertices.length*3)*4)*index), - "byteLength": ((icon.vertices.length*3)*4), - "target": gltfConstants.ARRAY_BUFFER - }, - { - "buffer": 0, - "byteOffset": (((icon.vertices.length*3)*4)*icon.numberOfShapes), - "byteLength": (normalsArray.length*4), - "target": gltfConstants.ARRAY_BUFFER - }, - { - "buffer": 0, - "byteOffset": ((((icon.vertices.length*3)*4)*icon.numberOfShapes)+(normalsArray.length*4)), - "byteLength": (uvArray.length*4), - "target": gltfConstants.ARRAY_BUFFER - }, - { - "buffer": 0, - "byteOffset": (((((icon.vertices.length*3)*4)*icon.numberOfShapes)+(normalsArray.length*4))+(uvArray.length*4)), - "byteLength": (colourArray.length*4), - "target": gltfConstants.ARRAY_BUFFER - } - ]; - gltfOutput.accessors = [ - { - "bufferView": 0, - "componentType": gltfConstants.FLOAT, - "count": icon.vertices.length, - "type": "VEC3", - "max": [ 5.0, 5.0, 5.0], - "min": [-5.0, -5.0, -5.0], - "name": "Vertex Position Accessor" - }, - { - "bufferView": 1, - "componentType": gltfConstants.FLOAT, - "count": icon.vertices.length, - "type": "VEC3", - "max": [ 1.0, 1.0, 1.0], - "min": [-1.0, -1.0, -1.0], - "name": "Normal Accessor" - }, - { - "bufferView": 2, - "componentType": gltfConstants.FLOAT, - "count": icon.vertices.length, - "type": "VEC2", - "max": [ 1.0, 1.0], - "min": [-1.0, -1.0], - "name": "Texture Coordinate Accessor" - }, - { - "bufferView": 3, - "componentType": gltfConstants.FLOAT, - "count": icon.vertices.length, - "type": "VEC4", - "max": [ 1.0, 1.0, 1.0, 1.0], - "min": [ 0.0, 0.0, 0.0, 1.0], - "name": "Colour Accessor" - } - ]; - gltfOutput.asset = {"version": "2.0", "generator": `icondumper2/${icondumper2.version}`} - gltfOutput.extensionsUsed = ["KHR_materials_unlit"]; - gltfOutput.textures = [{"source": 0}]; - gltfOutput.images = [{"name": `Texture (${filename}#${index})`, "uri": `${filename}.png`}] - gltfOutputArray[index] = (gltfOutput); - } - let texture16 = null; // Uint16Array(16384) - switch(icon.textureFormat) { - case "N": { - texture16 = (new Uint16Array(16384)).fill(0xffff); - break; - } - case "C": { - texture16 = icondumper2.helpers.uncompressTexture(icon.texture.data); - break; - } - case "U": { - texture16 = icon.texture; - break; - } - } - let texture24 = new Uint8Array(49983); - texture24.set([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, - 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, - 0x08, 0x02, 0x00, 0x00, 0x00, // you may know - 0x4c, 0x5c, 0xf6, 0x9c, // what this is from 0x89. - 0x00, 0x00, 0xc3, 0x06, 0x49, 0x44, 0x41, 0x54, - 0x78, 0x01 // if you didn't get it, here's a clue - ],0); - let textureOffset = 43; - let texture24Data = new Array(); - let texture24CheckedData = new Array(); - for (let x = 0; x < 128; x++) { - let line = [(x === 127 ? 1 : 0), 0x81, 0x01, 0x7e, 0xfe, 0x00]; - texture24Data = texture24Data.concat(line); - texture24CheckedData.push(0); - let scanline = new Array(128*3); - for (let y = 0; y < 128; y++) { - color = rgb5a1_rgb8(texture16[(x*128)+y]); - scanline[(y*3) ] = ((color >> 0 ) & 255); - scanline[(y*3)+1] = ((color >> 8 ) & 255); - scanline[(y*3)+2] = ((color >> 16) & 255); - } - texture24Data = texture24Data.concat(scanline); - texture24CheckedData = texture24CheckedData.concat(scanline); - } - texture24.set(texture24Data, textureOffset); - textureOffset += texture24Data.length; - let a32conv = new DataView(new ArrayBuffer(4)); - a32conv.setInt32(0, getAdler32(new Uint8Array(texture24CheckedData))) - texture24.set([a32conv.getUint8(0), a32conv.getUint8(1), a32conv.getUint8(2), a32conv.getUint8(3)], textureOffset); - textureOffset += 4; - let crc32 = getCrc32(new Uint8Array([ - 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, ...texture24Data, - a32conv.getUint8(0), a32conv.getUint8(1), - a32conv.getUint8(2), a32conv.getUint8(3) - ])); - texture24.set([ - (crc32 >> 24) & 0xff, - (crc32 >> 16) & 0xff, - (crc32 >> 8) & 0xff, - crc32 & 0xff - ], textureOffset); - textureOffset += 4; - texture24.set([ - 0x00, 0x00, 0x00, 0x00, - 0x49, 0x45, 0x4E, 0x44, - 0xae, 0x42, 0x60, 0x82 - ], textureOffset); - return {objects: gltfOutputArray, buffer: outputFloatArray, texture: texture24}; -} - -function loadAndConvertIcon(inputData, attemptedFilename = "-") { - if (inputData.hasOwnProperty("numberOfShapes") === false) { - throw "Expected a icondumper2 Intermediate Model Format object."; - } - const filename = encodeURIComponent(attemptedFilename).replaceAll(/\%[0-9A-F]{2,2}/g, "").replaceAll(".", "_"); - const glTF_output = imf2gltf(inputData, filename); - for (let index = 0; index < (inputData.numberOfShapes); index++) { - (require("fs")).writeFileSync(`${filename}_${index}.gltf`, new TextEncoder().encode(JSON.stringify(glTF_output.objects[index]))); - console.info(`Saved shape ${filename}#${index} as "${filename}_${index}.gltf".`); - } - (require("fs")).writeFileSync(`${filename}.bin`, glTF_output.buffer); - console.info(`Saved glTF buffer as "${filename}.bin".`); - - (require("fs")).writeFileSync(`${filename}.png`, glTF_output.texture); - console.info(`Saved texture as "${filename}.png".\n`); -} - -// can anything de-dupe this code somehow? (index.js) -console.info(`icon.js version ${icondumper2.version}, ${(new Date()).getFullYear().toString()} (c) yellows111`); -switch(processObj.argv[2]) { - case "psu": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu"); - const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.n].data), PS2D.filenames.n); - if(PS2D.filenames.n !== PS2D.filenames.c) { - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.c].data), PS2D.filenames.c); - } - if(PS2D.filenames.n !== PS2D.filenames.d) { - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.d].data), PS2D.filenames.d); - } - break; - } - case "psv": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv"); - const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - const PS2D = iconjs.readPS2D(parsed["icon.sys"]); - //i should probably make PSV readers more like the others, but why should I? It's giving me shortcuts to what I want. - loadAndConvertIcon(iconjs.readIconFile(parsed.icons.n), PS2D.filenames.n) - if(PS2D.filenames.n !== PS2D.filenames.c) { - loadAndConvertIcon(iconjs.readIconFile(parsed.icons.c), PS2D.filenames.c) - } - if(PS2D.filenames.n !== PS2D.filenames.d) { - loadAndConvertIcon(iconjs.readIconFile(parsed.icons.d), PS2D.filenames.d) - } - break; - } - case "sps": - case "xps": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps"); - const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.n].data), PS2D.filenames.n); - if(PS2D.filenames.n !== PS2D.filenames.c) { - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.c].data), PS2D.filenames.c); - } - if(PS2D.filenames.n !== PS2D.filenames.d) { - loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.d].data), PS2D.filenames.d); - } - break; - } - case "sys": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys"); - const PS2D = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - let getFile = filesystem.readFileSync(PS2D.filenames.n); - loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.n); - if(PS2D.filenames.n !== PS2D.filenames.c) { - let getFile = filesystem.readFileSync(PS2D.filenames.c); - loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.c); - } - if(PS2D.filenames.n !== PS2D.filenames.d) { - let getFile = filesystem.readFileSync(PS2D.filenames.d); - loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.d); - } - break; - } - case "ico": - case "icn": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "input.icn"); - loadAndConvertIcon(iconjs.readIconFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)), require("path").basename(processObj.argv[3])); - break; - } - default: { - //Template literal goes here. - console.info( -`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client (glTF exporter version) subcommands: -psu: Read a EMS Memory Adapter export file. -psv: Read a PS3 export file. -sps: Read a SharkPort export file. -xps: Read a X-Port export file. - -sys: Read a icon.sys (964 bytes) file, and attempt - to read icon files from the current directory. -icn: Read an icon file directly. (Also as: ico) -` ); // end of template - processObj.exit(1); - } -} +const icondumper2 = require("./icon.js"); +const iconjs = icondumper2.readers; +const filesystem = require("fs"); +const processObj = require("process"); + +const gltfConstants = { + "FLOAT": 5126, + "ARRAY_BUFFER": 34962, + "LINEAR": 9729, + "NEAREST_MIPMAP_LINEAR": 9986, + "REPEAT": 10497 +}; + +function getCrc32(data) { + let output = -1; + for (let byteIndex of data) { + for (let index = 0; index < 8; index++, byteIndex >>>= 1) { + output = (output >>> 1) ^ (-((output ^ byteIndex) & 1) & 0xEDB88320); + } // 0xEDB88320 is a reverse polynomial that's common in crc32 + } //i know it's some bitwise madness, it works, either way. + return ((~output) >>> 0); +} + +function getAdler32(data) { + let s1 = 1; + let s2 = 0; + for (let index of data) { + s1 = (s1 + index) % 65521; + s2 = (s2 + s1) % 65521; + } + return (s2 << 16) | s1; +} + +function rgb5a1_rgb8(colour) { + let b = ( colour & 0b11111); + let g = ((colour >> 5) & 0b11111); + let r = ((colour >> 10) & 0b11111); + let output = new Number(); + output |= ((r * 8) << 16); + output |= ((g * 8) << 8); + output |= ((b * 8) << 0); + return output; +} + +function imf2gltf(icon = null, filename = "untitled") { + if (icon === null) { + throw "Missing first argument, of which should be a icondumper2 Intermediate Model Format object."; + } + if (icon.hasOwnProperty("numberOfShapes") === false) { + throw "Expected a icondumper2 Intermediate Model Format object."; + } + let shapesArray = new Array(icon.numberOfShapes); + for (let index = 0; index < icon.numberOfShapes; index++) { + shapesArray[index] = new Array(); + } + let verticesArray = new Array(); + let normalsArray = new Array(); + let uvArray = new Array(); + let colourArray = new Array(); + icon.vertices.forEach(function(vertexObject){ + for (let index = 0; index < icon.numberOfShapes; index++) { + shapesArray[index].push(vertexObject.shapes[index].x); + shapesArray[index].push(vertexObject.shapes[index].y); + shapesArray[index].push(vertexObject.shapes[index].z); + } + normalsArray.push(vertexObject.normal.x); + normalsArray.push(vertexObject.normal.y); + normalsArray.push(vertexObject.normal.z); + uvArray.push(vertexObject.uv.u); + uvArray.push(vertexObject.uv.v); + // gamma correction, glTF clients expect lineari(s|z)ed-sRGB, not sRGB. + colourArray.push(Math.pow((vertexObject.color.r/255), 2.2)); + colourArray.push(Math.pow((vertexObject.color.g/255), 2.2)); + colourArray.push(Math.pow((vertexObject.color.b/255), 2.2)); + colourArray.push((vertexObject.color.a > 1) ? (vertexObject.color.a/255): 1); + }); + shapesArray.forEach(function(arr) { + verticesArray = [...verticesArray, ...arr]; + }); + let outputFloatArray = new Float32Array([...verticesArray, ...normalsArray, ...uvArray, ...colourArray]); // 3[nOS], 3, 2, 4# + let gltfOutputArray = new Array(icon.numberOfShapes); + for (let index = 0; index < icon.numberOfShapes; index++) { + const gltfOutput = new Object(); + //setting up GLTF + gltfOutput.scene = 0; + gltfOutput.scenes = [{"name": filename, "nodes": [0]}]; + gltfOutput.nodes = [{"mesh": 0, "name": `${filename}#${index}`, "rotation": [1,0,0,0]}]; + gltfOutput.meshes = [{ + "name": `Mesh (${filename}#${index})`, + "primitives": [{ + "attributes": { + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2, + "COLOR_0": 3, + }, + "material": 0 + }] + }]; // no indices because who needs indexing when you're transcoding? + gltfOutput.materials = [{ + "name": `Material (${filename}#${index})`, + "pbrMetallicRoughness": { + "baseColorTexture": {"index":0, "texCoord": 0} + }, + "extensions": { // or we get annoying PBR and specular stuff we don't need + "KHR_materials_unlit": {} + } + }]; + gltfOutput.buffers = [{"uri": `${filename}.bin`, "byteLength": outputFloatArray.byteLength}]; + gltfOutput.bufferViews = [ + { + "buffer": 0, + "byteOffset": (((icon.vertices.length*3)*4)*index), + "byteLength": ((icon.vertices.length*3)*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": (((icon.vertices.length*3)*4)*icon.numberOfShapes), + "byteLength": (normalsArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": ((((icon.vertices.length*3)*4)*icon.numberOfShapes)+(normalsArray.length*4)), + "byteLength": (uvArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": (((((icon.vertices.length*3)*4)*icon.numberOfShapes)+(normalsArray.length*4))+(uvArray.length*4)), + "byteLength": (colourArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + } + ]; + gltfOutput.accessors = [ + { + "bufferView": 0, + "componentType": gltfConstants.FLOAT, + "count": icon.vertices.length, + "type": "VEC3", + "max": [ 5.0, 5.0, 5.0], + "min": [-5.0, -5.0, -5.0], + "name": "Vertex Position Accessor" + }, + { + "bufferView": 1, + "componentType": gltfConstants.FLOAT, + "count": icon.vertices.length, + "type": "VEC3", + "max": [ 1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "name": "Normal Accessor" + }, + { + "bufferView": 2, + "componentType": gltfConstants.FLOAT, + "count": icon.vertices.length, + "type": "VEC2", + "max": [ 1.0, 1.0], + "min": [-1.0, -1.0], + "name": "Texture Coordinate Accessor" + }, + { + "bufferView": 3, + "componentType": gltfConstants.FLOAT, + "count": icon.vertices.length, + "type": "VEC4", + "max": [ 1.0, 1.0, 1.0, 1.0], + "min": [ 0.0, 0.0, 0.0, 1.0], + "name": "Colour Accessor" + } + ]; + gltfOutput.asset = {"version": "2.0", "generator": `icondumper2/${icondumper2.version}`} + gltfOutput.extensionsUsed = ["KHR_materials_unlit"]; + gltfOutput.textures = [{"source": 0}]; + gltfOutput.images = [{"name": `Texture (${filename}#${index})`, "uri": `${filename}.png`}] + gltfOutputArray[index] = (gltfOutput); + } + let texture16 = null; // Uint16Array(16384) + switch(icon.textureFormat) { + case "N": { + texture16 = (new Uint16Array(16384)).fill(0xffff); + break; + } + case "C": { + texture16 = icondumper2.helpers.uncompressTexture(icon.texture.data); + break; + } + case "U": { + texture16 = icon.texture; + break; + } + } + let texture24 = new Uint8Array(49983); + texture24.set([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x08, 0x02, 0x00, 0x00, 0x00, // you may know + 0x4c, 0x5c, 0xf6, 0x9c, // what this is from 0x89. + 0x00, 0x00, 0xc3, 0x06, 0x49, 0x44, 0x41, 0x54, + 0x78, 0x01 // if you didn't get it, here's a clue + ],0); + let textureOffset = 43; + let texture24Data = new Array(); + let texture24CheckedData = new Array(); + for (let x = 0; x < 128; x++) { + let line = [(x === 127 ? 1 : 0), 0x81, 0x01, 0x7e, 0xfe, 0x00]; + texture24Data = texture24Data.concat(line); + texture24CheckedData.push(0); + let scanline = new Array(128*3); + for (let y = 0; y < 128; y++) { + color = rgb5a1_rgb8(texture16[(x*128)+y]); + scanline[(y*3) ] = ((color >> 0 ) & 255); + scanline[(y*3)+1] = ((color >> 8 ) & 255); + scanline[(y*3)+2] = ((color >> 16) & 255); + } + texture24Data = texture24Data.concat(scanline); + texture24CheckedData = texture24CheckedData.concat(scanline); + } + texture24.set(texture24Data, textureOffset); + textureOffset += texture24Data.length; + let a32conv = new DataView(new ArrayBuffer(4)); + a32conv.setInt32(0, getAdler32(new Uint8Array(texture24CheckedData))) + texture24.set([a32conv.getUint8(0), a32conv.getUint8(1), a32conv.getUint8(2), a32conv.getUint8(3)], textureOffset); + textureOffset += 4; + let crc32 = getCrc32(new Uint8Array([ + 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, ...texture24Data, + a32conv.getUint8(0), a32conv.getUint8(1), + a32conv.getUint8(2), a32conv.getUint8(3) + ])); + texture24.set([ + (crc32 >> 24) & 0xff, + (crc32 >> 16) & 0xff, + (crc32 >> 8) & 0xff, + crc32 & 0xff + ], textureOffset); + textureOffset += 4; + texture24.set([ + 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4E, 0x44, + 0xae, 0x42, 0x60, 0x82 + ], textureOffset); + return {objects: gltfOutputArray, buffer: outputFloatArray, texture: texture24}; +} + +function loadAndConvertIcon(inputData, attemptedFilename = "-") { + if (inputData.hasOwnProperty("numberOfShapes") === false) { + throw "Expected a icondumper2 Intermediate Model Format object."; + } + const filename = encodeURIComponent(attemptedFilename).replaceAll(/\%[0-9A-F]{2,2}/g, "").replaceAll(".", "_"); + const glTF_output = imf2gltf(inputData, filename); + for (let index = 0; index < (inputData.numberOfShapes); index++) { + (require("fs")).writeFileSync(`${filename}_${index}.gltf`, new TextEncoder().encode(JSON.stringify(glTF_output.objects[index]))); + console.info(`Saved shape ${filename}#${index} as "${filename}_${index}.gltf".`); + } + (require("fs")).writeFileSync(`${filename}.bin`, glTF_output.buffer); + console.info(`Saved glTF buffer as "${filename}.bin".`); + + (require("fs")).writeFileSync(`${filename}.png`, glTF_output.texture); + console.info(`Saved texture as "${filename}.png".\n`); +} + +// can anything de-dupe this code somehow? (index.js) +console.info(`icon.js version ${icondumper2.version}, ${(new Date()).getFullYear().toString()} (c) yellows111`); +switch(processObj.argv[2]) { + case "psu": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu"); + const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.n].data), PS2D.filenames.n); + if(PS2D.filenames.n !== PS2D.filenames.c) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.c].data), PS2D.filenames.c); + } + if(PS2D.filenames.n !== PS2D.filenames.d) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.d].data), PS2D.filenames.d); + } + break; + } + case "psv": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv"); + const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed["icon.sys"]); + //i should probably make PSV readers more like the others, but why should I? It's giving me shortcuts to what I want. + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.n), PS2D.filenames.n) + if(PS2D.filenames.n !== PS2D.filenames.c) { + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.c), PS2D.filenames.c) + } + if(PS2D.filenames.n !== PS2D.filenames.d) { + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.d), PS2D.filenames.d) + } + break; + } + case "sps": + case "xps": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps"); + const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.n].data), PS2D.filenames.n); + if(PS2D.filenames.n !== PS2D.filenames.c) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.c].data), PS2D.filenames.c); + } + if(PS2D.filenames.n !== PS2D.filenames.d) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames.d].data), PS2D.filenames.d); + } + break; + } + case "sys": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys"); + const PS2D = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + let getFile = filesystem.readFileSync(PS2D.filenames.n); + loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.n); + if(PS2D.filenames.n !== PS2D.filenames.c) { + let getFile = filesystem.readFileSync(PS2D.filenames.c); + loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.c); + } + if(PS2D.filenames.n !== PS2D.filenames.d) { + let getFile = filesystem.readFileSync(PS2D.filenames.d); + loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), PS2D.filenames.d); + } + break; + } + case "ico": + case "icn": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "input.icn"); + loadAndConvertIcon(iconjs.readIconFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)), require("path").basename(processObj.argv[3])); + break; + } + default: { + //Template literal goes here. + console.info( +`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client (glTF exporter version) subcommands: +psu: Read a EMS Memory Adapter export file. +psv: Read a PS3 export file. +sps: Read a SharkPort export file. +xps: Read a X-Port export file. + +sys: Read a icon.sys (964 bytes) file, and attempt + to read icon files from the current directory. +icn: Read an icon file directly. (Also as: ico) +` ); // end of template + processObj.exit(1); + } +} processObj.exit(0); \ No newline at end of file diff --git a/icon.js b/icon.js index 597f567..6b00b1a 100644 --- a/icon.js +++ b/icon.js @@ -1,523 +1,523 @@ -//todo: Make this a module/mjs file. C6 compatibility can stay, if needed. -//LOOKING FOR: LZARI implementation (for MAX), description of CBS compression (node zlib doesn't tackle it, even with RC4'ing the data) -ICONJS_DEBUG = false; -ICONJS_STRICT = true; -ICONJS_VERSION = "0.5.1"; - -function setDebug(value) { - ICONJS_DEBUG = !!value; -} - -function setStrictness(value) { - ICONJS_STRICT = !!value; -} - -// where U = uncompressed, N = none, C = compressed - -function getTextureFormat(i) { - if (i<8) { - if(i==3) { - return 'N'; - } - return 'U'; - } else if (i>=8) { - return 'C'; - } else { - return void(0); - } -} - -function uncompressTexture(texData) { - // for texture formats 8-15 - if (texData.length & 1) { - throw "Texture size isn't a multiple of 2 (was ${texData.length})"; - } - const view = new DataView(texData); - const u16le = function(i){return view.getUint16(i, 1)} - let uncompressed = new Uint16Array(16384); - let offset = 0; - for (let index = 0; index < 16384;) { - currentValue = u16le(offset); - if(currentValue === 0) { - // if this is specifically a katamari 1 or 2 icon, skip this byte - // because it's formatted like that for some reason - offset += 2; - currentValue = u16le(offset); - } - offset += 2; - if (currentValue >= 0xff00) { - //do a raw copy of the next currentValue bytes - let length = ((0x10000 - currentValue)); - for (let enumerator = 0; enumerator < length; enumerator++) { - uncompressed[index] = u16le(offset); - offset += 2; - index++; - } - } else { - //repeat next byte currentValue times - uncompressed[index] = u16le(offset); - for (let indey = 0; indey < currentValue; indey++) { - uncompressed[index] = u16le(offset); - index++ - } - offset += 2; - } - } - return uncompressed; -} - -function convertBGR5A1toRGB5A1(bgrData) { - if(bgrData.byteLength !== 32768) { - throw `Not a 128x128x16 texture. (length was ${bgrData.length})`; - } - // converts 5-bit blue, green, red (in that order) with one alpha bit to GL-compatible RGB5A1 - let converted = new Uint16Array(16384); - for (let index = 0; index < 16384; index++) { - let b = ( bgrData[index] & 0b11111); - let g = ((bgrData[index] >> 5) & 0b11111); - let r = ((bgrData[index] >> 10) & 0b11111); - // rrrrrgggggbbbbba (a = 1 because 0 is 127 which is 1.0f opacity for the GS) - let newValue = 0b0000000000000001; - // mind you, the alpha bit ^ seems to be set randomly on textures, anyway. Maybe i'm reading these wrong? - newValue |= (r << 1); - newValue |= (g << 6); - newValue |= (b << 11); - converted[index] = newValue; - } - return converted; -} - -function stringScrubber(dirty) { - return dirty.replaceAll("\x00","").substring(0, (dirty.indexOf("\x00") === -1) ? dirty.length : dirty.indexOf("\x00")); -} - -function readPS2D(input) { - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)} - const f32le = function(i){return view.getFloat32(i, 1)} - //!pattern ps2d.hexpat - const header = u32le(0); - if (header !== 0x44325350) { - throw `Not a PS2D file (was ${header}, expected ${0x44325350})`; - } - //:skip 2 - const titleOffset = u32le(6); - //:skip 2 - const bgAlpha = u32le(12); // should read as a u8, (k/127?.5) for float value (255 = a(2.0~)) - const bgColors = [ - {r: u32le(16), g: u32le(20), b: u32le(24), a: u32le(28)}, - {r: u32le(32), g: u32le(36), b: u32le(40), a: u32le(44)}, - {r: u32le(48), g: u32le(52), b: u32le(56), a: u32le(60)}, - {r: u32le(64), g: u32le(68), b: u32le(72), a: u32le(76)} - ]; // top-left, top-right, bottom-left, bottom-right; - const lightIndices = [ - {x: f32le(80), y: f32le(84), z: f32le(88)}, //:skip 4 - {x: f32le(96), y: f32le(100), z: f32le(104)}, //:skip 4 - {x: f32le(112), y: f32le(116), z: f32le(120)} //:skip 4 - ] - const lightColors = [ - {r: f32le(128), g: f32le(132), b: f32le(136), a: f32le(140)}, - {r: f32le(144), g: f32le(148), b: f32le(152), a: f32le(156)}, - {r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)}, - {r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)} - ] - // P2SB says color 1 is ambient, 2-4 are for 3-point cameras - // official HDD icon.sys files (completely different PS2ICON text-based format) also say the same. - const int_title = input.slice(0xc0, 0x100); - const tmp_title16 = new Uint16Array(int_title); - for (let index = 0; index < 32; index++) { - //find "bad" shift-jis two-bytes, and convert to spaces (or NULL if strict) - if(tmp_title16[index] === 129 || tmp_title16[index] === 33343) { - console.warn(`PS2D syntax error: known bad two-byte sequence 0x${tmp_title16[index].toString(16).padEnd(4,"0")} (at ${index}). Replacing with ${(ICONJS_STRICT) ? "NULL" : "0x8140"}.\n${(ICONJS_STRICT) ? "This is console-accurate, so" : "An actual console does not do this."} I recommend patching your icon.sys files!`); - tmp_title16[index] = (ICONJS_STRICT) ? 0 : 0x4081; // is a reference, so this'll edit int_title too - } - } - //:skip 4 -- Unless proven, keep 64 bytes for display name. - const int_filename_n = input.slice(0x104, 0x143); - const int_filename_c = input.slice(0x144, 0x183); - const int_filename_d = input.slice(0x184, 0x1C3); - //;skip 512 -- rest of this is just padding - const rawTitle = (new TextDecoder("shift-jis")).decode(int_title); - const title = [ - stringScrubber(rawTitle.substring(0,(titleOffset/2))), - (rawTitle.indexOf("\x00") < (titleOffset/2)) ? "" : stringScrubber(rawTitle.substring((titleOffset/2))) - ]; - const filenames = { - n: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_n)), - c: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_c)), - d: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_d)) - } - if(ICONJS_DEBUG){ - console.debug({header, titleOffset, bgAlpha, bgColors, lightIndices, lightColors, title, filenames}); - } - return {filenames, title, background: {colors: bgColors, alpha: bgAlpha}, lighting: {points: lightIndices, colors: lightColors}}; -} - -function readIconFile(input) { - //!pattern ps2icon-hacked.hexpat - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)} - const f32le = function(i){return view.getFloat32(i, 1)} - const f16le = function(i){return (view.getInt16(i, 1) / 4096)} - const u32_rgba8 = function(i) {return { - r: (i & 0xff), - g: ((i & 0xff00) >> 8), - b: ((i & 0xff0000) >> 16), - a: (i > 0x7fffffff ? 255 : (((i & 0xff000000) >>> 24) * 2)+1) - // I don't think alpha transparency is actually USED in icons?, rendering with it looks strange. - }}; - const magic = u32le(0); - if (magic !== 0x010000) { - // USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic. - // Can someone provide me pointers to such an icon if one exists? - throw `Not a PS2 icon file (was ${magic}, expected ${0x010000})`; - } - const numberOfShapes = u32le(4); - if(numberOfShapes > 16) { - throw `Too many defined shapes! Is this a valid file? (file reports ${numberOfShapes} shapes)`; - } - const textureType = u32le(8); - const textureFormat = getTextureFormat(textureType); - //:skip 4 - const numberOfVertexes = u32le(16); - if(!!(numberOfVertexes % 3)){ - throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`; - } - // format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk] - let offset = 20; - let vertices = new Array(); - const chunkLength = ((numberOfShapes * 8) + 16); - - // for numberOfVertexes, copy chunkLength x times, then append with normal, uv and rgba8 color. Floats are 16-bit. - for (let index = 0; index < numberOfVertexes; index++) { - let shapes = new Array(); - for (let indey = 0; indey < numberOfShapes; indey++) { - shapes.push({ - x: f16le(offset+((chunkLength*index)+(indey*8))), - y: f16le(offset+((chunkLength*index)+(indey*8))+2), - z: f16le(offset+((chunkLength*index)+(indey*8))+4) - }); - //:skip 2 - } - - let normal = { - x: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))), - y: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+2), - z: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+4) - }; - //:skip 2 - let uv = { - u: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+8), - v: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+10) - }; - let color = u32_rgba8(u32le(offset+(chunkLength*index)+((numberOfShapes * 8))+12)); - // keep original u32le color? - vertices.push({shapes, normal, uv, color}); - } - offset = (20+(numberOfVertexes * chunkLength)); - animationHeader = {id: u32le(offset), length: u32le(offset+4), speed: f32le(offset+8), "offset": u32le(offset+12), keyframes: u32le(offset+16)}; - let animData = new Array(); - // now we have to enumerate values, so now we introduce an offset value. - // format for a keyframe: sssskkkk[ffffvvvv] where [ffffvvvv] repeat based on the value that kkkk(eys) has. - // sssskkkk[ffffvvvv] is repeated based on animationHeader.keyframes value. - offset += 20; - for (let index = 0; index < animationHeader.keyframes; index++) { - let frameData = new Array(); - let shapeId = u32le(offset); - let keys = u32le(offset+4); - offset += 8; - for (let indey = 0; indey < keys; indey++) { - frameData.push({frame: f32le(offset), value: f32le(offset+4)}); - offset += 8; - } - animData.push({shapeId, keys, frameData}); - } - let texture = null; - switch(textureFormat) { - case 'N': { - break; - } - case 'U': { - //where every 16-bit entry is a BGR5A1 color 0b[bbbbbgggggrrrrra] - texture = new Uint16Array(input.slice(offset, (offset+0x8000))); - //see convertBGR5A1toRGB5A1() for more info. - break; - } - case 'C': { - // compression format is RLE-based, where first u32 is size, and format is defined as: - /** - * u16 rleType; - * if (rleType >= 0xff00) { - * //do a raw copy - * let length = (0x10000 - rleType); - * byte data[length]; - * } else { - * //repeat next byte rleType times - * data = new Uint16Array(rleType); - * for (let index = 0; index < rleType; index++) { - * data[index] = u16 repeater @ +4; - * } - * } - **/ - //output of this will be another u16[0x4000] of the decompressed texture - //after that just parse output as-if it was uncompressed. - //see uncompressTexture() and convertBGR5A1toRGB5A1() for more info. - size = u32le(offset); - texture = {size, data: input.slice(offset+4, offset+(4+size))}; - } - } - if(ICONJS_DEBUG){ - console.debug({magic, numberOfShapes, textureType, textureFormat, numberOfVertexes, chunkLength, vertices, animationHeader, animData, texture}); - } - return {numberOfShapes, vertices, textureFormat, texture, animData}; -} - -function readEntryBlock(input) { - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)}; - const t64le = function(i){return { - seconds: view.getUint8(i+1), - minutes: view.getUint8(i+2), - hours: view.getUint8(i+3), - day: view.getUint8(i+4), - month: view.getUint8(i+5), - year: view.getUint16(i+6, 1) - }}; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly! - //!pattern psu_file.hexpat - const permissions = u32le(0); - let type; - if (permissions>0xffff) { - throw `Not a EMS Memory Adapter (PSU) export file (was ${permissions}, expected less than ${0xffff})`; - } - if((permissions & 0b00100000)>=1){ - type = "directory"; - } - if((permissions & 0b00010000)>=1){ - type = "file"; - } - if((permissions & 0b0001100000000000)>=1){ - throw `I don't parse portable applications or legacy save data. (${permissions} has bits 10 or 11 set)`; - } - const size = u32le(4); - const createdTime = t64le(8); - const sectorOffset = u32le(16); - const dirEntry = u32le(20); - const modifiedTime = t64le(24); - const specialSection = input.slice(0x20, 0x40); - const int_filename = input.slice(0x40, 512); - const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); - if(ICONJS_DEBUG){ - console.debug({permissions, type, size, createdTime, sectorOffset, dirEntry, modifiedTime, specialSection, filename}); - } - return {type, size, filename, createdTime, modifiedTime}; -} - -function readEmsPsuFile(input){ - const header = readEntryBlock(input.slice(0,0x1ff)); - if(header.size > 0x7f) { - throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})` - } - let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: {created: header.createdTime, modified: header.modifiedTime}}; - let output = new Object(); - let offset = 512; - for (let index = 0; index < header.size; index++) { - fdesc = readEntryBlock(input.slice(offset, offset + 512)); - switch(fdesc.type) { - case "directory": { - offset += 512; - output[fdesc.filename] = null; - break; - } - case "file": { - if(ICONJS_DEBUG){ - console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); - } - offset += 512; - const originalOffset = offset; - if((fdesc.size % 1024) > 0) { - offset += ((fdesc.size & 0b11111111110000000000) + 1024); - } else { - offset += fdesc.size; - // if we're already filling 1k blocks fully, why change the value? - } - output[fdesc.filename] = { - size: fdesc.size, - data: input.slice(originalOffset, originalOffset+fdesc.size) - }; - break; - } - } - } - fsOut[header.filename] = output; - return fsOut; -} - -function readPsvFile(input){ - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)}; - const t64le = function(i){return { - seconds: view.getUint8(i+1), - minutes: view.getUint8(i+2), - hours: view.getUint8(i+3), - day: view.getUint8(i+4), - month: view.getUint8(i+5), - year: view.getUint16(i+6, 1) - }}; - //!pattern psv_file.hexpat - const magic = u32le(0); - if (magic !== 0x50535600) { - throw `Not a PS3 export (PSV) file (was ${magic}, expected ${0x50535600})`; - } - //:skip 4 - //:skip 20 // key seed, console ignores this - //:skip 20 // sha1 hmac digest, useful for verifying that this is, indeed, save data. - //:skip 8 - const type1 = u32le(56); - const type2 = u32le(60); - if(type1 !== 0x2c && type2 !== 2) { - throw `Not parsing, this is not in the PS2 save export format (was ${type1}:${type2}, expected 44:2)`; - } - const displayedSize = u32le(64); - const ps2dOffset = u32le(68); - const ps2dSize = u32le(72); // don't know why this is included if its always 964 - const nModelOffset = u32le(76); - const nModelSize = u32le(80); - const cModelOffset = u32le(84); - const cModelSize = u32le(88); - const dModelOffset = u32le(92); - const dModelSize = u32le(96); - const numberOfFiles = u32le(100); // in-case this library changes stance on other files - // file = {t64le created, t64le modified, u32 size, u32 permissions, byte[32] title} - // and if it's not the root directory, add another u32 for offset/location - const rootDirectoryData = input.slice(104, 162); - const timestamps = {created: t64le(104), modified: t64le(112)}; - let offset = 162; - let fileData = new Array(); - for (let index = 0; index < numberOfFiles; index++) { - fileData.push(input.slice(offset,offset+0x3c)); - offset += 0x3c; - }; - //then file data after this but we already have pointers to the files we care about - const icons = { - n: input.slice(nModelOffset, nModelOffset+nModelSize), - c: input.slice(cModelOffset, cModelOffset+cModelSize), - d: input.slice(dModelOffset, dModelOffset+dModelSize), - } - if (ICONJS_DEBUG) { - console.debug({magic, type1, type2, displayedSize, ps2dOffset, ps2dSize, nModelOffset, nModelSize, cModelOffset, cModelSize, dModelOffset, dModelSize, numberOfFiles, rootDirectoryData, fileData}) - } - return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps}; -} - -function readSxpsDescriptor(input) { - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)}; - const t64le = function(i){return { - seconds: view.getUint8(i+1), - minutes: view.getUint8(i+2), - hours: view.getUint8(i+3), - day: view.getUint8(i+4), - month: view.getUint8(i+5), - year: view.getUint16(i+6, 1) - }}; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly! - //!pattern sps-xps_file.hexpat - //:skip 2 // ... it's the file descriptor block size (including the bytes themselves, so 250) - const int_filename = input.slice(2, 66); - const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); - const size = u32le(66); - const startSector = u32le(70); - const endSector = u32le(74); - const permissions = u32le(78); // the first two bytes are *swapped*. The comments that ensued were not kept. - let type; - if (permissions>0xffff) { - throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${permissions}, expected less than ${0xffff})`; - } - if((permissions & 0b0010000000000000)>=1){ - type = "directory"; - } - if((permissions & 0b0001000000000000)>=1){ - type = "file"; - } - if((permissions & 0b00011000)>=1){ - throw `I don't parse portable applications or legacy save data. (${permissions} has bits 4 or 5 set)`; - } - const timestamps = {created: t64le(82), modified: t64le(90)}; - //:skip 4 - //:skip 4 - u32 optional (98) - //:skip 8 - t64 optionalTime (102) // I don't know why this is here. - const int_asciiName = input.slice(114, 178); - const int_shiftjisName = input.slice(178, 242); // Because why parse a PS2D when you can hard-code it? - //:skip 8 - if(ICONJS_DEBUG) { - console.debug({int_filename, size, startSector, endSector, permissions, type, timestamps, int_asciiName, int_shiftjisName}); - } - return {type, size, filename, timestamps}; -} - -function readSharkXPortSxpsFile(input) { - const view = new DataView(input); - const u32le = function(i){return view.getUint32(i, 1)}; - //!pattern sps-xps_file.hexpat - const identLength = u32le(0); - if(identLength !== 13) { - throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${identLength}, expected 13)`; - } - let offset = 4; - const ident = input.slice(offset, offset+identLength); - if((new TextDecoder("utf-8")).decode(ident) !== "SharkPortSave") { - throw `Unrecognized file identification string. Expected "SharkPortSave".` - } - offset += (identLength + 4); - const titleLength = u32le(offset); - const title = input.slice(offset + 4, (offset + 4) + titleLength); - offset += (titleLength + 4); - const descriptionLength = u32le(offset); - const description = input.slice(offset + 4, (offset + 4) + descriptionLength); - offset += (descriptionLength + 8); - const comments = { - "game": stringScrubber((new TextDecoder("utf-8")).decode(title)), - "name": stringScrubber((new TextDecoder("utf-8")).decode(description)) - } - const totalSize = u32le(offset); - offset += 4; - const header = readSxpsDescriptor(input.slice(offset, offset + 250)); - offset += 250; - // alright now lets parse some actual data - let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments}; - let output = new Object(); - for (let index = 0; index < (header.size - 2); index++) { - fdesc = readSxpsDescriptor(input.slice(offset, offset + 250)); - switch(fdesc.type) { - case "directory": { - offset += 250; - output[fdesc.filename] = null; - break; - } - case "file": { - if(ICONJS_DEBUG){ - console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); - } - offset += 250; - output[fdesc.filename] = { - size: fdesc.size, - data: input.slice(offset, offset+fdesc.size) - }; - offset += fdesc.size; - break; - } - } - } - fsOut[header.filename] = output; - //:skip 4 // then here lies, at offset (the end of file), a u32 checksum. - return fsOut; -} - -if(typeof module !== "undefined") { - // for C6JS - module.exports = { - readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile}, - helpers: {uncompressTexture, convertBGR5A1toRGB5A1}, - options: {setDebug, setStrictness}, - version: ICONJS_VERSION - }; +//todo: Make this a module/mjs file. C6 compatibility can stay, if needed. +//LOOKING FOR: LZARI implementation (for MAX), description of CBS compression (node zlib doesn't tackle it, even with RC4'ing the data) +ICONJS_DEBUG = false; +ICONJS_STRICT = true; +ICONJS_VERSION = "0.5.1"; + +function setDebug(value) { + ICONJS_DEBUG = !!value; +} + +function setStrictness(value) { + ICONJS_STRICT = !!value; +} + +// where U = uncompressed, N = none, C = compressed + +function getTextureFormat(i) { + if (i<8) { + if(i==3) { + return 'N'; + } + return 'U'; + } else if (i>=8) { + return 'C'; + } else { + return void(0); + } +} + +function uncompressTexture(texData) { + // for texture formats 8-15 + if (texData.length & 1) { + throw "Texture size isn't a multiple of 2 (was ${texData.length})"; + } + const view = new DataView(texData); + const u16le = function(i){return view.getUint16(i, 1)} + let uncompressed = new Uint16Array(16384); + let offset = 0; + for (let index = 0; index < 16384;) { + currentValue = u16le(offset); + if(currentValue === 0) { + // if this is specifically a katamari 1 or 2 icon, skip this byte + // because it's formatted like that for some reason + offset += 2; + currentValue = u16le(offset); + } + offset += 2; + if (currentValue >= 0xff00) { + //do a raw copy of the next currentValue bytes + let length = ((0x10000 - currentValue)); + for (let enumerator = 0; enumerator < length; enumerator++) { + uncompressed[index] = u16le(offset); + offset += 2; + index++; + } + } else { + //repeat next byte currentValue times + uncompressed[index] = u16le(offset); + for (let indey = 0; indey < currentValue; indey++) { + uncompressed[index] = u16le(offset); + index++ + } + offset += 2; + } + } + return uncompressed; +} + +function convertBGR5A1toRGB5A1(bgrData) { + if(bgrData.byteLength !== 32768) { + throw `Not a 128x128x16 texture. (length was ${bgrData.length})`; + } + // converts 5-bit blue, green, red (in that order) with one alpha bit to GL-compatible RGB5A1 + let converted = new Uint16Array(16384); + for (let index = 0; index < 16384; index++) { + let b = ( bgrData[index] & 0b11111); + let g = ((bgrData[index] >> 5) & 0b11111); + let r = ((bgrData[index] >> 10) & 0b11111); + // rrrrrgggggbbbbba (a = 1 because 0 is 127 which is 1.0f opacity for the GS) + let newValue = 0b0000000000000001; + // mind you, the alpha bit ^ seems to be set randomly on textures, anyway. Maybe i'm reading these wrong? + newValue |= (r << 1); + newValue |= (g << 6); + newValue |= (b << 11); + converted[index] = newValue; + } + return converted; +} + +function stringScrubber(dirty) { + return dirty.replaceAll("\x00","").substring(0, (dirty.indexOf("\x00") === -1) ? dirty.length : dirty.indexOf("\x00")); +} + +function readPS2D(input) { + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)} + const f32le = function(i){return view.getFloat32(i, 1)} + //!pattern ps2d.hexpat + const header = u32le(0); + if (header !== 0x44325350) { + throw `Not a PS2D file (was ${header}, expected ${0x44325350})`; + } + //:skip 2 + const titleOffset = u32le(6); + //:skip 2 + const bgAlpha = u32le(12); // should read as a u8, (k/127?.5) for float value (255 = a(2.0~)) + const bgColors = [ + {r: u32le(16), g: u32le(20), b: u32le(24), a: u32le(28)}, + {r: u32le(32), g: u32le(36), b: u32le(40), a: u32le(44)}, + {r: u32le(48), g: u32le(52), b: u32le(56), a: u32le(60)}, + {r: u32le(64), g: u32le(68), b: u32le(72), a: u32le(76)} + ]; // top-left, top-right, bottom-left, bottom-right; + const lightIndices = [ + {x: f32le(80), y: f32le(84), z: f32le(88)}, //:skip 4 + {x: f32le(96), y: f32le(100), z: f32le(104)}, //:skip 4 + {x: f32le(112), y: f32le(116), z: f32le(120)} //:skip 4 + ] + const lightColors = [ + {r: f32le(128), g: f32le(132), b: f32le(136), a: f32le(140)}, + {r: f32le(144), g: f32le(148), b: f32le(152), a: f32le(156)}, + {r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)}, + {r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)} + ] + // P2SB says color 1 is ambient, 2-4 are for 3-point cameras + // official HDD icon.sys files (completely different PS2ICON text-based format) also say the same. + const int_title = input.slice(0xc0, 0x100); + const tmp_title16 = new Uint16Array(int_title); + for (let index = 0; index < 32; index++) { + //find "bad" shift-jis two-bytes, and convert to spaces (or NULL if strict) + if(tmp_title16[index] === 129 || tmp_title16[index] === 33343) { + console.warn(`PS2D syntax error: known bad two-byte sequence 0x${tmp_title16[index].toString(16).padEnd(4,"0")} (at ${index}). Replacing with ${(ICONJS_STRICT) ? "NULL" : "0x8140"}.\n${(ICONJS_STRICT) ? "This is console-accurate, so" : "An actual console does not do this."} I recommend patching your icon.sys files!`); + tmp_title16[index] = (ICONJS_STRICT) ? 0 : 0x4081; // is a reference, so this'll edit int_title too + } + } + //:skip 4 -- Unless proven, keep 64 bytes for display name. + const int_filename_n = input.slice(0x104, 0x143); + const int_filename_c = input.slice(0x144, 0x183); + const int_filename_d = input.slice(0x184, 0x1C3); + //;skip 512 -- rest of this is just padding + const rawTitle = (new TextDecoder("shift-jis")).decode(int_title); + const title = [ + stringScrubber(rawTitle.substring(0,(titleOffset/2))), + (rawTitle.indexOf("\x00") < (titleOffset/2)) ? "" : stringScrubber(rawTitle.substring((titleOffset/2))) + ]; + const filenames = { + n: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_n)), + c: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_c)), + d: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_d)) + } + if(ICONJS_DEBUG){ + console.debug({header, titleOffset, bgAlpha, bgColors, lightIndices, lightColors, title, filenames}); + } + return {filenames, title, background: {colors: bgColors, alpha: bgAlpha}, lighting: {points: lightIndices, colors: lightColors}}; +} + +function readIconFile(input) { + //!pattern ps2icon-hacked.hexpat + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)} + const f32le = function(i){return view.getFloat32(i, 1)} + const f16le = function(i){return (view.getInt16(i, 1) / 4096)} + const u32_rgba8 = function(i) {return { + r: (i & 0xff), + g: ((i & 0xff00) >> 8), + b: ((i & 0xff0000) >> 16), + a: (i > 0x7fffffff ? 255 : (((i & 0xff000000) >>> 24) * 2)+1) + // I don't think alpha transparency is actually USED in icons?, rendering with it looks strange. + }}; + const magic = u32le(0); + if (magic !== 0x010000) { + // USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic. + // Can someone provide me pointers to such an icon if one exists? + throw `Not a PS2 icon file (was ${magic}, expected ${0x010000})`; + } + const numberOfShapes = u32le(4); + if(numberOfShapes > 16) { + throw `Too many defined shapes! Is this a valid file? (file reports ${numberOfShapes} shapes)`; + } + const textureType = u32le(8); + const textureFormat = getTextureFormat(textureType); + //:skip 4 + const numberOfVertexes = u32le(16); + if(!!(numberOfVertexes % 3)){ + throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`; + } + // format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk] + let offset = 20; + let vertices = new Array(); + const chunkLength = ((numberOfShapes * 8) + 16); + + // for numberOfVertexes, copy chunkLength x times, then append with normal, uv and rgba8 color. Floats are 16-bit. + for (let index = 0; index < numberOfVertexes; index++) { + let shapes = new Array(); + for (let indey = 0; indey < numberOfShapes; indey++) { + shapes.push({ + x: f16le(offset+((chunkLength*index)+(indey*8))), + y: f16le(offset+((chunkLength*index)+(indey*8))+2), + z: f16le(offset+((chunkLength*index)+(indey*8))+4) + }); + //:skip 2 + } + + let normal = { + x: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))), + y: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+2), + z: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+4) + }; + //:skip 2 + let uv = { + u: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+8), + v: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+10) + }; + let color = u32_rgba8(u32le(offset+(chunkLength*index)+((numberOfShapes * 8))+12)); + // keep original u32le color? + vertices.push({shapes, normal, uv, color}); + } + offset = (20+(numberOfVertexes * chunkLength)); + animationHeader = {id: u32le(offset), length: u32le(offset+4), speed: f32le(offset+8), "offset": u32le(offset+12), keyframes: u32le(offset+16)}; + let animData = new Array(); + // now we have to enumerate values, so now we introduce an offset value. + // format for a keyframe: sssskkkk[ffffvvvv] where [ffffvvvv] repeat based on the value that kkkk(eys) has. + // sssskkkk[ffffvvvv] is repeated based on animationHeader.keyframes value. + offset += 20; + for (let index = 0; index < animationHeader.keyframes; index++) { + let frameData = new Array(); + let shapeId = u32le(offset); + let keys = u32le(offset+4); + offset += 8; + for (let indey = 0; indey < keys; indey++) { + frameData.push({frame: f32le(offset), value: f32le(offset+4)}); + offset += 8; + } + animData.push({shapeId, keys, frameData}); + } + let texture = null; + switch(textureFormat) { + case 'N': { + break; + } + case 'U': { + //where every 16-bit entry is a BGR5A1 color 0b[bbbbbgggggrrrrra] + texture = new Uint16Array(input.slice(offset, (offset+0x8000))); + //see convertBGR5A1toRGB5A1() for more info. + break; + } + case 'C': { + // compression format is RLE-based, where first u32 is size, and format is defined as: + /** + * u16 rleType; + * if (rleType >= 0xff00) { + * //do a raw copy + * let length = (0x10000 - rleType); + * byte data[length]; + * } else { + * //repeat next byte rleType times + * data = new Uint16Array(rleType); + * for (let index = 0; index < rleType; index++) { + * data[index] = u16 repeater @ +4; + * } + * } + **/ + //output of this will be another u16[0x4000] of the decompressed texture + //after that just parse output as-if it was uncompressed. + //see uncompressTexture() and convertBGR5A1toRGB5A1() for more info. + size = u32le(offset); + texture = {size, data: input.slice(offset+4, offset+(4+size))}; + } + } + if(ICONJS_DEBUG){ + console.debug({magic, numberOfShapes, textureType, textureFormat, numberOfVertexes, chunkLength, vertices, animationHeader, animData, texture}); + } + return {numberOfShapes, vertices, textureFormat, texture, animData}; +} + +function readEntryBlock(input) { + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)}; + const t64le = function(i){return { + seconds: view.getUint8(i+1), + minutes: view.getUint8(i+2), + hours: view.getUint8(i+3), + day: view.getUint8(i+4), + month: view.getUint8(i+5), + year: view.getUint16(i+6, 1) + }}; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly! + //!pattern psu_file.hexpat + const permissions = u32le(0); + let type; + if (permissions>0xffff) { + throw `Not a EMS Memory Adapter (PSU) export file (was ${permissions}, expected less than ${0xffff})`; + } + if((permissions & 0b00100000)>=1){ + type = "directory"; + } + if((permissions & 0b00010000)>=1){ + type = "file"; + } + if((permissions & 0b0001100000000000)>=1){ + throw `I don't parse portable applications or legacy save data. (${permissions} has bits 10 or 11 set)`; + } + const size = u32le(4); + const createdTime = t64le(8); + const sectorOffset = u32le(16); + const dirEntry = u32le(20); + const modifiedTime = t64le(24); + const specialSection = input.slice(0x20, 0x40); + const int_filename = input.slice(0x40, 512); + const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); + if(ICONJS_DEBUG){ + console.debug({permissions, type, size, createdTime, sectorOffset, dirEntry, modifiedTime, specialSection, filename}); + } + return {type, size, filename, createdTime, modifiedTime}; +} + +function readEmsPsuFile(input){ + const header = readEntryBlock(input.slice(0,0x1ff)); + if(header.size > 0x7f) { + throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})` + } + let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: {created: header.createdTime, modified: header.modifiedTime}}; + let output = new Object(); + let offset = 512; + for (let index = 0; index < header.size; index++) { + fdesc = readEntryBlock(input.slice(offset, offset + 512)); + switch(fdesc.type) { + case "directory": { + offset += 512; + output[fdesc.filename] = null; + break; + } + case "file": { + if(ICONJS_DEBUG){ + console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); + } + offset += 512; + const originalOffset = offset; + if((fdesc.size % 1024) > 0) { + offset += ((fdesc.size & 0b11111111110000000000) + 1024); + } else { + offset += fdesc.size; + // if we're already filling 1k blocks fully, why change the value? + } + output[fdesc.filename] = { + size: fdesc.size, + data: input.slice(originalOffset, originalOffset+fdesc.size) + }; + break; + } + } + } + fsOut[header.filename] = output; + return fsOut; +} + +function readPsvFile(input){ + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)}; + const t64le = function(i){return { + seconds: view.getUint8(i+1), + minutes: view.getUint8(i+2), + hours: view.getUint8(i+3), + day: view.getUint8(i+4), + month: view.getUint8(i+5), + year: view.getUint16(i+6, 1) + }}; + //!pattern psv_file.hexpat + const magic = u32le(0); + if (magic !== 0x50535600) { + throw `Not a PS3 export (PSV) file (was ${magic}, expected ${0x50535600})`; + } + //:skip 4 + //:skip 20 // key seed, console ignores this + //:skip 20 // sha1 hmac digest, useful for verifying that this is, indeed, save data. + //:skip 8 + const type1 = u32le(56); + const type2 = u32le(60); + if(type1 !== 0x2c && type2 !== 2) { + throw `Not parsing, this is not in the PS2 save export format (was ${type1}:${type2}, expected 44:2)`; + } + const displayedSize = u32le(64); + const ps2dOffset = u32le(68); + const ps2dSize = u32le(72); // don't know why this is included if its always 964 + const nModelOffset = u32le(76); + const nModelSize = u32le(80); + const cModelOffset = u32le(84); + const cModelSize = u32le(88); + const dModelOffset = u32le(92); + const dModelSize = u32le(96); + const numberOfFiles = u32le(100); // in-case this library changes stance on other files + // file = {t64le created, t64le modified, u32 size, u32 permissions, byte[32] title} + // and if it's not the root directory, add another u32 for offset/location + const rootDirectoryData = input.slice(104, 162); + const timestamps = {created: t64le(104), modified: t64le(112)}; + let offset = 162; + let fileData = new Array(); + for (let index = 0; index < numberOfFiles; index++) { + fileData.push(input.slice(offset,offset+0x3c)); + offset += 0x3c; + }; + //then file data after this but we already have pointers to the files we care about + const icons = { + n: input.slice(nModelOffset, nModelOffset+nModelSize), + c: input.slice(cModelOffset, cModelOffset+cModelSize), + d: input.slice(dModelOffset, dModelOffset+dModelSize), + } + if (ICONJS_DEBUG) { + console.debug({magic, type1, type2, displayedSize, ps2dOffset, ps2dSize, nModelOffset, nModelSize, cModelOffset, cModelSize, dModelOffset, dModelSize, numberOfFiles, rootDirectoryData, fileData}) + } + return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps}; +} + +function readSxpsDescriptor(input) { + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)}; + const t64le = function(i){return { + seconds: view.getUint8(i+1), + minutes: view.getUint8(i+2), + hours: view.getUint8(i+3), + day: view.getUint8(i+4), + month: view.getUint8(i+5), + year: view.getUint16(i+6, 1) + }}; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly! + //!pattern sps-xps_file.hexpat + //:skip 2 // ... it's the file descriptor block size (including the bytes themselves, so 250) + const int_filename = input.slice(2, 66); + const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); + const size = u32le(66); + const startSector = u32le(70); + const endSector = u32le(74); + const permissions = u32le(78); // the first two bytes are *swapped*. The comments that ensued were not kept. + let type; + if (permissions>0xffff) { + throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${permissions}, expected less than ${0xffff})`; + } + if((permissions & 0b0010000000000000)>=1){ + type = "directory"; + } + if((permissions & 0b0001000000000000)>=1){ + type = "file"; + } + if((permissions & 0b00011000)>=1){ + throw `I don't parse portable applications or legacy save data. (${permissions} has bits 4 or 5 set)`; + } + const timestamps = {created: t64le(82), modified: t64le(90)}; + //:skip 4 + //:skip 4 - u32 optional (98) + //:skip 8 - t64 optionalTime (102) // I don't know why this is here. + const int_asciiName = input.slice(114, 178); + const int_shiftjisName = input.slice(178, 242); // Because why parse a PS2D when you can hard-code it? + //:skip 8 + if(ICONJS_DEBUG) { + console.debug({int_filename, size, startSector, endSector, permissions, type, timestamps, int_asciiName, int_shiftjisName}); + } + return {type, size, filename, timestamps}; +} + +function readSharkXPortSxpsFile(input) { + const view = new DataView(input); + const u32le = function(i){return view.getUint32(i, 1)}; + //!pattern sps-xps_file.hexpat + const identLength = u32le(0); + if(identLength !== 13) { + throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${identLength}, expected 13)`; + } + let offset = 4; + const ident = input.slice(offset, offset+identLength); + if((new TextDecoder("utf-8")).decode(ident) !== "SharkPortSave") { + throw `Unrecognized file identification string. Expected "SharkPortSave".` + } + offset += (identLength + 4); + const titleLength = u32le(offset); + const title = input.slice(offset + 4, (offset + 4) + titleLength); + offset += (titleLength + 4); + const descriptionLength = u32le(offset); + const description = input.slice(offset + 4, (offset + 4) + descriptionLength); + offset += (descriptionLength + 8); + const comments = { + "game": stringScrubber((new TextDecoder("utf-8")).decode(title)), + "name": stringScrubber((new TextDecoder("utf-8")).decode(description)) + } + const totalSize = u32le(offset); + offset += 4; + const header = readSxpsDescriptor(input.slice(offset, offset + 250)); + offset += 250; + // alright now lets parse some actual data + let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments}; + let output = new Object(); + for (let index = 0; index < (header.size - 2); index++) { + fdesc = readSxpsDescriptor(input.slice(offset, offset + 250)); + switch(fdesc.type) { + case "directory": { + offset += 250; + output[fdesc.filename] = null; + break; + } + case "file": { + if(ICONJS_DEBUG){ + console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); + } + offset += 250; + output[fdesc.filename] = { + size: fdesc.size, + data: input.slice(offset, offset+fdesc.size) + }; + offset += fdesc.size; + break; + } + } + } + fsOut[header.filename] = output; + //:skip 4 // then here lies, at offset (the end of file), a u32 checksum. + return fsOut; +} + +if(typeof module !== "undefined") { + // for C6JS + module.exports = { + readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile}, + helpers: {uncompressTexture, convertBGR5A1toRGB5A1}, + options: {setDebug, setStrictness}, + version: ICONJS_VERSION + }; } \ No newline at end of file diff --git a/index.js b/index.js index f4103eb..889d229 100644 --- a/index.js +++ b/index.js @@ -1,87 +1,87 @@ -const icondumper2 = require("./icon.js"); -const iconjs = icondumper2.readers; -const filesystem = require("fs"); -const processObj = require("process"); - -// to make it viewable -require("util").inspect.defaultOptions.maxArrayLength = 10; -require("util").inspect.defaultOptions.compact = true; -require("util").inspect.defaultOptions.depth = 2; - -// output debugging information -icondumper2.options.setDebug(false); - -// node.js client -console.log(`icon.js version ${icondumper2.version}, ${(new Date()).getFullYear().toString()} (c) yellows111`); -switch(processObj.argv[2]) { - case "psu": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu"); - const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); - let output = {parsed, PS2D} - Object.keys(PS2D.filenames).forEach(function(file) { - output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data); - }); - console.log(output); - break; - } - case "psv": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv"); - const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - console.log(parsed); - const PS2D = iconjs.readPS2D(parsed["icon.sys"]); - let output = {parsed, PS2D}; - console.log(output); - break; - } - case "sps": - case "xps": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps"); - const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - console.log(parsed); - const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); - let output = {parsed, PS2D} - Object.keys(PS2D.filenames).forEach(function(file) { - output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data); - }); - console.log(output); - break; - } - case "sys": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys"); - const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); - console.log("\noutput:", metadata, "\n") - if(processObj.argv.length > 4 && processObj.argv[4].toLowerCase() === "--no-read-models") {break;} else { - Object.keys(metadata.filenames).forEach(function(file) { - let getFile = filesystem.readFileSync(metadata.filenames[file]); - const output = iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)); - //console.log(individialIcon); - console.log(`contents of ${metadata.filenames[file]} (${file}):`, output, "\n"); - }); - } - break; - } - case "ico": - case "icn": { - let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "input.icn"); - console.log(`contents of ${require("path").basename(processObj.argv[3])}:`, iconjs.readIconFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength))); - break; - } - default: { - //Template literal goes here. - console.log( -`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client subcommands: -psu: Read a EMS Memory Adapter export file. -psv: Read a PS3 export file. -sps: Read a SharkPort export file. -xps: Read a X-Port export file. - -sys: Read a icon.sys (964 bytes) file, and attempt - to read icon files from the current directory. - (suppress behaviour with --no-read-models) -icn: Read an icon file directly. (Also as: ico) -` ); // end of template - } - processObj.exit(1); -} +const icondumper2 = require("./icon.js"); +const iconjs = icondumper2.readers; +const filesystem = require("fs"); +const processObj = require("process"); + +// to make it viewable +require("util").inspect.defaultOptions.maxArrayLength = 10; +require("util").inspect.defaultOptions.compact = true; +require("util").inspect.defaultOptions.depth = 2; + +// output debugging information +icondumper2.options.setDebug(false); + +// node.js client +console.log(`icon.js version ${icondumper2.version}, ${(new Date()).getFullYear().toString()} (c) yellows111`); +switch(processObj.argv[2]) { + case "psu": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu"); + const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + let output = {parsed, PS2D} + Object.keys(PS2D.filenames).forEach(function(file) { + output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data); + }); + console.log(output); + break; + } + case "psv": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv"); + const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + console.log(parsed); + const PS2D = iconjs.readPS2D(parsed["icon.sys"]); + let output = {parsed, PS2D}; + console.log(output); + break; + } + case "sps": + case "xps": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps"); + const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + console.log(parsed); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + let output = {parsed, PS2D} + Object.keys(PS2D.filenames).forEach(function(file) { + output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data); + }); + console.log(output); + break; + } + case "sys": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys"); + const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + console.log("\noutput:", metadata, "\n") + if(processObj.argv.length > 4 && processObj.argv[4].toLowerCase() === "--no-read-models") {break;} else { + Object.keys(metadata.filenames).forEach(function(file) { + let getFile = filesystem.readFileSync(metadata.filenames[file]); + const output = iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)); + //console.log(individialIcon); + console.log(`contents of ${metadata.filenames[file]} (${file}):`, output, "\n"); + }); + } + break; + } + case "ico": + case "icn": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "input.icn"); + console.log(`contents of ${require("path").basename(processObj.argv[3])}:`, iconjs.readIconFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength))); + break; + } + default: { + //Template literal goes here. + console.log( +`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client subcommands: +psu: Read a EMS Memory Adapter export file. +psv: Read a PS3 export file. +sps: Read a SharkPort export file. +xps: Read a X-Port export file. + +sys: Read a icon.sys (964 bytes) file, and attempt + to read icon files from the current directory. + (suppress behaviour with --no-read-models) +icn: Read an icon file directly. (Also as: ico) +` ); // end of template + } + processObj.exit(1); +} processObj.exit(0); \ No newline at end of file diff --git a/input.htm b/input.htm index 913d26c..2d4b6f7 100644 --- a/input.htm +++ b/input.htm @@ -1,579 +1,579 @@ - - - - - - - icondumper2 - HTML reference client - - - - - - - - - - - - - - (enables console-accurate title parsing) | - - -
-
-

No File

-

Loaded

-
- Background/icon preview (rotate: ←/→ keys, scale: ↑/↓ keys):
- - -
-

Normal: (no file) Copying: (no file) Deleting: (no file)

-
-
- - -
- - -
-
-
- - -
- - -
- - -
-

- Date created: --:--:-- --/--/---- UTC+09:00 - - Date modified: --:--:-- --/--/---- UTC+09:00 -

-

- File comments: (no title) - (no description) -

- - icondumper2 (unknown icon.js version) [C: Loading...] — © 2023 yellows111 - - + + + + + + + icondumper2 - HTML reference client + + + + + + + + + + + + + + (enables console-accurate title parsing) | + + +
+
+

No File

+

Loaded

+
+ Background/icon preview (rotate: ←/→ keys, scale: ↑/↓ keys):
+ + +
+

Normal: (no file) Copying: (no file) Deleting: (no file)

+
+
+ + +
+ + +
+
+
+ + +
+ + +
+ + +
+

+ Date created: --:--:-- --/--/---- UTC+09:00 + + Date modified: --:--:-- --/--/---- UTC+09:00 +

+

+ File comments: (no title) - (no description) +

+ + icondumper2 (unknown icon.js version) [C: Loading...] — © 2023 yellows111 + + \ No newline at end of file