Compare commits

..

10 Commits

Author SHA1 Message Date
yellows111 f05ad6fac4 Soft client interpolation update, minor security update.
Interpolation between two frames of animation is now implemented,
and can be controlled with the PERIOD and COMMA keys.

Automatic playing of interpolated frames is not yet complete.

Security:
All objects which can output arbitrary text has their prototype set to null.

Some other objects have also been set to null prototypes,
which are t64le and the yellowDataReader constructor output.

Still yet to parse actual animation data...
Probably need to know how to make requestAnimationFrame loops practical...

Authors Comment: This filled my evening from doing nothing.
2024-02-29 13:43:08 +00:00
yellows111 a6ae91dffc formatting changes
* made lzari.js very es5-friendly.
* added jshint (yes, seriously) ruling to icon.js and lzari.js.
* Updated README.md
* Fixed up the werid mixed syntax on iconwriter.js
* -> basically made all the tab-based jank that only showed up correctly in...
* n++ a bit more readable outside of such. (minor tab to space consistancy fix)
* Added and removed semicolons to match some syntax standards.
* Made some things that should of been variables, variables !(global_leakage)
* expand module exporting code to clean up some 'object short notation' uses.
2024-02-21 01:45:19 +00:00
yellows111 8ab26610e6 iconwriter: write an additional extra 16 icons
Now going from 0x00 to 0x1f.
Removed seperate compressed texture code, as I can just make one binary array, and it works fine.

No texture is green.
Uncompressed is red.
Compression is blue.
2024-02-15 08:27:07 +00:00
yellows111 b0212bc607 0.8.2: It's a bitmask.
I knew writing that as a test would be worth it.
2024-02-15 00:14:08 +00:00
yellows111 4031ec9e18 Fix glTF exporter not writing the pbrMetallicRoughness correctly.
* added tests/iconwriter.js, which writes icons with formats 0-7.

Authors notes: I am aware that texture format 1 may work the same as texture format 3.
I am waiting on the tests provided by iconwriter.js to verify the functionality.
2024-02-14 15:16:53 +00:00
yellows111 aa53573a14 minor non-final changes
Reminder of TODOs:
Animation parsing, parsing animation data into visible data. ->
* Render animations in HTML client.
* Write animations to glTF2 exporter.

Considered ideas:
* Multi-format file input box (auto-detect format based off first bytes)
2023-12-25 08:20:58 +00:00
yellows111 d57d9129de Bumped RLE raw-copy threshold by 0x100
the icons that Gauntlet: Dark Legacy uses for it's save games are rarely even worth being compressed, considering that it only saves around 2900 bytes from just leaving it uncompressed. However, the fact that it's still compressed while still parsing properly ONLY with this change makes me think many others have had this constant wrong for a while. Could it be lower? needs research.

* changed a space for a no-break space in a file input description to make it flow better when text is split.

I was considering making this 0.8.0+u1, but eeh, nah.
2023-12-18 23:00:14 +00:00
yellows111 23ef7c7ca1 0.8.0 (actually) forgot to -a 2023-12-18 14:03:51 +00:00
yellows111 eb8bc8e068 0.8.0: "Maximum Power"
I think this is it. The last major format for icondumper2.
It's been a fun ride while it lasted.
See you on the flip-side! Thanks for the pineapples.

+ Now supports MAX files
+ Added an LZARI implementation to the repository.
* Now considering icondumper2 feature-complete.
* [HTML] Fixed no-webgl fallback mode.
* Reworked modules object to not clobber one that already exists
- Dropped "goes here" terminology from file input boxes.

Ending comments: There's still a few bugs involving the HTML client...
Ambient lighting is quite jank. Needs better support...
Feature: Do interpolated animations of models

For now though... I want to do something else... for a bit, at least.
2023-12-18 14:02:10 +00:00
yellows111 73e618369b cosmetic update to HTML refclient
* changed inline-grid to table-cell, I like it better, and older browsers like it better since its not technically flex-width
- removed inflator-installed id from inflator script, as it was unused
* changed pako version to es5 version, since it works with the absolute minimum version browsers that we technically target, and performance isn't the most important thing when you only need to decompress the data once per export file.

Author's comments: Wondering how LZARI works. Not much I can add to that part of the conversation. Once it's done, it's done. Until then, progress may be stale...
2023-12-08 23:00:36 +00:00
9 changed files with 1073 additions and 190 deletions

View File

@ -14,7 +14,7 @@ jobs:
- name: Install node.js - name: Install node.js
uses: actions/setup-node@main uses: actions/setup-node@main
- name: Compile documentation with JSDoc - name: Compile documentation with JSDoc
run: npx jsdoc ./icon.js -d ./documentation -R ./README.md run: npx jsdoc ./icon.js ./lzari.js -d ./documentation -R ./README.md
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@main uses: actions/upload-artifact@main
with: with:

2
.gitignore vendored
View File

@ -13,6 +13,8 @@ icon.sys
*.p2m *.p2m
*.md *.md
*.cbs *.cbs
*.max
*.pws
# Unused data # Unused data

View File

@ -6,9 +6,11 @@ A set of vertices with may or may not include a texture while defining colours f
## Why? ## Why?
Current implementations had some issues with rendering some icons. These were mostly: Current implementations had some issues with rendering some icons. These were mostly:
* Not rendering any color for texture type 3. * Not rendering any color for texture types 0-3.
* Failing to decompress some specific RLE-compressed icons. (types above 8) * Failing to decompress some specific RLE-compressed icons. (types with bit 4 enabled)
* Requires writing/reading a specific format for successful output of data. * Requires writing/reading a specific format for successful output of data.
* Incorrect analysis of texture types leading to assuming texture type 1 isn't the same group as texture type 3.
* Further incorrect analysis revealing it's that the texture type is a bitmask.
As of writing, there was no exporter that exists for the format that exhibited one of these problems. As of writing, there was no exporter that exists for the format that exhibited one of these problems.
@ -20,6 +22,7 @@ As of writing, there was no exporter that exists for the format that exhibited o
* SharkPort export files (.sps) * SharkPort export files (.sps)
* X-Port export files (.xps) * X-Port export files (.xps)
* CodeBreaker Save export files (.cbs) * CodeBreaker Save export files (.cbs)
* Max Drive "PowerSave"/export files (.max)
* PS2 icons (.ico, .icn) * PS2 icons (.ico, .icn)
* PS2D format (icon.sys) * PS2D format (icon.sys)
@ -30,6 +33,7 @@ As of writing, there was no exporter that exists for the format that exhibited o
* CommonJS (that includes node!) module exporting while still being compatible with other JavaScript implementations. * CommonJS (that includes node!) module exporting while still being compatible with other JavaScript implementations.
* Convert a 128x128x16 BGR5A1 bitmap to a standard RGB5A1 format. * Convert a 128x128x16 BGR5A1 bitmap to a standard RGB5A1 format.
* Convert an icon or a set of icons to glTF 2, with textures saved as PNG. * Convert an icon or a set of icons to glTF 2, with textures saved as PNG.
* Decompress any LZARI-formatted data.
## What it doesn't do: ## What it doesn't do:
* Create, manipulate or otherwise taint save files. * Create, manipulate or otherwise taint save files.
@ -37,9 +41,9 @@ As of writing, there was no exporter that exists for the format that exhibited o
* Use any implementation-specific features. * Use any implementation-specific features.
## Client compatibility: ## Client compatibility:
The library requires use of `const`, `let` and `class` declarations. The library currently requires use of `const`, `let` and `class` declarations, template literals, and destructuring assignment for variables.
Any JavaScript implementation should work if they support all three of these declarations. Any JavaScript implementation should work if they support all of the required features.
### Tested clients: ### Tested clients:
* Chrome (or Blink-based browser) 49 (or higher) - HTML reference client * Chrome (or Blink-based browser) 49 (or higher) - HTML reference client
@ -51,11 +55,13 @@ Because it replaced what *was* left of icondumper (1).
## Included files: ## Included files:
| File | Description | | File | Description |
| ---------------- | ----------------------------------------- | | ------------------- | ----------------------------------------------- |
| icon.js | The library itself. | | icon.js | The library itself. |
| index.js | Node.js example client. | | index.js | Node.js example client. |
| gltf-exporter.js | Node.js client to export icons to glTF 2. | | gltf-exporter.js | Node.js client to export icons to glTF 2. |
| index.htm | HTML reference client. | | index.htm | HTML reference client. |
| lzari.js | A LZARI decompression-only library. |
| tests/iconwriter.js | Node.js. Creates icons with texture types 0-31. |
## Included example files: ## Included example files:
| Directory | Description | Formats | | Directory | Description | Formats |

View File

