A compatibility update.

No, this isn't a late 'Fool's joke, I was just having fun and now I can target pure ES6/ES2015 if I wanted to.

…except Chakra ('Edge <= 44), that thing never implemented `TextDecoder`/`TextEncoder` for some reason. We'll never know why...

I'm researching what makes over-bright happen... give me time!
This commit is contained in:
yellows111 2023-12-05 18:40:46 +00:00
parent 5738fbb09c
commit 98edefff2e
4 changed files with 124 additions and 58 deletions

View File

@ -1,7 +1,20 @@
# icondumper2 (working title)
A JavaScript library (sorta) to read PS2 icons, and their related formats.
## What it supports
## What is a "PS2 icon"?
A set of vertices with may or may not include a texture while defining colours for those vertices.
## 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)
* Requires writing/reading a specific format for successful output of data.
As of writing, there was no exporter that exists for the format that exhibited one of these problems.
**icondumper2** is the result of me trying to avoid these problems.
## What it supports:
* EMS Memory Adapter export files (.psu)
* PS3 virtual memory card export files (.psv)
* SharkPort export files (.sps)
@ -9,30 +22,41 @@ A JavaScript library (sorta) to read PS2 icons, and their related formats.
* PS2 icons (.ico, .icn)
* PS2D format (icon.sys)
## What can it do
* Allow any file in a PSU's or SPS/XPS's virtual filesystem to be dumped.
## What can it do:
* Allow any file in a supported virtual filesystem (such as those from export files) to be read.
* Warn of invalid icon.sys display names.
* Read and parse an EMS MA export file.
* Export the icon model, with all seperate shapes included to a JavaScript Object.
* Node.js compatible (CommonJS) exporting while still being compatible with other JavaScript implementations.
* Convert a 128x128x16 BGR5A1 bitmap to a RGB5A1 format.
* Export the icon model, with all seperate shapes included as a JavaScript Object.
* 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.
## What it doesn't do
* (Re)build save files.
* Modify the original input files.
* Use any Node.js-exclusive features.
## What it doesn't do:
* Create, manipulate or otherwise taint save files.
* Modify any original input files.
* Use any implementation-specific features.
## Client compatibility
(todo: write this)
## Client compatibility:
The library requires use of `const`, `let` and `class` declarations.
Any JavaScript implementation should work if they support all three of these declarations.
### Tested clients:
* Chrome (or Blink-based browser) 49 (or higher) - HTML reference client
* Firefox (or Gecko-based browser) 45 (or higher) - HTML reference client
* Node.js 13 (or higher) - Example client and glTF 2 exporter.
## Why "icondumper2"?
Because it replaced what *was* left of icondumper (1).
## Included files
## 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. |
## Included example files:
| Directory | Description | Formats |
| ------------ | ----------------------------------------- | -------- |
| /example_001 | A tetrahedron with all 3 base colors set. | PSU, SPS |

View File

@ -249,7 +249,7 @@ function loadAndConvertIcon(inputData, attemptedFilename = "-") {
if (inputData.hasOwnProperty("numberOfShapes") === false) {
throw "Expected a icondumper2 Intermediate Model Format object.";
}
const filename = encodeURIComponent(attemptedFilename).replaceAll(/\%[0-9A-F]{2,2}/g, "").replaceAll(".", "_");
const filename = encodeURIComponent(attemptedFilename).replace(/\%[0-9A-F]{2,2}/g, "").replace(/\./g, "_");
const glTF_output = imf2gltf(inputData, filename);
for (let index = 0; index < (inputData.numberOfShapes); index++) {
(require("fs")).writeFileSync(`${filename}_${index}.gltf`, new TextEncoder().encode(JSON.stringify(glTF_output.objects[index])));

30
icon.js
View File

@ -1,21 +1,22 @@
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
//LOOKING FOR: LZARI implementation (for MAX), description of CBS compression (node zlib doesn't tackle it, even with RC4'ing the data)
ICONJS_DEBUG = false;
ICONJS_STRICT = true;
var ICONJS_DEBUG = false;
var ICONJS_STRICT = true;
/**
* The current version of the library.
* @constant {string}
* @default
*/
const ICONJS_VERSION = "0.6.1";
const ICONJS_VERSION = "0.6.1+u1";
/**
* Extension of DataView to add shortcuts for datatypes that I use often.
* @augments DataView
* @constructor
* @param {ArrayBuffer} buffer ArrayBuffer to base DataView from.
* @returns {Object.<string, function(number): number>}
* @returns {Object.<string, function(number): number>} [u16le, f16le, u32le, f32le]
* @returns {Object.<string, function(number): Object.<string.number>>} [t64le]
* @access protected
*/
class yellowDataReader extends DataView {
@ -40,9 +41,16 @@ class yellowDataReader extends DataView {
*/
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).
* @param {number} i Indice offset.
* @returns {Object.<string, number>}
* @property {number} seconds - Seconds.
* @property {number} minutes - Minutes.
* @property {number} hours - Hours.
* @property {number} day - Day.
* @property {number} month - Month.
* @property {number} year - Year.
*/
t64le(i){return {
seconds: super.getUint8(i+1),
@ -78,7 +86,7 @@ function setDebug(value) {
* Select if invalid characters in titles should be replaced with either spaces or nulls
* @param {boolean} value - true: with nulls, false: with spaces
* @default true
* @deprecated unlikely to ever need this?
* @deprecated Hasn't been needed for a while. Dropping support by ESM transition.
* @public
*/
function setStrictness(value) {
@ -182,7 +190,7 @@ function convertBGR5A1toRGB5A1(bgrData) {
* @access protected
*/
function stringScrubber(dirty) {
return dirty.replaceAll("\x00","").substring(0, (dirty.indexOf("\x00") === -1) ? dirty.length : dirty.indexOf("\x00"));
return dirty.replace(/\0/g, "").substring(0, (dirty.indexOf("\x00") === -1) ? dirty.length : dirty.indexOf("\x00"));
}
/**
@ -627,9 +635,9 @@ function readSharkXPortSxpsFile(input) {
}
/**
* Define (module.)exports with all public functions
* Define (module.)exports with all public functions.
* @exports icondumper2/icon
*/
*/ // start c6js
exports = {
readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile},
helpers: {uncompressTexture, convertBGR5A1toRGB5A1},
@ -640,3 +648,9 @@ exports = {
if(typeof module !== "undefined") {
module.exports = exports;
}
//end c6js
//start esm
/*export {
readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, uncompressTexture, convertBGR5A1toRGB5A1, setDebug, ICONJS_VERSION
};*/
//end esm

View File

@ -20,6 +20,13 @@
input[type="file"] {line-height: 2em;}
#version {text-shadow: 1px 1px 2px black;}
a {color: #ccc;}
.inputbox {
display: inline-grid;
margin-right: 0.25em;
border: 1px gray solid;
padding: 0.175em 0.25em 0 0.25em;
margin-bottom: 4px;
}
</style>
<meta data-comment="WebGL Shader: Icon">
<script type="text/plain" id="shader-icon-v">
@ -133,15 +140,18 @@
<br>
</div>
<hr>
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label>
<input type="file" id="psuinput" name="psuinput" accept=".psu" />
<br>
<label for="psvinput">PS3 export file (.psv) goes here:</label>
<input type="file" id="psvinput" name="psvinput" accept=".psv" />
<br>
<label for="spsinput">SharkPort/X-Port export file (.sps, .xps) goes here:</label>
<input type="file" id="spsinput" name="spsinput" accept=".sps, .xps" />
<br>
<div class="inputbox">
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label>
<input type="file" id="psuinput" name="psuinput" accept=".psu" />
</div>
<div class="inputbox">
<label for="psvinput">PS3 export file (.psv) goes here:</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>
<input type="file" id="spsinput" name="spsinput" accept=".sps, .xps" />
</div>
<p>
<span>Date&nbsp;created: </span><span id="dateCreated">--:--:--&nbsp;--/--/----</span><span> UTC+09:00</span>
<wbr><span>&ndash;</span>
@ -152,16 +162,19 @@
</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}};
const GlobalState = {rotations: 2, dataLength: 0, uniforms: {rotation: null, scale: null}, iconState: {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"]');
allInputs.forEach(
Array.from(allInputs).forEach(
function(nodeObject) {
nodeObject.onclick = function() {
allInputs.forEach(function(elementObject) {elementObject.value = null;});
Array.from(allInputs).forEach(function(elementObject) {elementObject.value = null;});
}
}
);
function p0in(input) { // "prefix 0 if needed"
return ((input.length>=2) ? input : `0${input}`);
};
// rotation stuff
const rotationDensity = 60;
document.body.onkeydown = function(ev) {
@ -394,7 +407,7 @@
//.section LIGHTING
let colours = fileMetadata.lighting.colors[0];
//glFgContext.uniform3f(uniforms.ambientLighting, colours.r, colours.g, colours.b);
glFgContext.uniform3f(uniforms.ambientLighting, 1, 1, 1);
glFgContext.uniform3f(uniforms.ambientLighting, 0.75, 0.75, 0.75);
//.section SCALING
GlobalState.uniforms.scale = uniforms.scale;
@ -421,25 +434,30 @@
if(filebox.files.length === 0) {
return;
}
filebox.files[0].arrayBuffer().then(function(d){
GlobalState.fileReader.readAsArrayBuffer(filebox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let output = readPS2D(d);
let output = readPS2D(GlobalState.fileReader.result);
console.info("icon.sys", output);
updateDisplay(output);
} catch(e) {
if(glBgContext!==null){glBgContext.clear(glBgContext.COLOR_BUFFER_BIT);}
console.error(e);
alert(e);
}
});
}
}
iconbox = document.getElementById("icon");
iconbox.onchange = function(e) {
if(iconbox.files.length === 0) {
return;
}
iconbox.files[0].arrayBuffer().then(function(d){
GlobalState.fileReader.readAsArrayBuffer(iconbox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let output = readIconFile(d);
let output = readIconFile(GlobalState.fileReader.result);
GlobalState.iconState.cachedIconSys = null;
GlobalState.iconState.source = null;
GlobalState.iconState.currentSubmodel = 0;
@ -448,9 +466,10 @@
console.info("model data (ic*)",output);
} catch(e) {
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
console.error(e);
alert(e);
}
});
}
}
psubox = document.getElementById("psuinput");
psubox.onchange = function(e) {
@ -458,9 +477,11 @@
if(psubox.files.length === 0) {
return;
}
psubox.files[0].arrayBuffer().then(function(d){
GlobalState.fileReader.readAsArrayBuffer(psubox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let vFilesystem = readEmsPsuFile(d);
let vFilesystem = readEmsPsuFile(GlobalState.fileReader.result);
let output = readPS2D(vFilesystem[vFilesystem.rootDirectory]["icon.sys"].data);
updateDisplay(output);
let output2 = new Object();
@ -475,16 +496,17 @@
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 = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
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}`;
console.info("model files (psu)", output2);
console.info("icon.sys (psu)", 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);
}
});
}
}
psvbox = document.getElementById("psvinput");
psvbox.onchange = function(e) {
@ -492,9 +514,11 @@
if(psvbox.files.length === 0) {
return;
}
psvbox.files[0].arrayBuffer().then(function(d){
GlobalState.fileReader.readAsArrayBuffer(psvbox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let inputData = readPsvFile(d);
let inputData = readPsvFile(GlobalState.fileReader.result);
let output = readPS2D(inputData["icon.sys"]);
updateDisplay(output);
const icons = {
@ -510,16 +534,17 @@
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 = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
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}`;
console.info("model files (psv)", icons);
console.info("icon.sys (psv)", 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);
}
});
}
}
spsbox = document.getElementById("spsinput");
spsbox.onchange = function(e) {
@ -527,9 +552,11 @@
if(spsbox.files.length === 0) {
return;
}
spsbox.files[0].arrayBuffer().then(function(d){
GlobalState.fileReader.readAsArrayBuffer(spsbox.files[0]);
GlobalState.fileReader.onloadend = function() {
GlobalState.fileReader.onloadend = void(0);
try {
let vFilesystem = readSharkXPortSxpsFile(d);
let vFilesystem = readSharkXPortSxpsFile(GlobalState.fileReader.result);
let output = readPS2D(vFilesystem[vFilesystem.rootDirectory]["icon.sys"].data);
updateDisplay(output);
let output2 = new Object();
@ -544,8 +571,8 @@
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 = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
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}`;
document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game;
document.getElementById("fileCommentName").textContent = vFilesystem.comments.name;
if(vFilesystem.comments.hasOwnProperty("desc")) {
@ -556,9 +583,10 @@
} 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);
}
});
};
}
function createShader(gl, type, source) {
let shader = gl.createShader(type);
@ -639,7 +667,7 @@
<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.6.6";
document.getElementById("clientVersion").textContent = "0.6.7";
document.getElementById("currentYear").textContent = (new Date()).getFullYear().toString();
</script>
</body>