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
uses: actions/setup-node@main
- 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
uses: actions/upload-artifact@main
with:

2
.gitignore vendored
View File

@ -13,6 +13,8 @@ icon.sys
*.p2m
*.md
*.cbs
*.max
*.pws
# 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?
Current implementations had some issues with rendering some icons. These were mostly:
* Not rendering any color for texture type 3.
* Failing to decompress some specific RLE-compressed icons. (types above 8)
* Not rendering any color for texture types 0-3.
* Failing to decompress some specific RLE-compressed icons. (types with bit 4 enabled)
* 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.
@ -20,6 +22,7 @@ As of writing, there was no exporter that exists for the format that exhibited o
* SharkPort export files (.sps)
* X-Port export files (.xps)
* CodeBreaker Save export files (.cbs)
* Max Drive "PowerSave"/export files (.max)
* PS2 icons (.ico, .icn)
* 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.
* 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.
* Decompress any LZARI-formatted data.
## What it doesn't do:
* 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.
## 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:
* 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:
| File | Description |
| ---------------- | ----------------------------------------- |
| ------------------- | ----------------------------------------------- |
| icon.js | The library itself. |
| index.js | Node.js example client. |
| gltf-exporter.js | Node.js client to export icons to glTF 2. |
| 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:
| 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?
gltfOutput.materials = [{
"name": `Material (${filename}#${index})`,
"pbrMetallicRoughness": {
"baseColorTexture": {"index":0, "texCoord": 0}
},
"pbrMetallicRoughness": null,
"extensions": { // or we get annoying PBR and specular stuff we don't need
"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.bufferViews = [
{
@ -173,16 +182,14 @@ function imf2gltf(icon = null, filename = "untitled") {
];
gltfOutput.asset = {"version": "2.0", "generator": `icondumper2/${icondumper2.version}`}
gltfOutput.extensionsUsed = ["KHR_materials_unlit"];
if(icon.textureFormat !== "N") {
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;
@ -192,6 +199,7 @@ function imf2gltf(icon = null, filename = "untitled") {
break;
}
}
if(texture16 !== null) {
let texture24 = new Uint8Array(49983);
texture24.set([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
@ -243,6 +251,9 @@ function imf2gltf(icon = null, filename = "untitled") {
0xae, 0x42, 0x60, 0x82
], textureOffset);
return {objects: gltfOutputArray, buffer: outputFloatArray, texture: texture24};
} else {
return {objects: gltfOutputArray, buffer: outputFloatArray, texture: null};
}
}
function loadAndConvertIcon(inputData, attemptedFilename = "-") {
@ -257,9 +268,10 @@ function loadAndConvertIcon(inputData, attemptedFilename = "-") {
}
(require("fs")).writeFileSync(`${filename}.bin`, glTF_output.buffer);
console.info(`Saved glTF buffer as "${filename}.bin".`);
if(glTF_output.texture !== null) {
(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)
@ -322,6 +334,23 @@ switch(processObj.argv[2]) {
}
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": {
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));
@ -352,6 +381,8 @@ psv: Read a PS3 export file.
sps: Read a SharkPort export file.
xps: Read a X-Port 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
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.
//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_STRICT = true;
@ -8,7 +8,7 @@ var ICONJS_STRICT = true;
* @constant {string}
* @default
*/
const ICONJS_VERSION = "0.7.0";
const ICONJS_VERSION = "0.8.4";
/**
* The RC4 key used for ciphering CodeBreaker Saves.
@ -63,22 +63,22 @@ class yellowDataReader extends DataView {
* @param {number} i Indice offset.
* @returns {number}
*/
u16le(i){return super.getUint16(i, 1)};
u16le(i){return super.getUint16(i, 1);}
/** Fixed-point 16-bit, Little Endian.
* @param {number} i Indice offset.
* @returns {number}
*/
f16le(i){return (super.getInt16(i, 1) / 4096)};
f16le(i){return (super.getInt16(i, 1) / 4096);}
/** Unsigned 32-bit, Little Endian.
* @param {number} i Indice offset.
* @returns {number}
*/
u32le(i){return super.getUint32(i, 1)};
u32le(i){return super.getUint32(i, 1);}
/** Floating-point 32-bit, Little Endian.
* @param {number} i Indice offset.
* @returns {number}
*/
f32le(i){return super.getFloat32(i, 1)};
f32le(i){return super.getFloat32(i, 1);}
/** 64-bit Timestamp structure, Little Endian.
* Time returned is set for JST (UTC+09:00) instead of UTC.
* Time returned is going to be offseted for JST (GMT+09:00).
@ -92,13 +92,14 @@ class yellowDataReader extends DataView {
* @property {number} year - Year.
*/
t64le(i){return {
"__proto__": null,
seconds: super.getUint8(i+1),
minutes: super.getUint8(i+2),
hours: super.getUint8(i+3),
day: super.getUint8(i+4),
month: super.getUint8(i+5),
year: super.getUint16(i+6, 1)
}};
};}
constructor(buffer) {
super(buffer);
return {
@ -107,7 +108,7 @@ class yellowDataReader extends DataView {
u32le: this.u32le.bind(this),
f32le: this.f32le.bind(this),
t64le: this.t64le.bind(this)
}
};
}
}
@ -152,6 +153,7 @@ function setDebug(value) {
* @public
*/
function setStrictness(value) {
console.info("setStrictness is deprecated!");
ICONJS_STRICT = !!value;
}
@ -162,15 +164,18 @@ function setStrictness(value) {
* @access protected
*/
function getTextureFormat(i) {
if (i<8) {
if(i==3) {
return 'N';
if(ICONJS_DEBUG) {
console.debug("Texture format: %i", i);
}
return 'U';
} else if (i>=8) {
return 'C';
} else {
return void(0);
// bit 1: enable smooth shading (TODO)
// bit 2: ??? something weird with colours, check iconwriter.js output...
if(!!(i & 4)) { // if bit 3 (textured)...
if(!!(i & 8)) { // if bit 4 (compressed)...
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 offset = 0;
for (let index = 0; index < 16384;) {
currentValue = u16le(offset);
let 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
@ -197,7 +202,7 @@ function uncompressTexture(texData) {
currentValue = u16le(offset);
}
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
let length = ((0x10000 - currentValue));
for (let enumerator = 0; enumerator < length; enumerator++) {
@ -210,7 +215,7 @@ function uncompressTexture(texData) {
uncompressed[index] = u16le(offset);
for (let indey = 0; indey < currentValue; indey++) {
uncompressed[index] = u16le(offset);
index++
index++;
}
offset += 2;
}
@ -282,13 +287,13 @@ function readPS2D(input) {
{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);
@ -314,7 +319,7 @@ function readPS2D(input) {
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});
}
@ -336,7 +341,7 @@ function readIconFile(input) {
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.
@ -351,7 +356,7 @@ function readIconFile(input) {
const textureFormat = getTextureFormat(textureType);
//:skip 4
const numberOfVertexes = u32le(16);
if(!!(numberOfVertexes % 3)){
if((numberOfVertexes % 3) > 0){
throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`;
}
// format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk]
@ -386,7 +391,7 @@ function readIconFile(input) {
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)};
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();
// 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.
@ -433,7 +438,7 @@ function readIconFile(input) {
//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);
const size = u32le(offset);
texture = {size, data: input.slice(offset+4, offset+(4+size))};
}
}
@ -488,13 +493,13 @@ function readEntryBlock(input) {
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})`
throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})`;
}
let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps};
let output = new Object();
let fsOut = {"__proto__": null, length: header.size, rootDirectory: header.filename, timestamps: header.timestamps};
let output = {"__proto__": null};
let offset = 512;
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) {
case "directory": {
offset += 512;
@ -566,15 +571,21 @@ function readPsvFile(input){
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})
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};
}
@ -637,7 +648,7 @@ function readSharkXPortSxpsFile(input) {
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".`
throw `Unrecognized file identification string. Expected "SharkPortSave".`;
}
offset += (identLength + 4);
const titleLength = u32le(offset);
@ -657,19 +668,19 @@ function readSharkXPortSxpsFile(input) {
const comments = {
"game": stringScrubber((new TextDecoder("utf-8")).decode(title)),
"name": stringScrubber((new TextDecoder("utf-8")).decode(description))
}
};
if(description2Length !== 0) {
comments.desc = stringScrubber((new TextDecoder("utf-8")).decode(description2));
}
const totalSize = u32le(offset);
//const totalSize = u32le(offset); has data, unused in script
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();
let fsOut = {"__proto__": null, length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments};
let output = {"__proto__": null};
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) {
case "directory": {
offset += 250;
@ -703,7 +714,7 @@ function readSharkXPortSxpsFile(input) {
*/
function readCodeBreakerCbsDirectory(input) {
const {u32le, t64le} = new yellowDataReader(input);
const virtualFilesystem = new Object();
const virtualFilesystem = {"__proto__": null};
for (let offset = 0; offset < input.byteLength;) {
const timestamps = {created: t64le(offset), modified: t64le(offset+8)};
const dataSize = u32le(offset+16);
@ -728,7 +739,7 @@ function readCodeBreakerCbsDirectory(input) {
*/
function readCodeBreakerCbsFile(input, inflator = null) {
if(typeof inflator !== "function") {
throw `No inflator function passed. Skipping.`;
throw "No inflator function passed. Skipping.";
}
const {u32le, t64le} = new yellowDataReader(input);
const magic = u32le(0);
@ -754,28 +765,105 @@ function readCodeBreakerCbsFile(input, inflator = null) {
const compressedData = input.slice(dataOffset, dataOffset + compressedSize);
const decipheredData = rc4Cipher(ICONJS_CBS_RC4_KEY, new Uint8Array(compressedData));
const inflatedData = inflator(decipheredData);
const fsOut = {rootDirectory: dirName, timestamps};
const fsOut = {"__proto__": null, rootDirectory: dirName, timestamps};
fsOut[dirName] = readCodeBreakerCbsDirectory(inflatedData);
if(ICONJS_DEBUG) {
console.debug({magic, dataOffset, compressedSize, dirName, permissions, displayName});
}
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.
* @exports icondumper2/icon
*/ // start c6js
exports = {
readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile},
helpers: {uncompressTexture, convertBGR5A1toRGB5A1},
options: {setDebug, setStrictness},
*/ // start c6js#
/* globals exports: true */
if(typeof exports !== "object") {
exports = {
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
};
};
} 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") {
module.exports = exports;
}
//end c6js
//start esm
/*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

239
index.htm
View File

@ -4,10 +4,12 @@
<meta charset="utf-8"></meta>
<meta name="viewport" content="initial-scale=1.5"></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>
<!-- If you need pako to be optional, remove/comment the bottom line. This will disable support for CBS reading, however -->
<script src="https://cdn.jsdelivr.net/npm/pako/dist/pako_inflate.min.js" integrity="sha512-mlnC6JeOvg9V4vBpWMxGKscsCdScB6yvGVCeFF2plnQMRmwH69s9F8SHPbC0oirqfePmRBhqx2s3Bx7WIvHfWg==" crossorigin="anonymous" id="inflator-installed"></script>
<!-- Removing or commenting below will disable MAX reading... -->
<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>
html {color: #ccc; background: black; font-family: sans-serif}
#title1, #title2 {
@ -23,22 +25,31 @@
#version {text-shadow: 1px 1px 2px black;}
a {color: #ccc;}
.inputbox {
display: inline-grid;
display: table-cell;
margin-right: 0.25em;
border: 1px gray solid;
padding: 0.175em 0.25em 0 0.25em;
margin-bottom: 4px;
border-right: 0;
}
.last-input {
border-right: 1px gray solid;
}
.inputbox > input {
width: 100%;
}
</style>
<meta data-comment="WebGL Shader: Icon">
<script type="text/plain" id="shader-icon-v">
attribute vec3 a_position;
attribute vec3 a_nextPosition;
attribute vec3 a_normal;
attribute vec2 a_textureCoords;
attribute vec4 a_color;
uniform float u_rotation;
uniform float u_scale;
uniform float u_interp;
uniform highp vec3 u_ambientLight;
//uniform highp vec3 u_lightColorA;
//uniform highp vec3 u_lightColorB;
@ -50,11 +61,12 @@
void main() {
float angle = radians(360.0) * u_rotation;
vec2 pos = vec2(cos(angle), sin(angle));
vec3 lv_interp = mix(a_position, a_nextPosition, u_interp);
// x, y, z, scale (w)
gl_Position = vec4(
(a_position.x * pos.x) + (a_position.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
(a_position.x * -pos.y) + (a_position.z * pos.x), //transform the z position
(lv_interp.x * pos.x) + (lv_interp.z * pos.y), //transform the x position
(-lv_interp.y) - 2.75, // invert the y position and move down -2.75, which will center the model
(lv_interp.x * -pos.y) + (lv_interp.z * pos.x), //transform the z position
u_scale
);
// flip it, scale it
@ -125,7 +137,7 @@
<h1 id="title1">&#xFF2E;&#xFF4F;&#x3000;&#xFF26;&#xFF49;&#xFF4C;&#xFF45;</h1>
<h1 id="title2">&#xFF2C;&#xFF4F;&#xFF41;&#xFF44;&#xFF45;&#xFF44;</h1>
</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="iconcanvas" width="480" height="480"></canvas>
<hr>
@ -133,31 +145,35 @@
<div id="advanced">
<hr>
<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" />
</div>
<div class="inputbox">
<label for="icon">raw icon file goes here:</label>
<div class="inputbox last-input">
<label for="icon">raw icon file:</label>
<input type="file" id="icon" name="icon" accept=".icn, .ico" />
</div>
</div>
<hr>
<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" />
</div>
<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" />
</div>
<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" />
</div>
<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" />
</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>
<span>Date&nbsp;created: </span><span id="dateCreated">--:--:--&nbsp;--/--/----</span><span> UTC+09:00</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>
</p>
<script>
// I usually don't do in-body <script>'s, but I didn't want to do an await 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)};
// I usually don't do in-body <script>'s, but I didn't want to do an awaited onload() again
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.
let allInputs = document.querySelectorAll('input[type="file"]');
Array.from(allInputs).forEach(
@ -179,10 +199,15 @@
}
);
function p0in(input) { // "prefix 0 if needed"
if(typeof input !== "string") {input = input.toString()};
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
const rotationDensity = 60;
var rotationDensity = 60;
var interpolationRate = 0.34;
document.body.onkeydown = function(ev) {
if(glBgContext === null || GlobalState.iconState.currentIcon === null) {return;}
if(typeof GlobalState.uniforms.rotation !== "undefined") {
@ -212,6 +237,7 @@
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;
}
@ -220,6 +246,7 @@
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;
}
@ -241,12 +268,41 @@
renderIcon(GlobalState.iconState.currentIcon, GlobalState.iconState.cachedIconSys, false);
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: {
return;
}
};
glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale);
glFgContext.uniform1f(GlobalState.uniforms.rotation, (GlobalState.rotations/rotationDensity));
glFgContext.uniform1f(GlobalState.uniforms.interpolation, GlobalState.interpfactor);
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
} else {return;}
}
@ -280,7 +336,7 @@
}
}
function resetDisplay() {
//reset displayed elements
// reset displayed elements
document.getElementById("title1").textContent = "\uff0d";
document.getElementById("title2").textContent = "\uff0d";
document.getElementById("iconn").textContent = "?";
@ -291,7 +347,7 @@
document.getElementById("fileCommentGame").textContent = "(no title)";
document.getElementById("fileCommentName").textContent = "(no description)";
document.getElementById("fileCommentDesc").textContent = "(no other text)";
//reset globalstate parameters
// reset globalstate parameters
GlobalState.iconState.cachedIconSys = null;
GlobalState.iconState.currentIcon = null;
GlobalState.iconState.source = null;
@ -300,19 +356,21 @@
GlobalState.dataLength = 0;
GlobalState.rotations = 2;
GlobalState.iconState.currentSubmodel = 0;
//clear buffers
// clear buffers
if(glFgContext !== null) {
glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);
}
}
function renderIcon(iconData, fileMetadata = null, clearData = true) {
if(fileMetadata === null) {
fileMetadata = {
"lighting": {
"points": [{x:0,y:0,z:0},{x:0,y:0,z:0},{x:0,y:0,z:0}],
"colors": [
{r:1,g:1,b:1,a:1}, //ambient
{r:1,g:0,b:0,a:1}, //p[0]
{r:0,g:1,b:0,a:1}, //p[1]
{r:0,g:0,b:1,a:1} //p[2]
{r:1,g:1,b:1,a:1}, // ambient
{r:1,g:0,b:0,a:1}, // point0
{r:0,g:1,b:0,a:1}, // point1
{r:0,g:0,b:1,a:1} // point2
]
}
};
@ -341,37 +399,37 @@
}
let iconProgram = createProgram(glFgContext, iconVertexShader, iconFragmentShader);
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") {
var attributes = {
position: glFgContext.getAttribLocation(iconProgram, "a_position"),
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")
}
attributes["textureCoords"] = glFgContext.getAttribLocation(iconProgram, "a_textureCoords");
uniforms["sampler"] = glFgContext.getUniformLocation(iconProgram, "u_sampler");
}
//.section SETUP
let verticesArray = new Array();
let nextVerticesArray = new Array();
let colourArray = new Array();
let uvArray = new Array();
let nextShape = GlobalState.iconState.currentSubmodel + 1;
if(nextShape > (iconData.numberOfShapes - 1)) {
nextShape = 0;
}
iconData.vertices.forEach(function(vertexObject){
verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].x);
verticesArray.push(vertexObject.shapes[GlobalState.iconState.currentSubmodel].y);
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.g/255);
colourArray.push(vertexObject.color.b/255);
@ -379,12 +437,19 @@
uvArray.push(vertexObject.uv.u);
uvArray.push(vertexObject.uv.v);
});
// TODO: Might need normals too for lighting...
//.section VERTICES
const positionBuffer = glFgContext.createBuffer();
glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, positionBuffer);
glFgContext.enableVertexAttribArray(attributes.position);
glFgContext.bufferData(glFgContext.ARRAY_BUFFER, new Float32Array(verticesArray), glFgContext.STATIC_DRAW);
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
const colorBuffer = glFgContext.createBuffer();
glFgContext.bindBuffer(glFgContext.ARRAY_BUFFER, colorBuffer);
@ -407,21 +472,27 @@
// sets the angle uniform to 2/rotationDensity, this puts the icon at an angle.
// globalize uniform rotation
GlobalState.uniforms.rotation = uniforms.rotation;
if(clearData){GlobalState.rotations = 2;}
if (clearData) {GlobalState.rotations = 2;}
glFgContext.uniform1f(GlobalState.uniforms.rotation, GlobalState.rotations/rotationDensity);
//.section LIGHTING
let colours = fileMetadata.lighting.colors[0];
//glFgContext.uniform3f(uniforms.ambientLighting, colours.r, colours.g, colours.b);
let colours = fileMetadata.lighting.colors[0]; // get ambient lighting colours
// 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);
//.section SCALING
GlobalState.uniforms.scale = uniforms.scale;
if(clearData){GlobalState.scale = 3.5;}
if (clearData) {GlobalState.scale = 3.5;}
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
//globalize count of triangles, as well
// globalize count of triangles, as well
GlobalState.dataLength = (verticesArray.length/3);
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
}
@ -493,9 +564,9 @@
renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified;
//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("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`;
// TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = timeToString(mTime);
console.info("model files (psu)", output2);
console.info("icon.sys (psu)", output);
} catch(e) {
@ -531,9 +602,9 @@
renderIcon(icons.n, output);
let cTime = inputData.timestamps.created;
let mTime = inputData.timestamps.modified;
//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("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`;
// TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = timeToString(mTime);
console.info("model files (psv)", icons);
console.info("icon.sys (psv)", output);
} catch(e) {
@ -568,9 +639,9 @@
renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified;
//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("dateModified").textContent = `${p0in(mTime.hours.toString())}:${p0in(mTime.minutes.toString())}:${p0in(mTime.seconds.toString())} ${p0in(mTime.day.toString())}/${p0in(mTime.month.toString())}/${mTime.year}`;
// TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = timeToString(cTime);
document.getElementById("dateModified").textContent = timeToString(mTime);
document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game;
document.getElementById("fileCommentName").textContent = vFilesystem.comments.name;
if(vFilesystem.comments.hasOwnProperty("desc")) {
@ -613,14 +684,49 @@
renderIcon(output2.n, output);
let cTime = vFilesystem.timestamps.created;
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 root directory time is null, read icon.sys instead
cTime = vFilesystem[vFilesystem.rootDirectory]["icon.sys"].timestamps.created;
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("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("dateCreated").textContent = timeToString(cTime);
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("icon.sys (cbs)", output);
} catch(e) {
@ -707,12 +813,15 @@
if (typeof pako === "undefined") {
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>
<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>
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();
</script>
</body>

View File

@ -62,6 +62,22 @@ switch(processObj.argv[2]) {
console.log(output);
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": {
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));
@ -70,7 +86,6 @@ switch(processObj.argv[2]) {
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");
});
}
@ -91,6 +106,8 @@ psv: Read a PS3 export file.
sps: Read a SharkPort export file.
xps: Read a X-Port 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
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);
}
}