@ -99,13 +99,22 @@ function imf2gltf(icon = null, filename = "untitled") {
}]; // no indices because who needs indexing when you're transcoding? }]; // no indices because who needs indexing when you're transcoding?
gltfOutput.materials = [{ gltfOutput.materials = [{
"name": `Material (${filename}#${index})`, "name": `Material (${filename}#${index})`,
"pbrMetallicRoughness": { "pbrMetallicRoughness": null,
"baseColorTexture": {"index":0, "texCoord": 0}
},
"extensions": { // or we get annoying PBR and specular stuff we don't need "extensions": { // or we get annoying PBR and specular stuff we don't need
"KHR_materials_unlit": {} "KHR_materials_unlit": {}
} }
}]; }];
if(icon.textureFormat !== "N") {
gltfOutput.materials[0].pbrMetallicRoughness = {
"baseColorTexture": {"index":0, "texCoord": 0}
};
} else {
gltfOutput.materials[0].pbrMetallicRoughness = {
"baseColorFactor": [1.0, 1.0, 1.0, 1.0],
"metallicFactor": 0.0,
"roughnessFactor": 1.0
}
}
gltfOutput.buffers = [{"uri": `${filename}.bin`, "byteLength": outputFloatArray.byteLength}]; gltfOutput.buffers = [{"uri": `${filename}.bin`, "byteLength": outputFloatArray.byteLength}];
gltfOutput.bufferViews = [ gltfOutput.bufferViews = [
{ {
@ -173,16 +182,14 @@ function imf2gltf(icon = null, filename = "untitled") {
]; ];
gltfOutput.asset = {"version": "2.0", "generator": `icondumper2/${icondumper2.version}`} gltfOutput.asset = {"version": "2.0", "generator": `icondumper2/${icondumper2.version}`}
gltfOutput.extensionsUsed = ["KHR_materials_unlit"]; gltfOutput.extensionsUsed = ["KHR_materials_unlit"];
if(icon.textureFormat !== "N") {
gltfOutput.textures = [{"source": 0}]; gltfOutput.textures = [{"source": 0}];
gltfOutput.images = [{"name": `Texture (${filename}#${index})`, "uri": `${filename}.png`}] gltfOutput.images = [{"name": `Texture (${filename}#${index})`, "uri": `${filename}.png`}]
}
gltfOutputArray[index] = (gltfOutput); gltfOutputArray[index] = (gltfOutput);
} }
let texture16 = null; // Uint16Array(16384) let texture16 = null; // Uint16Array(16384)
switch(icon.textureFormat) { switch(icon.textureFormat) {
case "N": {
texture16 = (new Uint16Array(16384)).fill(0xffff);
break;
}
case "C": { case "C": {
texture16 = icondumper2.helpers.uncompressTexture(icon.texture.data); texture16 = icondumper2.helpers.uncompressTexture(icon.texture.data);
break; break;
@ -192,6 +199,7 @@ function imf2gltf(icon = null, filename = "untitled") {
break; break;
} }
} }
if(texture16 !== null) {
let texture24 = new Uint8Array(49983); let texture24 = new Uint8Array(49983);
texture24.set([ texture24.set([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
@ -243,6 +251,9 @@ function imf2gltf(icon = null, filename = "untitled") {
0xae, 0x42, 0x60, 0x82 0xae, 0x42, 0x60, 0x82
], textureOffset); ], textureOffset);
return {objects: gltfOutputArray, buffer: outputFloatArray, texture: texture24}; return {objects: gltfOutputArray, buffer: outputFloatArray, texture: texture24};
} else {
return {objects: gltfOutputArray, buffer: outputFloatArray, texture: null};
}
} }
function loadAndConvertIcon(inputData, attemptedFilename = "-") { function loadAndConvertIcon(inputData, attemptedFilename = "-") {
@ -257,9 +268,10 @@ function loadAndConvertIcon(inputData, attemptedFilename = "-") {
} }
(require("fs")).writeFileSync(`${filename}.bin`, glTF_output.buffer); (require("fs")).writeFileSync(`${filename}.bin`, glTF_output.buffer);
console.info(`Saved glTF buffer as "${filename}.bin".`); console.info(`Saved glTF buffer as "${filename}.bin".`);
if(glTF_output.texture !== null) {
(require("fs")).writeFileSync(`${filename}.png`, glTF_output.texture); (require("fs")).writeFileSync(`${filename}.png`, glTF_output.texture);
console.info(`Saved texture as "${filename}.png".\n`); console.info(`Saved texture as "${filename}.png".\n`);
}
} }
// can anything de-dupe this code somehow? (index.js) // can anything de-dupe this code somehow? (index.js)
@ -322,6 +334,23 @@ switch(processObj.argv[2]) {
} }
break; break;
} }
case "max":
case "pws": {
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.max");
function myUnlzari(inputBuffer) {
return (require("./lzari.js").decodeLzari(inputBuffer)).buffer;
}
const parsed = iconjs.readMaxPwsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength), myUnlzari);
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": { case "sys": {
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.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)); const PS2D = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
@ -352,6 +381,8 @@ psv: Read a PS3 export file.
sps: Read a SharkPort export file. sps: Read a SharkPort export file.
xps: Read a X-Port export file. xps: Read a X-Port export file.
cbs: Read a CodeBreaker Save export file. cbs: Read a CodeBreaker Save export file.
max: Read a Max Drive export file.
pws: Read a PowerSave export file.
sys: Read a icon.sys (964 bytes) file, and attempt sys: Read a icon.sys (964 bytes) file, and attempt
to read icon files from the current directory. to read icon files from the current directory.

188
icon.js
View File

@ -1,5 +1,5 @@
//To swap between mjs/esm and c6js, go to the end of this file, and (un)comment your wanted module mode. //To swap between mjs/esm and c6js, go to the end of this file, and (un)comment your wanted module mode.
//LOOKING FOR: LZARI implementation (for MAX and PWS files. THIS WILL COMPLETE ICONDUMPER2.) /* jshint bitwise: false, esversion: 6, -W009, -W010 */ // not doing this makes linters scream about BWOs, es6 features, and using new Primitive() instead of said primitives
var ICONJS_DEBUG = false; var ICONJS_DEBUG = false;
var ICONJS_STRICT = true; var ICONJS_STRICT = true;
@ -8,7 +8,7 @@ var ICONJS_STRICT = true;
* @constant {string} * @constant {string}
* @default * @default
*/ */
const ICONJS_VERSION = "0.7.0"; const ICONJS_VERSION = "0.8.4";
/** /**
* The RC4 key used for ciphering CodeBreaker Saves. * The RC4 key used for ciphering CodeBreaker Saves.
@ -63,22 +63,22 @@ class yellowDataReader extends DataView {
* @param {number} i Indice offset. * @param {number} i Indice offset.
* @returns {number} * @returns {number}
*/ */
u16le(i){return super.getUint16(i, 1)}; u16le(i){return super.getUint16(i, 1);}
/** Fixed-point 16-bit, Little Endian. /** Fixed-point 16-bit, Little Endian.
* @param {number} i Indice offset. * @param {number} i Indice offset.
* @returns {number} * @returns {number}
*/ */
f16le(i){return (super.getInt16(i, 1) / 4096)}; f16le(i){return (super.getInt16(i, 1) / 4096);}
/** Unsigned 32-bit, Little Endian. /** Unsigned 32-bit, Little Endian.
* @param {number} i Indice offset. * @param {number} i Indice offset.
* @returns {number} * @returns {number}
*/ */
u32le(i){return super.getUint32(i, 1)}; u32le(i){return super.getUint32(i, 1);}
/** Floating-point 32-bit, Little Endian. /** Floating-point 32-bit, Little Endian.
* @param {number} i Indice offset. * @param {number} i Indice offset.
* @returns {number} * @returns {number}
*/ */
f32le(i){return super.getFloat32(i, 1)}; f32le(i){return super.getFloat32(i, 1);}
/** 64-bit Timestamp structure, Little Endian. /** 64-bit Timestamp structure, Little Endian.
* Time returned is set for JST (UTC+09:00) instead of UTC. * Time returned is set for JST (UTC+09:00) instead of UTC.
* Time returned is going to be offseted for JST (GMT+09:00). * Time returned is going to be offseted for JST (GMT+09:00).
@ -92,13 +92,14 @@ class yellowDataReader extends DataView {
* @property {number} year - Year. * @property {number} year - Year.
*/ */
t64le(i){return { t64le(i){return {
"__proto__": null,
seconds: super.getUint8(i+1), seconds: super.getUint8(i+1),
minutes: super.getUint8(i+2), minutes: super.getUint8(i+2),
hours: super.getUint8(i+3), hours: super.getUint8(i+3),
day: super.getUint8(i+4), day: super.getUint8(i+4),
month: super.getUint8(i+5), month: super.getUint8(i+5),
year: super.getUint16(i+6, 1) year: super.getUint16(i+6, 1)
}}; };}
constructor(buffer) { constructor(buffer) {
super(buffer); super(buffer);
return { return {
@ -107,7 +108,7 @@ class yellowDataReader extends DataView {
u32le: this.u32le.bind(this), u32le: this.u32le.bind(this),
f32le: this.f32le.bind(this), f32le: this.f32le.bind(this),
t64le: this.t64le.bind(this) t64le: this.t64le.bind(this)
} };
} }
} }
@ -152,6 +153,7 @@ function setDebug(value) {
* @public * @public
*/ */
function setStrictness(value) { function setStrictness(value) {
console.info("setStrictness is deprecated!");
ICONJS_STRICT = !!value; ICONJS_STRICT = !!value;
} }
@ -162,15 +164,18 @@ function setStrictness(value) {
* @access protected * @access protected
*/ */
function getTextureFormat(i) { function getTextureFormat(i) {
if (i<8) { if(ICONJS_DEBUG) {
if(i==3) { console.debug("Texture format: %i", i);
return 'N';
} }
return 'U'; // bit 1: enable smooth shading (TODO)
} else if (i>=8) { // bit 2: ??? something weird with colours, check iconwriter.js output...
return 'C'; if(!!(i & 4)) { // if bit 3 (textured)...
} else { if(!!(i & 8)) { // if bit 4 (compressed)...
return void(0); return "C"; // Compressed.
}
return "U"; // Uncompressed.
} else { // if bit 3 isn't set...
return "N"; // No texture.
} }
} }
@ -189,7 +194,7 @@ function uncompressTexture(texData) {
let uncompressed = new Uint16Array(16384); let uncompressed = new Uint16Array(16384);
let offset = 0; let offset = 0;
for (let index = 0; index < 16384;) { for (let index = 0; index < 16384;) {
currentValue = u16le(offset); let currentValue = u16le(offset);
if(currentValue === 0) { if(currentValue === 0) {
// if this is specifically a katamari 1 or 2 icon, skip this byte // if this is specifically a katamari 1 or 2 icon, skip this byte
// because it's formatted like that for some reason // because it's formatted like that for some reason
@ -197,7 +202,7 @@ function uncompressTexture(texData) {
currentValue = u16le(offset); currentValue = u16le(offset);
} }
offset += 2; offset += 2;
if (currentValue >= 0xff00) { if (currentValue >= 0xfe00) { // everyone says this is 0xff00 but gauntlet:DL tells me its lower
//do a raw copy of the next currentValue bytes //do a raw copy of the next currentValue bytes
let length = ((0x10000 - currentValue)); let length = ((0x10000 - currentValue));
for (let enumerator = 0; enumerator < length; enumerator++) { for (let enumerator = 0; enumerator < length; enumerator++) {
@ -210,7 +215,7 @@ function uncompressTexture(texData) {
uncompressed[index] = u16le(offset); uncompressed[index] = u16le(offset);
for (let indey = 0; indey < currentValue; indey++) { for (let indey = 0; indey < currentValue; indey++) {
uncompressed[index] = u16le(offset); uncompressed[index] = u16le(offset);
index++ index++;
} }
offset += 2; offset += 2;
} }
@ -282,13 +287,13 @@ function readPS2D(input) {
{x: f32le(80), y: f32le(84), z: f32le(88)}, //:skip 4 {x: f32le(80), y: f32le(84), z: f32le(88)}, //:skip 4
{x: f32le(96), y: f32le(100), z: f32le(104)}, //:skip 4 {x: f32le(96), y: f32le(100), z: f32le(104)}, //:skip 4
{x: f32le(112), y: f32le(116), z: f32le(120)} //:skip 4 {x: f32le(112), y: f32le(116), z: f32le(120)} //:skip 4
] ];
const lightColors = [ const lightColors = [
{r: f32le(128), g: f32le(132), b: f32le(136), a: f32le(140)}, {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(144), g: f32le(148), b: f32le(152), a: f32le(156)},
{r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)}, {r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)},
{r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)} {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 // 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. // official HDD icon.sys files (completely different PS2ICON text-based format) also say the same.
const int_title = input.slice(0xc0, 0x100); const int_title = input.slice(0xc0, 0x100);
@ -314,7 +319,7 @@ function readPS2D(input) {
n: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_n)), n: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_n)),
c: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_c)), c: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_c)),
d: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_d)) d: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_d))
} };
if(ICONJS_DEBUG){ if(ICONJS_DEBUG){
console.debug({header, titleOffset, bgAlpha, bgColors, lightIndices, lightColors, title, filenames}); console.debug({header, titleOffset, bgAlpha, bgColors, lightIndices, lightColors, title, filenames});
} }
@ -336,7 +341,7 @@ function readIconFile(input) {
b: ((i & 0xff0000) >> 16), b: ((i & 0xff0000) >> 16),
a: (i > 0x7fffffff ? 255 : (((i & 0xff000000) >>> 24) * 2)+1) 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. // I don't think alpha transparency is actually USED in icons?, rendering with it looks strange.
}}; };};
const magic = u32le(0); const magic = u32le(0);
if (magic !== 0x010000) { if (magic !== 0x010000) {
// USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic. // USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic.
@ -351,7 +356,7 @@ function readIconFile(input) {
const textureFormat = getTextureFormat(textureType); const textureFormat = getTextureFormat(textureType);
//:skip 4 //:skip 4
const numberOfVertexes = u32le(16); const numberOfVertexes = u32le(16);
if(!!(numberOfVertexes % 3)){ if((numberOfVertexes % 3) > 0){
throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`; throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`;
} }
// format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk] // format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk]
@ -386,7 +391,7 @@ function readIconFile(input) {
vertices.push({shapes, normal, uv, color}); vertices.push({shapes, normal, uv, color});
} }
offset = (20+(numberOfVertexes * chunkLength)); offset = (20+(numberOfVertexes * chunkLength));
animationHeader = {id: u32le(offset), length: u32le(offset+4), speed: f32le(offset+8), "offset": u32le(offset+12), keyframes: u32le(offset+16)}; const animationHeader = {id: u32le(offset), length: u32le(offset+4), speed: f32le(offset+8), "offset": u32le(offset+12), keyframes: u32le(offset+16)};
let animData = new Array(); let animData = new Array();
// now we have to enumerate values, so now we introduce an offset value. // 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. // format for a keyframe: sssskkkk[ffffvvvv] where [ffffvvvv] repeat based on the value that kkkk(eys) has.
@ -433,7 +438,7 @@ function readIconFile(input) {
//output of this will be another u16[0x4000] of the decompressed texture //output of this will be another u16[0x4000] of the decompressed texture
//after that just parse output as-if it was uncompressed. //after that just parse output as-if it was uncompressed.
//see uncompressTexture() and convertBGR5A1toRGB5A1() for more info. //see uncompressTexture() and convertBGR5A1toRGB5A1() for more info.
size = u32le(offset); const size = u32le(offset);
texture = {size, data: input.slice(offset+4, offset+(4+size))}; texture = {size, data: input.slice(offset+4, offset+(4+size))};
} }
} }
@ -488,13 +493,13 @@ function readEntryBlock(input) {
function readEmsPsuFile(input){ function readEmsPsuFile(input){
const header = readEntryBlock(input.slice(0,0x1ff)); const header = readEntryBlock(input.slice(0,0x1ff));
if(header.size > 0x7f) { if(header.size > 0x7f) {
throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})` throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})`;
} }
let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps}; let fsOut = {"__proto__": null, length: header.size, rootDirectory: header.filename, timestamps: header.timestamps};
let output = new Object(); let output = {"__proto__": null};
let offset = 512; let offset = 512;
for (let index = 0; index < header.size; index++) { for (let index = 0; index < header.size; index++) {
fdesc = readEntryBlock(input.slice(offset, offset + 512)); const fdesc = readEntryBlock(input.slice(offset, offset + 512));
switch(fdesc.type) { switch(fdesc.type) {
case "directory": { case "directory": {
offset += 512; offset += 512;
@ -566,15 +571,21 @@ function readPsvFile(input){
for (let index = 0; index < numberOfFiles; index++) { for (let index = 0; index < numberOfFiles; index++) {
fileData.push(input.slice(offset,offset+0x3c)); fileData.push(input.slice(offset,offset+0x3c));
offset += 0x3c; offset += 0x3c;
}; }
//then file data after this but we already have pointers to the files we care about //then file data after this but we already have pointers to the files we care about
const icons = { const icons = {
n: input.slice(nModelOffset, nModelOffset+nModelSize), n: input.slice(nModelOffset, nModelOffset+nModelSize),
c: input.slice(cModelOffset, cModelOffset+cModelSize), c: input.slice(cModelOffset, cModelOffset+cModelSize),
d: input.slice(dModelOffset, dModelOffset+dModelSize), d: input.slice(dModelOffset, dModelOffset+dModelSize),
} };
if (ICONJS_DEBUG) { if (ICONJS_DEBUG) {
console.debug({magic, type1, type2, displayedSize, ps2dOffset, ps2dSize, nModelOffset, nModelSize, cModelOffset, cModelSize, dModelOffset, dModelSize, numberOfFiles, rootDirectoryData, fileData}) 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}; return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps};
} }
@ -637,7 +648,7 @@ function readSharkXPortSxpsFile(input) {
let offset = 4; let offset = 4;
const ident = input.slice(offset, offset+identLength); const ident = input.slice(offset, offset+identLength);
if((new TextDecoder("utf-8")).decode(ident) !== "SharkPortSave") { if((new TextDecoder("utf-8")).decode(ident) !== "SharkPortSave") {
throw `Unrecognized file identification string. Expected "SharkPortSave".` throw `Unrecognized file identification string. Expected "SharkPortSave".`;
} }
offset += (identLength + 4); offset += (identLength + 4);
const titleLength = u32le(offset); const titleLength = u32le(offset);
@ -657,19 +668,19 @@ function readSharkXPortSxpsFile(input) {
const comments = { const comments = {
"game": stringScrubber((new TextDecoder("utf-8")).decode(title)), "game": stringScrubber((new TextDecoder("utf-8")).decode(title)),
"name": stringScrubber((new TextDecoder("utf-8")).decode(description)) "name": stringScrubber((new TextDecoder("utf-8")).decode(description))
} };
if(description2Length !== 0) { if(description2Length !== 0) {
comments.desc = stringScrubber((new TextDecoder("utf-8")).decode(description2)); comments.desc = stringScrubber((new TextDecoder("utf-8")).decode(description2));
} }
const totalSize = u32le(offset); //const totalSize = u32le(offset); has data, unused in script
offset += 4; offset += 4;
const header = readSxpsDescriptor(input.slice(offset, offset + 250)); const header = readSxpsDescriptor(input.slice(offset, offset + 250));
offset += 250; offset += 250;
// alright now lets parse some actual data // alright now lets parse some actual data
let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments}; let fsOut = {"__proto__": null, length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments};
let output = new Object(); let output = {"__proto__": null};
for (let index = 0; index < (header.size - 2); index++) { for (let index = 0; index < (header.size - 2); index++) {
fdesc = readSxpsDescriptor(input.slice(offset, offset + 250)); const fdesc = readSxpsDescriptor(input.slice(offset, offset + 250));
switch(fdesc.type) { switch(fdesc.type) {
case "directory": { case "directory": {
offset += 250; offset += 250;
@ -703,7 +714,7 @@ function readSharkXPortSxpsFile(input) {
*/ */
function readCodeBreakerCbsDirectory(input) { function readCodeBreakerCbsDirectory(input) {
const {u32le, t64le} = new yellowDataReader(input); const {u32le, t64le} = new yellowDataReader(input);
const virtualFilesystem = new Object(); const virtualFilesystem = {"__proto__": null};
for (let offset = 0; offset < input.byteLength;) { for (let offset = 0; offset < input.byteLength;) {
const timestamps = {created: t64le(offset), modified: t64le(offset+8)}; const timestamps = {created: t64le(offset), modified: t64le(offset+8)};
const dataSize = u32le(offset+16); const dataSize = u32le(offset+16);
@ -728,7 +739,7 @@ function readCodeBreakerCbsDirectory(input) {
*/ */
function readCodeBreakerCbsFile(input, inflator = null) { function readCodeBreakerCbsFile(input, inflator = null) {
if(typeof inflator !== "function") { if(typeof inflator !== "function") {
throw `No inflator function passed. Skipping.`; throw "No inflator function passed. Skipping.";
} }
const {u32le, t64le} = new yellowDataReader(input); const {u32le, t64le} = new yellowDataReader(input);
const magic = u32le(0); const magic = u32le(0);
@ -754,28 +765,105 @@ function readCodeBreakerCbsFile(input, inflator = null) {
const compressedData = input.slice(dataOffset, dataOffset + compressedSize); const compressedData = input.slice(dataOffset, dataOffset + compressedSize);
const decipheredData = rc4Cipher(ICONJS_CBS_RC4_KEY, new Uint8Array(compressedData)); const decipheredData = rc4Cipher(ICONJS_CBS_RC4_KEY, new Uint8Array(compressedData));
const inflatedData = inflator(decipheredData); const inflatedData = inflator(decipheredData);
const fsOut = {rootDirectory: dirName, timestamps}; const fsOut = {"__proto__": null, rootDirectory: dirName, timestamps};
fsOut[dirName] = readCodeBreakerCbsDirectory(inflatedData); fsOut[dirName] = readCodeBreakerCbsDirectory(inflatedData);
if(ICONJS_DEBUG) {
console.debug({magic, dataOffset, compressedSize, dirName, permissions, displayName});
}
return fsOut; return fsOut;
} }
/**
* Read a Max Drive (MAX) or PowerSave (PWS) file's directory structure.
* @param {ArrayBuffer} input - Uncompressed input
* @returns {Object} (user didn't write a description)
* @protected
*/
function readMaxPwsDirectory(input, directorySize) {
const {u32le} = new yellowDataReader(input);
const virtualFilesystem = {"__proto__": null};
let offset = 0;
for (let index = 0; index < directorySize; index++) {
const dataSize = u32le(offset);
const _filename = input.slice(offset+4, offset+36);
const filename = stringScrubber((new TextDecoder("utf-8")).decode(_filename));
if(filename === "") { throw `Unexpected null filename at byte ${offset+4}.`; }
offset += 36;
const data = input.slice(offset, offset+dataSize);
offset += dataSize;
if(index !== directorySize - 1) {
while((offset & 15) !== 8) {
offset++;
}
}
virtualFilesystem[filename] = ({dataSize, data});
}
return virtualFilesystem;
}
/**
* Read a Max Drive (MAX) or PowerSave (PWS) file.
* @param {ArrayBuffer} input - MAX/PWS formatted file
* @param {function(Uint8Array): ArrayBuffer} unlzari - a function which provides a LZARI-compatible decompression function.
* @returns {Object} (user didn't write a description)
* @public
*/
function readMaxPwsFile(input, unlzari) {
if(typeof unlzari !== "function") {
throw "No decompresser function passed. Skipping.";
}
const {u32le} = new yellowDataReader(input);
const ident = input.slice(0, 12);
if((new TextDecoder("utf-8")).decode(ident) !== "Ps2PowerSave") {
throw "Unrecognized file identification string. Expected \"Ps2PowerSave\".";
}
//:skip 4 (u32 checksum)
const _dirName = input.slice(0x10, 0x30);
const dirName = stringScrubber((new TextDecoder("utf-8")).decode(_dirName));
const _displayName = input.slice(0x30, 0x50);
const displayName = stringScrubber((new TextDecoder("utf-8")).decode(_displayName));
const compressedSize = u32le(0x50);
if(compressedSize !== (input.byteLength - 88)) {
console.warn(`This file says it's larger then it actually is! (Given size: ${compressedSize}, actual size: ${input.byteLength - 88})`);
}
const size = u32le(0x54);
const compressedData = input.slice(88, input.byteLength);
const uncompressedData = unlzari(new Uint8Array(compressedData)); // read above why we can't trust given size
const fsOut = {"__proto__": null, rootDirectory: dirName};
fsOut[dirName] = readMaxPwsDirectory(uncompressedData, size);
// there's no... timestamps or permissions... this doesn't bode well.
if(ICONJS_DEBUG) {
console.debug({ident, compressedSize, dirName, displayName});
}
return fsOut;
}
/** /**
* Define (module.)exports with all public functions. * Define (module.)exports with all public functions.
* @exports icondumper2/icon * @exports icondumper2/icon
*/ // start c6js */ // start c6js#
exports = { /* globals exports: true */
readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile}, if(typeof exports !== "object") {
helpers: {uncompressTexture, convertBGR5A1toRGB5A1}, exports = {
options: {setDebug, setStrictness}, readers: {"readIconFile": readIconFile, "readPS2D": readPS2D, "readEmsPsuFile": readEmsPsuFile, "readPsvFile": readPsvFile, "readSharkXPortSxpsFile": readSharkXPortSxpsFile, "readCodeBreakerCbsFile": readCodeBreakerCbsFile, "readMaxPwsFile": readMaxPwsFile},
helpers: {"uncompressTexture": uncompressTexture, "convertBGR5A1toRGB5A1": convertBGR5A1toRGB5A1},
options: {"setDebug": setDebug, "setStrictness": setStrictness},
version: ICONJS_VERSION version: ICONJS_VERSION
}; };
} else {
exports.readers = {"readIconFile": readIconFile, "readPS2D": readPS2D, "readEmsPsuFile": readEmsPsuFile, "readPsvFile": readPsvFile, "readSharkXPortSxpsFile": readSharkXPortSxpsFile, "readCodeBreakerCbsFile": readCodeBreakerCbsFile, "readMaxPwsFile": readMaxPwsFile};
exports.helpers = {"uncompressTexture": uncompressTexture, "convertBGR5A1toRGB5A1": convertBGR5A1toRGB5A1};
exports.options = {"setDebug": setDebug, "setStrictness": setStrictness};
exports.version = ICONJS_VERSION;
}
/* globals module: true */
if(typeof module !== "undefined") { if(typeof module !== "undefined") {
module.exports = exports; module.exports = exports;
} }
//end c6js //end c6js
//start esm //start esm
/*export { /*export {
readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile, uncompressTexture, convertBGR5A1toRGB5A1, setDebug, ICONJS_VERSION readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile, readMaxPwsFile, uncompressTexture, convertBGR5A1toRGB5A1, setDebug, ICONJS_VERSION
};*/ };*/
//end esm //end esm

239
index.htm
View File

@ -4,10 +4,12 @@
<meta charset="utf-8"></meta> <meta charset="utf-8"></meta>
<meta name="viewport" content="initial-scale=1.5"></meta> <meta name="viewport" content="initial-scale=1.5"></meta>
<meta name="description" content="A HTML client for icondumper2"></meta> <meta name="description" content="A HTML client for icondumper2"></meta>
<title>icondumper2 - HTML reference client</title> <title>icondumper2 HTML reference client</title>
<script src="icon.js"></script> <script src="icon.js"></script>
<!-- If you need pako to be optional, remove/comment the bottom line. This will disable support for CBS reading, however --> <!-- Removing or commenting below will disable MAX reading... -->
<script src="https://cdn.jsdelivr.net/npm/pako/dist/pako_inflate.min.js" integrity="sha512-mlnC6JeOvg9V4vBpWMxGKscsCdScB6yvGVCeFF2plnQMRmwH69s9F8SHPbC0oirqfePmRBhqx2s3Bx7WIvHfWg==" crossorigin="anonymous" id="inflator-installed"></script> <script src="lzari.js"></script>
<!-- If you need pako to be optional, remove/comment the line below. This will disable support for CBS reading, however. -->
<script src="https://cdn.jsdelivr.net/npm/pako/dist/pako_inflate.es5.min.js" integrity="sha512-tHdgbM+jAAm3zeGYP67IjUouqHYEMuT/Wg/xvTrfEE7zsSX2GJj0G26pyobvn8Hb2LVWGp+UwsLM2HvXwCY+og==" crossorigin="anonymous"></script>
<style> <style>
html {color: #ccc; background: black; font-family: sans-serif} html {color: #ccc; background: black; font-family: sans-serif}
#title1, #title2 { #title1, #title2 {
@ -23,22 +25,31 @@
#version {text-shadow: 1px 1px 2px black;} #version {text-shadow: 1px 1px 2px black;}
a {color: #ccc;} a {color: #ccc;}
.inputbox { .inputbox {
display: inline-grid; display: table-cell;
margin-right: 0.25em; margin-right: 0.25em;
border: 1px gray solid; border: 1px gray solid;
padding: 0.175em 0.25em 0 0.25em; padding: 0.175em 0.25em 0 0.25em;
margin-bottom: 4px; margin-bottom: 4px;
border-right: 0;
}
.last-input {
border-right: 1px gray solid;
}
.inputbox > input {
width: 100%;
} }
</style> </style>
<meta data-comment="WebGL Shader: Icon"> <meta data-comment="WebGL Shader: Icon">
<script type="text/plain" id="shader-icon-v"> <script type="text/plain" id="shader-icon-v">
attribute vec3 a_position; attribute vec3 a_position;
attribute vec3 a_nextPosition;
attribute vec3 a_normal; attribute vec3 a_normal;
attribute vec2 a_textureCoords; attribute vec2 a_textureCoords;
attribute vec4 a_color; attribute vec4 a_color;
uniform float u_rotation; uniform float u_rotation;
uniform float u_scale; uniform float u_scale;
uniform float u_interp;
uniform highp vec3 u_ambientLight; uniform highp vec3 u_ambientLight;
//uniform highp vec3 u_lightColorA; //uniform highp vec3 u_lightColorA;
//uniform highp vec3 u_lightColorB; //uniform highp vec3 u_lightColorB;
@ -50,11 +61,12 @@
void main() { void main() {
float angle = radians(360.0) * u_rotation; float angle = radians(360.0) * u_rotation;
vec2 pos = vec2(cos(angle), sin(angle)); vec2 pos = vec2(cos(angle), sin(angle));
vec3 lv_interp = mix(a_position, a_nextPosition, u_interp);
// x, y, z, scale (w) // x, y, z, scale (w)
gl_Position = vec4( gl_Position = vec4(
(a_position.x * pos.x) + (a_position.z * pos.y), //transform the x position (lv_interp.x * pos.x) + (lv_interp.z * pos.y), //transform the x position
(0.0 - a_position.y) - 2.75, // invert the y position and move down -2.75, which will center the model (-lv_interp.y) - 2.75, // invert the y position and move down -2.75, which will center the model
(a_position.x * -pos.y) + (a_position.z * pos.x), //transform the z position (lv_interp.x * -pos.y) + (lv_interp.z * pos.x), //transform the z position
u_scale u_scale
); );
// flip it, scale it // flip it, scale it
@ -125,7 +137,7 @@
<h1 id="title1">&#xFF2E;&#xFF4F;&#x3000;&#xFF26;&#xFF49;&#xFF4C;&#xFF45;</h1> <h1 id="title1">&#xFF2E;&#xFF4F;&#x3000;&#xFF26;&#xFF49;&#xFF4C;&#xFF45;</h1>
<h1 id="title2">&#xFF2C;&#xFF4F;&#xFF41;&#xFF44;&#xFF45;&#xFF44;</h1> <h1 id="title2">&#xFF2C;&#xFF4F;&#xFF41;&#xFF44;&#xFF45;&#xFF44;</h1>
</div> </div>
<span>Background/icon preview (Keyboard controls: rotate: &larr;/&rarr;, scale: &uarr;/&darr;, frame-step: &minus;/&equals;, change icon: 1:N/2:C/3:D):</span><br> <span>Background/icon preview (Keyboard controls: rotate: &larr;/&rarr;, scale: &uarr;/&darr;, step: &minus;/=, play: &lt;/&gt;, change icon: 1:N/2:C/3:D):</span><br>
<canvas id="bgcanvas" width="480" height="480"></canvas> <canvas id="bgcanvas" width="480" height="480"></canvas>
<canvas id="iconcanvas" width="480" height="480"></canvas> <canvas id="iconcanvas" width="480" height="480"></canvas>
<hr> <hr>
@ -133,31 +145,35 @@
<div id="advanced"> <div id="advanced">
<hr> <hr>
<div class="inputbox"> <div class="inputbox">
<label for="input">icon.sys goes here:</label> <label for="input">icon.sys:</label>
<input type="file" id="input" name="input" accept=".sys" /> <input type="file" id="input" name="input" accept=".sys" />
</div> </div>
<div class="inputbox"> <div class="inputbox last-input">
<label for="icon">raw icon file goes here:</label> <label for="icon">raw icon file:</label>
<input type="file" id="icon" name="icon" accept=".icn, .ico" /> <input type="file" id="icon" name="icon" accept=".icn, .ico" />
</div> </div>
</div> </div>
<hr> <hr>
<div class="inputbox"> <div class="inputbox">
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label> <label for="psuinput">EMS Memory Adapter export file (.psu):</label>
<input type="file" id="psuinput" name="psuinput" accept=".psu" /> <input type="file" id="psuinput" name="psuinput" accept=".psu" />
</div> </div>
<div class="inputbox"> <div class="inputbox">
<label for="psvinput">PS3 export file (.psv) goes here:</label> <label for="psvinput">PS3 export file (.psv):</label>
<input type="file" id="psvinput" name="psvinput" accept=".psv" /> <input type="file" id="psvinput" name="psvinput" accept=".psv" />
</div> </div>
<div class="inputbox"> <div class="inputbox">
<label for="spsinput">SharkPort/X-Port export file (.sps, .xps) goes here:</label> <label for="spsinput">SharkPort/X-Port export file (.sps,&nbsp;.xps):</label>
<input type="file" id="spsinput" name="spsinput" accept=".sps, .xps" /> <input type="file" id="spsinput" name="spsinput" accept=".sps, .xps" />
</div> </div>
<div class="inputbox"> <div class="inputbox">
<label for="cbsinput">CodeBreaker Save export file (.cbs) goes here:</label> <label for="cbsinput">CodeBreaker Save export file (.cbs):</label>
<input type="file" id="cbsinput" name="cbsinput" accept=".cbs" /> <input type="file" id="cbsinput" name="cbsinput" accept=".cbs" />
</div> </div>
<div class="inputbox last-input">
<label for="maxinput">Max Drive/PowerSave export file (.max):</label>
<input type="file" id="maxinput" name="maxinput" accept=".max, .pws" />
</div>
<p> <p>
<span>Date&nbsp;created: </span><span id="dateCreated">--:--:--&nbsp;--/--/----</span><span> UTC+09:00</span> <span>Date&nbsp;created: </span><span id="dateCreated">--:--:--&nbsp;--/--/----</span><span> UTC+09:00</span>
<wbr><span>&ndash;</span> <wbr><span>&ndash;</span>
@ -167,8 +183,12 @@
<span>File comments: </span><span id="fileCommentGame">(no title)</span><span> - </span><span id="fileCommentName">(no description)</span><span> - </span><span id="fileCommentDesc">(no other text)</span> <span>File comments: </span><span id="fileCommentGame">(no title)</span><span> - </span><span id="fileCommentName">(no description)</span><span> - </span><span id="fileCommentDesc">(no other text)</span>
</p> </p>
<script> <script>
// I usually don't do in-body <script>'s, but I didn't want to do an await onload() again // I usually don't do in-body <script>'s, but I didn't want to do an awaited onload() again
const GlobalState = {rotations: 2, dataLength: 0, uniforms: {rotation: null, scale: null}, iconState: {source: null, currentIcon: null, currentSubmodel: 0, cachedIconSys: null}, fileReader: (new FileReader)}; const GlobalState = {"__proto__": null, rotations: 2, scale: 0, interpfactor: 0, dataLength: 0,
uniforms: {"__proto__": null, rotation: null, scale: null},
iconState: {"__proto__": null, source: null, currentIcon: null, currentSubmodel: 0, cachedIconSys: null},
fileReader: (new FileReader)
};
// I don't care HOW disgusting doing this is, I'm sick of pressing escape to clear these. // I don't care HOW disgusting doing this is, I'm sick of pressing escape to clear these.
let allInputs = document.querySelectorAll('input[type="file"]'); let allInputs = document.querySelectorAll('input[type="file"]');
Array.from(allInputs).forEach( Array.from(allInputs).forEach(
@ -179,10 +199,15 @@
} }
); );
function p0in(input) { // "prefix 0 if needed" function p0in(input) { // "prefix 0 if needed"
if(typeof input !== "string") {input = input.toString()};
return ((input.length>=2) ? input : `0${input}`); return ((input.length>=2) ? input : `0${input}`);
}; };
function timeToString(t64leOutput) {
return `${p0in(t64leOutput.hours)}:${p0in(t64leOutput.minutes)}:${p0in(t64leOutput.seconds)} ${p0in(t64leOutput.day)}/${p0in(t64leOutput.month)}/${t64leOutput.year}`
}
// rotation stuff // rotation stuff
const rotationDensity = 60; var rotationDensity = 60;
var interpolationRate = 0.34;
document.body.onkeydown = function(ev) { document.body.onkeydown = function(ev) {
if(glBgContext === null || GlobalState.iconState.currentIcon === null) {return;} if(glBgContext === null || GlobalState.iconState.currentIcon === null) {return;}
if(typeof GlobalState.uniforms.rotation !== "undefined") { if(typeof GlobalState.uniforms.rotation !== "undefined") {
@ -212,6 +237,7 @@
if(GlobalState.iconState.currentSubmodel < 0) { if(GlobalState.iconState.currentSubmodel < 0) {
GlobalState.iconState.currentSubmodel = GlobalState.iconState.currentIcon.numberOfShapes - 1; GlobalState.iconState.currentSubmodel = GlobalState.iconState.currentIcon.numberOfShapes - 1;
} }
GlobalState.interpfactor = 1.0;
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false); renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
break; break;
} }
@ -220,6 +246,7 @@
if(GlobalState.iconState.currentSubmodel > (GlobalState.iconState.currentIcon.numberOfShapes - 1)) { if(GlobalState.iconState.currentSubmodel > (GlobalState.iconState.currentIcon.numberOfShapes - 1)) {
GlobalState.iconState.currentSubmodel = 0; GlobalState.iconState.currentSubmodel = 0;
} }
GlobalState.interpfactor = 0.0;
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false); renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
break; break;
} }
@ -241,12 +268,41 @@
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false); renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
break; break;
} }
case "Comma": {
GlobalState.interpfactor -= interpolationRate;
// if we're at the lowest interp, move onto previous shape
if(GlobalState.interpfactor <= 0.0) {
GlobalState.iconState.currentSubmodel--;
if(GlobalState.iconState.currentSubmodel < 0) {
GlobalState.iconState.currentSubmodel = GlobalState.iconState.currentIcon.numberOfShapes - 1;
}
GlobalState.interpfactor = 1.0;
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
break;
}
break;;
}
case "Period": {
GlobalState.interpfactor += interpolationRate;
// if we're at the highest interp, move onto next shape
if(GlobalState.interpfactor >= 1.0) {
GlobalState.iconState.currentSubmodel++;
if(GlobalState.iconState.currentSubmodel > (GlobalState.iconState.currentIcon.numberOfShapes - 1)) {
GlobalState.iconState.currentSubmodel = 0;
}
GlobalState.interpfactor = 0.0;
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
break;
}
break;
}
default: { default: {
return; return;
} }
}; };
glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale); glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale);
glFgContext.uniform1f(GlobalState.uniforms.rotation, (GlobalState.rotations/rotationDensity)); glFgContext.uniform1f(GlobalState.uniforms.rotation, (GlobalState.rotations/rotationDensity));
glFgContext.uniform1f(GlobalState.uniforms.interpolation, GlobalState.interpfactor);
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength); glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
} else {return;} } else {return;}
} }
@ -280,7 +336,7 @@
} }
} }
function resetDisplay() { function resetDisplay() {
//reset displayed elements // reset displayed elements
document.getElementById("title1").textContent = "\uff0d"; document.getElementById("title1").textContent = "\uff0d";
document.getElementById("title2").textContent = "\uff0d"; document.getElementById("title2").textContent = "\uff0d";
document.getElementById("iconn").textContent = "?"; document.getElementById("iconn").textContent = "?";
@ -291,7 +347,7 @@
document.getElementById("fileCommentGame").textContent = "(no title)"; document.getElementById("fileCommentGame").textContent = "(no title)";
document.getElementById("fileCommentName").textContent = "(no description)"; document.getElementById("fileCommentName").textContent = "(no description)";
document.getElementById("fileCommentDesc").textContent = "(no other text)"; document.getElementById("fileCommentDesc").textContent = "(no other text)";
//reset globalstate parameters // reset globalstate parameters
GlobalState.iconState.cachedIconSys = null; GlobalState.iconState.cachedIconSys = null;
GlobalState.iconState.currentIcon = null; GlobalState.iconState.currentIcon = null;
GlobalState.iconState.source = null; GlobalState.iconState.source = null;
@ -300,19 +356,21 @@
GlobalState.dataLength = 0; GlobalState.dataLength = 0;
GlobalState.rotations = 2; GlobalState.rotations = 2;
GlobalState.iconState.currentSubmodel = 0; GlobalState.iconState.currentSubmodel = 0;
//clear buffers // clear buffers
if(glFgContext !== null) {
glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT); glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);
} }
}
function renderIcon(iconData, fileMetadata = null, clearData = true) { function renderIcon(iconData, fileMetadata = null, clearData = true) {
if(fileMetadata === null) { if(fileMetadata === null) {
fileMetadata = { fileMetadata = {
"lighting": { "lighting": {
"points": [{x:0,y:0,z:0},{x:0,y:0,z:0},{x:0,y:0,z:0}], "points": [{x:0,y:0,z:0},{x:0,y:0,z:0},{x:0,y:0,z:0}],
"colors": [ "colors": [
{r:1,g:1,b:1,a:1}, //ambient {r:1,g:1,b:1,a:1}, // ambient
{r:1,g:0,b:0,a:1}, //p[0] {r:1,g:0,b:0,a:1}, // point0
{r:0,g:1,b:0,a:1}, //p[1] {r:0,g:1,b:0,a:1}, // point1
{r:0,g:0,b:1,a:1} //p[2] {r:0,g:0,b:1,a:1} // point2
] ]
} }
}; };
@ -341,37 +399,37 @@
} }
let iconProgram = createProgram(glFgContext, iconVertexShader, iconFragmentShader); let iconProgram = createProgram(glFgContext, iconVertexShader, iconFragmentShader);
glFgContext.useProgram(iconProgram); glFgContext.useProgram(iconProgram);
var attributes = {
color: glFgContext.getAttribLocation(iconProgram, "a_color"),
position: glFgContext.getAttribLocation(iconProgram, "a_position"),
nextPosition: glFgContext.getAttribLocation(iconProgram, "a_nextPosition")
};
var uniforms = {
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight"),
scale: glFgContext.getUniformLocation(iconProgram, "u_scale"),
interpolation: glFgContext.getUniformLocation(iconProgram, "u_interp"),
rotation: glFgContext.getUniformLocation(iconProgram, "u_rotation")
}
if(iconData.textureFormat !== "N") { if(iconData.textureFormat !== "N") {
var attributes = { attributes["textureCoords"] = glFgContext.getAttribLocation(iconProgram, "a_textureCoords");
position: glFgContext.getAttribLocation(iconProgram, "a_position"), uniforms["sampler"] = glFgContext.getUniformLocation(iconProgram, "u_sampler");
textureCoords: glFgContext.getAttribLocation(iconProgram, "a_textureCoords"),
color: glFgContext.getAttribLocation(iconProgram, "a_color"),
};
var uniforms = {
rotation: glFgContext.getUniformLocation(iconProgram, "u_rotation"),
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight"),
scale: glFgContext.getUniformLocation(iconProgram, "u_scale")
}
} else {
var attributes = {
position: glFgContext.getAttribLocation(iconProgram, "a_position"),
color: glFgContext.getAttribLocation(iconProgram, "a_color"),
};
var uniforms = {
sampler: glFgContext.getUniformLocation(iconProgram, "u_sampler"),
rotation: glFgContext.getUniformLocation(iconProgram, "u_rotation"),
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight"),
scale: glFgContext.getUniformLocation(iconProgram, "u_scale")
}
} }
//.section SETUP //.section SETUP
let verticesArray = new Array(); let verticesArray = new Array();
let nextVerticesArray = new Array();
let colourArray = new Array(); let colourArray = new Array();
let uvArray = new Array(); let uvArray = new Array();
let nextShape = GlobalState.iconState.currentSubmodel + 1;
if(nextShape > (iconData.numberOfShapes - 1)) {
nextShape = 0;
}
iconData.vertices.forEach(function(vertexObject){ iconData.vertices.forEach(function(vertexObject){
verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].x); verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].x);
verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].y); verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].y);
verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].z); verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].z);
nextVerticesArray.push(vertexObject.shapes[nextShape].x);
nextVerticesArray.push(vertexObject.shapes[nextShape].y);
nextVerticesArray.push(vertexObject.shapes[nextShape].z);
colourArray.push(vertexObject.color.r/255); colourArray.push(vertexObject.color.r/255);
colourArray.push(vertexObject.color.g/255); colourArray.push(vertexObject.color.g/255);
colourArray.push(vertexObject.color.b/255); colourArray.push(vertexObject.color.b/255);
@ -379,12 +437,19 @@
uvArray.push(vertexObject.uv.u); uvArray.push(vertexObject.uv.u);
uvArray.push(vertexObject.uv.v); uvArray.push(vertexObject.uv.v);
}); });
// TODO: Might need normals too for lighting...
//.section VERTICES //.section VERTICES
const positionBuffer = glFgContext.createBuffer(); const positionBuffer = glFgContext.createBuffer();
glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, positionBuffer); glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, positionBuffer);
glFgContext.enableVertexAttribArray(attributes.position); glFgContext.enableVertexAttribArray(attributes.position);
glFgContext.bufferData(glFgContext.ARRAY_BUFFER, new Float32Array(verticesArray), glFgContext.STATIC_DRAW); glFgContext.bufferData(glFgContext.ARRAY_BUFFER, new Float32Array(verticesArray), glFgContext.STATIC_DRAW);
glFgContext.vertexAttribPointer(attributes.position, 3, glFgContext.FLOAT, false, 0, 0); glFgContext.vertexAttribPointer(attributes.position, 3, glFgContext.FLOAT, false, 0, 0);
//.section VERTICES_2
const nextPositionBuffer = glFgContext.createBuffer();
glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, nextPositionBuffer);
glFgContext.enableVertexAttribArray(attributes.nextPosition);
glFgContext.bufferData(glFgContext.ARRAY_BUFFER, new Float32Array(nextVerticesArray), glFgContext.STATIC_DRAW);
glFgContext.vertexAttribPointer(attributes.nextPosition, 3, glFgContext.FLOAT, false, 0, 0);
//.section COLOURS //.section COLOURS
const colorBuffer = glFgContext.createBuffer(); const colorBuffer = glFgContext.createBuffer();
glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, colorBuffer); glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, colorBuffer);
@ -407,21 +472,27 @@
// sets the angle uniform to 2/rotationDensity, this puts the icon at an angle. // sets the angle uniform to 2/rotationDensity, this puts the icon at an angle.
// globalize uniform rotation // globalize uniform rotation
GlobalState.uniforms.rotation = uniforms.rotation; GlobalState.uniforms.rotation = uniforms.rotation;
if(clearData){GlobalState.rotations = 2;} if (clearData) {GlobalState.rotations = 2;}
glFgContext.uniform1f(GlobalState.uniforms.rotation, GlobalState.rotations/rotationDensity); glFgContext.uniform1f(GlobalState.uniforms.rotation, GlobalState.rotations/rotationDensity);
//.section LIGHTING //.section LIGHTING
let colours = fileMetadata.lighting.colors[0]; let colours = fileMetadata.lighting.colors[0]; // get ambient lighting colours
//glFgContext.uniform3f(uniforms.ambientLighting, colours.r, colours.g, colours.b); // glFgContext.uniform3f(uniforms.ambientLighting, colours.r, colours.g, colours.b);
// TODO: figure out why rendering goes all sorts of bad when we use the actual values.
glFgContext.uniform3f(uniforms.ambientLighting, 0.75, 0.75, 0.75); glFgContext.uniform3f(uniforms.ambientLighting, 0.75, 0.75, 0.75);
//.section SCALING //.section SCALING
GlobalState.uniforms.scale = uniforms.scale; GlobalState.uniforms.scale = uniforms.scale;
if(clearData){GlobalState.scale = 3.5;} if (clearData) {GlobalState.scale = 3.5;}
glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale); glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale);
//.section INTERPOLATION
GlobalState.uniforms.interpolation = uniforms.interpolation;
if (clearData) {GlobalState.interpfactor = 0.0;}
glFgContext.uniform1f(GlobalState.uniforms.interpolation, GlobalState.interpfactor);
//.section WRITE //.section WRITE
//globalize count of triangles, as well // globalize count of triangles, as well
GlobalState.dataLength = (verticesArray.length/3); GlobalState.dataLength = (verticesArray.length/3);
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength); glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
} }
@ -493,9 +564,9 @@
renderIcon(output2.n, output); renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created; let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified; let mTime = vFilesystem.timestamps.modified;
//TODO: use Time() to align JST times to user-local timezone // TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = `${p0in(cTime.hours.toString())}:${p0in(cTime.minutes.toString())}:${p0in(cTime.seconds.toString())} ${p0in(cTime.day.toString())}/${p0in(cTime.month.toString())}/${cTime.year}`; document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`; document.getElementById("dateModified").textContent = timeToString(mTime);
console.info("model files (psu)", output2); console.info("model files (psu)", output2);
console.info("icon.sys (psu)", output); console.info("icon.sys (psu)", output);
} catch(e) { } catch(e) {
@ -531,9 +602,9 @@
renderIcon(icons.n, output); renderIcon(icons.n, output);
let cTime = inputData.timestamps.created; let cTime = inputData.timestamps.created;
let mTime = inputData.timestamps.modified; let mTime = inputData.timestamps.modified;
//TODO: use Time() to align JST times to user-local timezone // TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = `${p0in(cTime.hours.toString())}:${p0in(cTime.minutes.toString())}:${p0in(cTime.seconds.toString())} ${p0in(cTime.day.toString())}/${p0in(cTime.month.toString())}/${cTime.year}`; document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`; document.getElementById("dateModified").textContent = timeToString(mTime);
console.info("model files (psv)", icons); console.info("model files (psv)", icons);
console.info("icon.sys (psv)", output); console.info("icon.sys (psv)", output);
} catch(e) { } catch(e) {
@ -568,9 +639,9 @@
renderIcon(output2.n, output); renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created; let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified; let mTime = vFilesystem.timestamps.modified;
//TODO: use Time() to align JST times to user-local timezone // TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = `${p0in(cTime.hours.toString())}:${p0in(cTime.minutes.toString())}:${p0in(cTime.seconds.toString())} ${p0in(cTime.day.toString())}/${p0in(cTime.month.toString())}/${cTime.year}`; document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`; document.getElementById("dateModified").textContent = timeToString(mTime);
document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game; document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game;
document.getElementById("fileCommentName").textContent = vFilesystem.comments.name; document.getElementById("fileCommentName").textContent = vFilesystem.comments.name;
if(vFilesystem.comments.hasOwnProperty("desc")) { if(vFilesystem.comments.hasOwnProperty("desc")) {
@ -613,14 +684,49 @@
renderIcon(output2.n, output); renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created; let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified; let mTime = vFilesystem.timestamps.modified;
//TODO: use Time() to align JST times to user-local timezone // TODO: use Time() to align JST times to user-local timezone
if(cTime.year === 0) { if(cTime.year === 0) {
// if root directory time is null, read icon.sys instead // if root directory time is null, read icon.sys instead
cTime = vFilesystem[vFilesystem.rootDirectory]["icon.sys"].timestamps.created; cTime = vFilesystem[vFilesystem.rootDirectory]["icon.sys"].timestamps.created;
mTime = vFilesystem[vFilesystem.rootDirectory]["icon.sys"].timestamps.modified; mTime = vFilesystem[vFilesystem.rootDirectory]["icon.sys"].timestamps.modified;
} }
document.getElementById("dateCreated").textContent = `${p0in(cTime.hours.toString())}:${p0in(cTime.minutes.toString())}:${p0in(cTime.seconds.toString())} ${p0in(cTime.day.toString())}/${p0in(cTime.month.toString())}/${cTime.year.toString()}`; document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year.toString()}`; document.getElementById("dateModified").textContent = timeToString(mTime);
console.info("model files (cbs)", output2);
console.info("icon.sys (cbs)", output);
} catch(e) {
if(glBgContext!==null){glBgContext.clear(glBgContext.COLOR_BUFFER_BIT);}
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
console.error(e);
alert(e);
}
}
}
pwsbox = document.getElementById("maxinput");
pwsbox.onchange = function(e) {
resetDisplay();
if(pwsbox.files.length === 0) {
return;
}
function decompressor(data) {
return (decodeLzari(data)).buffer;
}
GlobalState.fileReader.readAsArrayBuffer(pwsbox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let vFilesystem = readMaxPwsFile(GlobalState.fileReader.result, decompressor);
let output = readPS2D(vFilesystem[vFilesystem.rootDirectory]["icon.sys"].data);
updateDisplay(output);
let output2 = new Object();
Object.keys(output.filenames).forEach(function(file) {
output2[file] = readIconFile(vFilesystem[vFilesystem.rootDirectory][output.filenames[file]].data);
});
GlobalState.iconState.cachedIconSys = output;
GlobalState.iconState.currentSubmodel = 0;
GlobalState.iconState.source = output2;
GlobalState.iconState.currentIcon = output2.n;
renderIcon(output2.n, output);
console.info("model files (cbs)", output2); console.info("model files (cbs)", output2);
console.info("icon.sys (cbs)", output); console.info("icon.sys (cbs)", output);
} catch(e) { } catch(e) {
@ -707,12 +813,15 @@
if (typeof pako === "undefined") { if (typeof pako === "undefined") {
document.getElementById("cbsinput").disabled = true; document.getElementById("cbsinput").disabled = true;
} }
//todo: More than one model shape rendering, other 2 icons (technically done? NMW though), Animation parsing, animation tweening if (typeof decodeLzari === "undefined") {
document.getElementById("maxinput").disabled = true;
}
// TODO: More than one model shape rendering, other 2 icons (technically done? NMW though), Animation parsing, animation tweening (technically too)
</script> </script>
<span id="version">icondumper2 <a href="./documentation" id="iconjsVersion">(unknown icon.js version)</a> [C: <span id="clientVersion">Loading...</span>] &mdash; &copy; <span id="currentYear">2023</span> yellows111</span> <span id="version">icondumper2 <a href="./documentation" id="iconjsVersion">(unknown icon.js version)</a> [C: <span id="clientVersion">Loading...</span>] &mdash; &copy; <span id="currentYear">2023</span> yellows111</span>
<script> <script>
document.getElementById("iconjsVersion").textContent = exports.version; document.getElementById("iconjsVersion").textContent = exports.version;
document.getElementById("clientVersion").textContent = "0.7.0"; document.getElementById("clientVersion").textContent = "0.8.1";
document.getElementById("currentYear").textContent = (new Date()).getFullYear().toString(); document.getElementById("currentYear").textContent = (new Date()).getFullYear().toString();
</script> </script>
</body> </body>

View File

@ -62,6 +62,22 @@ switch(processObj.argv[2]) {
console.log(output); console.log(output);
break; break;
} }
case "max":
case "pws": {
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.max");
function myUnlzari(inputBuffer) {
return (require("./lzari.js").decodeLzari(inputBuffer)).buffer;
}
const parsed = iconjs.readMaxPwsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength), myUnlzari);
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": { case "sys": {
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.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)); const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
@ -70,7 +86,6 @@ switch(processObj.argv[2]) {
Object.keys(metadata.filenames).forEach(function(file) { Object.keys(metadata.filenames).forEach(function(file) {
let getFile = filesystem.readFileSync(metadata.filenames[file]); let getFile = filesystem.readFileSync(metadata.filenames[file]);
const output = iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)); 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"); console.log(`contents of ${metadata.filenames[file]} (${file}):`, output, "\n");
}); });
} }
@ -91,6 +106,8 @@ psv: Read a PS3 export file.
sps: Read a SharkPort export file. sps: Read a SharkPort export file.
xps: Read a X-Port export file. xps: Read a X-Port export file.
cbs: Read a CodeBreaker Save export file. cbs: Read a CodeBreaker Save export file.
max: Read a Max Drive export file.
pws: Read a PowerSave export file.
sys: Read a icon.sys (964 bytes) file, and attempt sys: Read a icon.sys (964 bytes) file, and attempt
to read icon files from the current directory. to read icon files from the current directory.

261
lzari.js Normal file
View File

@ -0,0 +1,261 @@
// based on Luigi Auriemma's memory2memory LZARI modification. LZARI was created by Haruhiko Okumura.
// yellows111 modifications: forced all magic constants to be their actual values,
// static calculations have been squashed, some functions have been transcluded into others
/** @copyright MIT license:
* Based on a work by Haruhiko Okumura dated 1989-07-04.
* Copyright 2023 yellows111
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
**/
/* jshint bitwise: false */ // not doing this makes linters scream about BWOs
//TODO: privatize variables and document the library
var inputData = null;
var inputLocation = 0;
var low = 0;
var high = 131072;
var value = 0;
var text_buffer = new Array(4155);
var characterToSymbol = new Array(314);
var symbolToCharacter = new Array(315);
var symbolFrequency = new Array(315);
var symbolCumulative = new Array(315);
var positionCumulative = new Array(4097);
var bit_Buffer = 0;
var bit_Mask = 0;
function GetBit() {
//partial xgetc modification
if(inputLocation >= inputData.length) {return -1;}
if((bit_Mask >>= 1) === 0) {
bit_Buffer = inputData[inputLocation++];
bit_Mask = 128;
}
return +((bit_Buffer & bit_Mask) !== 0);
}
function BinarySearchSym(x) {
var i = 1;
var j = 314;
while (i < j) {
var k = ((i + j) / 2)|0;
if (symbolCumulative[k] > x) {
i = k + 1;
} else {
j = k;
}
}
return i;
}
function BinarySearchPos(x) {
var i = 1;
var j = 4096;
while (i < j) {
var k = ((i + j) / 2)|0;
if (positionCumulative[k] > x) {
i = k + 1;
} else {
j = k;
}
}
return i - 1;
}
function DecodeChar() {
var range = high - low;
var sym = BinarySearchSym((((value - low + 1) * symbolCumulative[0] - 1) / range)|0);
high = low + ((range * symbolCumulative[sym - 1]) / symbolCumulative[0])|0;
low += ((range * symbolCumulative[sym ]) / symbolCumulative[0])|0;
for ( ; ; ) {
if (low >= 65536) {
value -= 65536;
low -= 65536;
high -= 65536;
} else if (low >= 32768 && high <= 98304) {
value -= 32768;
low -= 32768;
high -= 32768;
} else if (high > 65536) { break; }
low += low;
high += high;
value = 2 * value + GetBit();
}
//transcluded UpdateModel
var character = symbolToCharacter[sym];
// do not remove above, will be overwritten otherwise!
var i;
if(symbolCumulative[0] >= 32767) {
var chr = 0;
for (i = 314; i > 0; i--) {
symbolCumulative[i] = chr;
chr += (symbolFrequency[i] = (symbolFrequency[i] + 1) >> 1);
}
symbolCumulative[0] = chr;
}
for(i = sym; symbolFrequency[i] === symbolFrequency[i - 1]; i--) {}
if (i < sym) {
var ch_i = symbolToCharacter[i];
var ch_sym = symbolToCharacter[sym];
symbolToCharacter[i] = ch_sym;
symbolToCharacter[sym] = ch_i;
characterToSymbol[ch_i] = sym;
characterToSymbol[ch_sym] = i;
//i would change these vars...
//but it looks so darn cool...
}
symbolFrequency[i]++;
while (--i >= 0) {
symbolCumulative[i]++;
}
//end transclusion
return character;
}
function DecodePosition() {
var range = high - low;
var position = BinarySearchPos((((value - low + 1) * positionCumulative[0] - 1) / range)|0);
high = low + ((range * positionCumulative[position ]) / positionCumulative[0])|0;
low += ((range * positionCumulative[position + 1]) / positionCumulative[0])|0;
for ( ; ; ) {
if (low >= 65536) {
value -= 65536;
low -= 65536;
high -= 65536;
} else if (low >= 32768 && high <= 98304) {
value -= 32768;
low -= 32768;
high -= 32768;
} else if (high > 65536) { break; }
low += low;
high += high;
value = 2 * value + GetBit();
}
return position;
}
/**
* Decompresses LZARI-formatted data.
* @param {Uint8Array} input - source data
* @returns {Uint8Array} output - uncompressed data
* @access public
*/
function decodeLzari(input) {
//transcluded reset function.
inputData = null;
inputLocation = 0;
low = 0;
high = 131072;
value = 0;
text_buffer = new Array(4155);
characterToSymbol = new Array(314);
symbolToCharacter = new Array(315);
symbolFrequency = new Array(315);
symbolCumulative = new Array(315);
positionCumulative = new Array(4097);
bit_Buffer = 0;
bit_Mask = 0;
//end transclusion
inputData = input;
inputLocation = 4;
var dataSize = new DataView(input.buffer).getInt32(0,1);
if (dataSize == 0) return(0);
if (dataSize < 0) return(-1);
var outputData = new Uint8Array(dataSize);
var outputLocation = 0;
//transcluded StartDecode
for (var i = 0; i < 17; i++) {
value = 2 * value + GetBit();
}
//transcluded StartModel
symbolCumulative[314] = 0;
for (var sym = 314; sym >= 1; sym--) {
var ch = sym - 1;
characterToSymbol[ch] = sym;
symbolToCharacter[sym] = ch;
symbolFrequency[sym] = 1;
symbolCumulative[sym - 1] = (symbolCumulative[sym] + symbolFrequency[sym]);
}
symbolFrequency[0] = 0;
positionCumulative[4096] = 0;
for (i = 4096; i >= 1; i--) { // redefine i
positionCumulative[i - 1] = (positionCumulative[i] + (10000 / (i + 200))|0);
}
//end transclusion
//normal Decode process
for (i = 0; i < 4036; i++) { // redefine i
text_buffer[i] = 32;
}
var r = 4036;
for (var count = 0; count < dataSize; ) {
if(inputLocation >= inputData.length) {break;}
var c = DecodeChar();
if (c < 256) {
outputData[outputLocation++] = c;
text_buffer[r++] = c;
r &= (4095);
count++;
} else {
i = (r - DecodePosition() - 1) & 4095; // redefine i
var j = c - 253;
for (var k = 0; k < j; k++) {
c = text_buffer[(i + k) & 4095];
outputData[outputLocation++] = c;
text_buffer[r++] = c;
r &= (4095);
count++;
}
}
}
//console.debug("LZARI I/O", {inputData, outputData});
return(outputData);
}
/**
* Define (module.)exports with all public functions.
* @exports icondumper2/lzari
*/ // start c6js
/* globals exports: true */
if(typeof exports !== "object") {
exports = {
"decodeLzari": decodeLzari
};
} else {
exports.decodeLzari = decodeLzari;
}
/* globals module: true */
if(typeof module !== "undefined") {
module.exports = exports;
}
//end c6js
//start esm
/*export {
decodeLzari
};*/
//end esm

369
tests/iconwriter.js Normal file
View File

@ -0,0 +1,369 @@
// version 1.0.0 icon, 1 shape, texture type 7 (will be discarded), something 1.0f, 36 vertices (cube)
const iconHeader = new Uint8Array([
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F,
0x24, 0x00, 0x00, 0x00
]); // 20 bytes
// format: [xxyyzzaa][xxyyzzaa][uuvv][rgba], (8 + 8 + 4 + 4) (position.xyzw, normal.xyzw, texcoords.st, colour.rgba)
// 0x1000 = 4096 (1.0f), 0xf000 = -4096 (-1.0f)
// color is lime green (64, 255, 64, 127)
const iconData = new Uint8Array([
// 1/36, [-1, -1, -1], [0.25, 0.25] poly 1.1 z-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
// 2/36, [-1, 1, -1], [0.25, 0.75] poly 1.2 z-1
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
// 3/36, [ 1, -1, -1], [0.75, 0.75] poly 1.3 z-1
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
// 4/36, [ 1, 1, -1] poly 2.1 z-1
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
// 5/36, [ 1, -1, -1] poly 2.2 z-1
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
// 6/36, [-1, 1, -1] poly 2.3 z-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
// 7/36, [-1, -1, -1] poly 3.1 x-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
// 8/36, [-1, -1, 1] poly 3.2 x-1
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
// 9/36, [-1, 1, -1] poly 3.3 x-1
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//10/36, [-1, -1, -1] poly 4.1 x-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//11/36, [-1, -1, 1] poly 4.2 x-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//12/36, [-1, 1, 1] poly 4.3 x-1
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//13/36, [-1, 1, -1] poly 5.1 y+1
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//14/36, [-1, 1, 1] poly 5.2 y+1
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//15/36, [ 1, 1, -1] poly 5.3 y+1
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//16/36, [ 1, 1, -1] poly 6.1 y+1
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//17/36, [-1, 1, 1] poly 6.2 y+1
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//18/36, [ 1, 1, 1] poly 6.3 y+1
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//19/36, [ 1, 1, -1] poly 7.1 x+1
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//20/36, [ 1, 1, 1] poly 7.2 x+1
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//21/36, [ 1, -1, -1] poly 7.3 x+1
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//22/36, [ 1, 1, 1] poly 8.1 x+1
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//23/36, [ 1, -1, 1] poly 8.2 x+1
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//24/36, [ 1, -1, -1] poly 8.3 x+1
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//25/36, [-1, -1, -1] poly 9.1 y-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//26/36, [ 1, -1, -1] poly 9.2 y-1
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//27/36, [-1, -1, 1] poly 9.3 y-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//28/36, [-1, -1, 1] poly 10.1 y-1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//29/36, [ 1, -1, -1] poly 10.2 y-1
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//30/36, [ 1, -1, 1] poly 10.3 y-1
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//31/36, [-1, 1, 1] poly 11.1 z+1
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//32/36, [-1, -1, 1] poly 11.2 z+1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//33/36, [ 1, 1, 1] poly 11.3 z+1
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//35/36, [ 1, -1, 1] poly 12.2 z+1
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
//34/36, [ 1, 1, 1] poly 12.1 z+1
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x40, 0xff, 0x40, 0x7f,
//36/36, [-1, -1, 1] poly 12.3 z+1
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0xf0, 0x00, 0xf0, 0x00, 0x10, 0x00, 0x04,
0x00, 0x0c, 0x00, 0x0c, 0x40, 0xff, 0x40, 0x7f,
]); // 864 bytes
const animData = new Uint8Array([
0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
]); // 36 bytes
const texture_ = new Uint16Array(16384);
/** for compressed textures (RLE: fill with blue) **/
texture_[0] = 0x0004;
texture_[1] = 0x0000;
texture_[2] = 0x4000;
texture_[3] = 0b1_11111_00000_00000; // 16 bytes
/** generate texture data (RAW: fill with red) **/
for (let indice = 4; indice < 16384; indice++) {
texture_[indice] = 0b1_00000_00000_11111; //A1BGR5
} // 32768 bytes
const textureData = new Uint8Array(texture_.buffer);
/** ps2d data **/
const metadataSkeleton = new Uint8Array(Array.from({...[
0x50, 0x53, 0x32, 0x44, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, // PS2D files have a max limit of 16 characters per line
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
// color1.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// color2.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// color3.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// color4.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// pos1.xyzw
0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f,
0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00,
// pos2.xyzw
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// pos3.xyzw
0x00, 0x00, 0x80, 0xbf, 0x00, 0x00, 0x80, 0xbf,
0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00,
// light1.rgba
0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x80, 0x3f,
0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00,
// light2.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// light3.rgba
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// amblight.rgba
0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x80, 0x3f,
0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00,
// title
0x82, 0x89, 0x82, 0x83, 0x82, 0x8f, 0x82, 0x8e, // icon
0x82, 0x84, 0x82, 0x95, 0x82, 0x8d, 0x82, 0x90, // dump
0x82, 0x85, 0x82, 0x92, 0x82, 0x51, 0x81, 0x40, // er2
0x82, 0x73, 0x82, 0x85, 0x82, 0x93, 0x82, 0x94, // Test
0x82, 0x73, 0x82, 0x85, 0x82, 0x98, 0x82, 0x94, // Text
0x82, 0x95, 0x82, 0x92, 0x82, 0x85, 0x81, 0x40, // ure
0x82, 0x73, 0x82, 0x99, 0x82, 0x90, 0x82, 0x85, // Type
0x81, 0x40, 0x82, 0x58, // 9
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// file - normal
0x74, 0x65, 0x78, 0x74, 0x79, 0x70, 0x65, 0x31,
0x2E, 0x69, 0x63, 0x6E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// file - copying
0x74, 0x65, 0x78, 0x74, 0x79, 0x70, 0x65, 0x31,
0x2E, 0x69, 0x63, 0x6E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// file - deleting
0x74, 0x65, 0x78, 0x74, 0x79, 0x70, 0x65, 0x31,
0x2E, 0x69, 0x63, 0x6E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
//], length:964}));
], length:1024})); // psu want block size
const CombinedIconData = new Uint8Array(33792/*20+864+36+32768*/); // psu still want block size
CombinedIconData.set(iconHeader, 0);
CombinedIconData.set(iconData, 20);
CombinedIconData.set(animData, 20+864);
CombinedIconData.set(textureData, 20+864+36);
// to be honest all of this could just be a generator
/** root directory **/
const psuHeader1 = new Uint8Array(Array.from({...[
0x27, 0x84, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x00, 0x16, 0x25, 0x12, 0x02, 0x05, 0xd2, 0x07,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0xd2, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x69, 0x64, 0x32, 0x5f, 0x74, 0x65, 0x78, 0x74,
0x65, 0x73, 0x74, 0x39
], length:512}));
/** . **/
const psuHeader2 = new Uint8Array(Array.from({...[
0x27, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x25, 0x12, 0x02, 0x05, 0xd2, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0xd2, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2e
], length:512}));
/** .. **/
const psuHeader3 = new Uint8Array(Array.from({...[
0x27, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x25, 0x12, 0x02, 0x05, 0xd2, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0xd2, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2e, 0x2e
], length:512}));
/** icon.sys **/
const psuHeader4 = new Uint8Array(Array.from({...[
0x17, 0x84, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00,
0x00, 0x16, 0x25, 0x12, 0x02, 0x05, 0xd2, 0x07,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0xd2, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x69, 0x63, 0x6f, 0x6e, 0x2e, 0x73, 0x79, 0x73
], length:512}));
/** textypeX.icn **/
const psuHeader5 = new Uint8Array(Array.from({...[
0x17, 0x84, 0x00, 0x00, 0x98, 0x83, 0x00, 0x00,
0x00, 0x16, 0x25, 0x12, 0x02, 0x05, 0xd2, 0x07,
0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0xd2, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x74, 0x65, 0x78, 0x74, 0x79, 0x70, 0x65, 0x39,
0x2E, 0x69, 0x63, 0x6E
], length:512}));
/** write psus **/
for (let iconIndice = 0; iconIndice < 32; iconIndice++) { // realistically only needs to be 0-15, not 0-31.
let needsAlpha = 0;
if(iconIndice > 9) {
needsAlpha = 7; // if we're past 9, offset to start at A instead
}
const PsuFileOutput = new Uint8Array(37888);// 37 uncompressed blocks
CombinedIconData[8] = iconIndice; // set texture type
psuHeader1[75] = 0x30 + iconIndice+needsAlpha; // set folder name
psuHeader5[71] = 0x30 + iconIndice+needsAlpha; // set file name
metadataSkeleton[251] = 0x4f + iconIndice+needsAlpha; // set display name
metadataSkeleton[267] = 0x30 + iconIndice+needsAlpha; // set normal file name
metadataSkeleton[331] = 0x30 + iconIndice+needsAlpha; // set copy file name
metadataSkeleton[395] = 0x30 + iconIndice+needsAlpha; // set deleting file name
PsuFileOutput.set(psuHeader1,0);
PsuFileOutput.set(psuHeader2,512);
PsuFileOutput.set(psuHeader3,1024);
PsuFileOutput.set(psuHeader4,1536);
PsuFileOutput.set(metadataSkeleton,2048);
PsuFileOutput.set(psuHeader5,3072);
PsuFileOutput.set(CombinedIconData, 3584);
// and then 512 bytes of padding since we don't reach a block yet
if(typeof require !== "undefined") {
require("fs").writeFileSync(`${iconIndice.toString(36)}_file.psu`, PsuFileOutput);
}
}