2023-10-12 02:49:07 -04:00
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
ICONJS _DEBUG = false ;
ICONJS _STRICT = true ;
2023-10-18 03:25:42 -04:00
ICONJS _VERSION = "0.4.1" ;
2023-10-12 02:49:07 -04:00
function setDebug ( value ) {
ICONJS _DEBUG = ! ! value ;
}
function setStrictness ( value ) {
ICONJS _STRICT = ! ! value ;
}
// where U = uncompressed, N = none, C = compressed
function getTextureFormat ( i ) {
if ( i < 8 ) {
if ( i == 3 ) {
return 'N' ;
}
return 'U' ;
} else if ( i >= 8 ) {
return 'C' ;
} else {
return void ( 0 ) ;
}
}
2023-10-17 15:59:54 -04:00
function uncompressTexture ( texData ) {
2023-10-18 03:25:42 -04:00
// for texture formats 8-15
2023-10-17 15:59:54 -04:00
if ( texData . length & 1 ) {
throw "Texture size isn't a multiple of 2 (was ${texData.length})" ;
}
const view = new DataView ( texData ) ;
const u16le = function ( i ) { return view . getUint16 ( i , 1 ) }
let uncompressed = new Uint16Array ( 16384 ) ;
let offset = 0 ;
2023-10-18 03:25:42 -04:00
for ( let index = 0 ; index < 16384 ; ) {
2023-10-17 15:59:54 -04:00
currentValue = u16le ( offset ) ;
2023-10-17 18:35:03 -04:00
if ( currentValue === 0 ) {
// if this is specifically a katamari 1 or 2 icon, skip this byte
// because it's formatted like that for some reason
offset += 2 ;
currentValue = u16le ( offset ) ;
}
2023-10-17 15:59:54 -04:00
offset += 2 ;
if ( currentValue >= 0xff00 ) {
2023-10-18 03:25:42 -04:00
//do a raw copy of the next currentValue bytes
2023-10-17 15:59:54 -04:00
let length = ( ( 0x10000 - currentValue ) ) ;
for ( let enumerator = 0 ; enumerator < length ; enumerator ++ ) {
uncompressed [ index ] = u16le ( offset ) ;
offset += 2 ;
index ++ ;
}
} else {
2023-10-18 03:25:42 -04:00
//repeat next byte currentValue times
2023-10-17 15:59:54 -04:00
uncompressed [ index ] = u16le ( offset ) ;
for ( let indey = 0 ; indey < currentValue ; indey ++ ) {
uncompressed [ index ] = u16le ( offset ) ;
index ++
}
offset += 2 ;
}
}
return uncompressed ;
}
function convertBGR5A1toRGB5A1 ( bgrData ) {
if ( bgrData . byteLength !== 32768 ) {
throw ` Not a 128x128x16 texture. (length was ${ bgrData . length } ) ` ;
}
// converts 5-bit blue, green, red (in that order) with one alpha bit to GL-compatible RGB5A1
let converted = new Uint16Array ( 16384 ) ;
for ( let index = 0 ; index < 16384 ; index ++ ) {
2023-10-18 03:25:42 -04:00
let b = ( bgrData [ index ] & 0b11111 ) ;
let g = ( ( bgrData [ index ] >> 5 ) & 0b11111 ) ;
let r = ( ( bgrData [ index ] >> 10 ) & 0b11111 ) ;
2023-10-17 15:59:54 -04:00
// rrrrrgggggbbbbba (a = 1 because 0 is 127 which is 1.0f opacity for the GS)
let newValue = 0b0000000000000001 ;
newValue |= ( r << 1 ) ;
newValue |= ( g << 6 ) ;
newValue |= ( b << 11 ) ;
converted [ index ] = newValue ;
}
return converted ;
2023-10-12 02:49:07 -04:00
}
function stringScrubber ( dirty ) {
return dirty . replaceAll ( "\x00" , "" ) . substring ( 0 , ( dirty . indexOf ( "\x00" ) === - 1 ) ? dirty . length : dirty . indexOf ( "\x00" ) ) ;
}
function readPS2D ( input ) {
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) }
const f32le = function ( i ) { return view . getFloat32 ( i , 1 ) }
//!pattern ps2d.hexpat
const header = u32le ( 0 ) ;
if ( header !== 0x44325350 ) {
throw ` Not a PS2D file (was ${ header } , expected ${ 0x44325350 } ) ` ;
}
//:skip 2
const titleOffset = u32le ( 6 ) ;
//:skip 2
const bgAlpha = u32le ( 12 ) ; // should read as a u8, (k/127?.5) for float value (255 = a(2.0~))
const bgColors = [
{ r : u32le ( 16 ) , g : u32le ( 20 ) , b : u32le ( 24 ) , a : u32le ( 28 ) } ,
{ r : u32le ( 32 ) , g : u32le ( 36 ) , b : u32le ( 40 ) , a : u32le ( 44 ) } ,
{ r : u32le ( 48 ) , g : u32le ( 52 ) , b : u32le ( 56 ) , a : u32le ( 60 ) } ,
{ r : u32le ( 64 ) , g : u32le ( 68 ) , b : u32le ( 72 ) , a : u32le ( 76 ) }
] ; // top-left, top-right, bottom-left, bottom-right;
const lightIndices = [
{ x : f32le ( 80 ) , y : f32le ( 84 ) , z : f32le ( 88 ) } , //:skip 4
{ x : f32le ( 96 ) , y : f32le ( 100 ) , z : f32le ( 104 ) } , //:skip 4
{ x : f32le ( 112 ) , y : f32le ( 116 ) , z : f32le ( 120 ) } //:skip 4
]
const lightColors = [
{ r : f32le ( 128 ) , g : f32le ( 132 ) , b : f32le ( 136 ) , a : f32le ( 140 ) } ,
{ r : f32le ( 144 ) , g : f32le ( 148 ) , b : f32le ( 152 ) , a : f32le ( 156 ) } ,
{ r : f32le ( 160 ) , g : f32le ( 164 ) , b : f32le ( 168 ) , a : f32le ( 172 ) } ,
{ r : f32le ( 176 ) , g : f32le ( 180 ) , b : f32le ( 184 ) , a : f32le ( 188 ) }
]
2023-10-15 15:28:59 -04:00
// 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.
2023-10-12 02:49:07 -04:00
const int _title = input . slice ( 0xc0 , 0x100 ) ;
const tmp _title16 = new Uint16Array ( int _title ) ;
for ( let index = 0 ; index < 32 ; index ++ ) {
//find "bad" shift-jis two-bytes, and convert to spaces (or NULL if strict)
if ( tmp _title16 [ index ] === 129 || tmp _title16 [ index ] === 33343 ) {
console . warn ( ` PS2D syntax error: known bad two-byte sequence 0x ${ tmp _title16 [ index ] . toString ( 16 ) . padEnd ( 4 , "0" ) } (at ${ index } ). Replacing with ${ ( ICONJS _STRICT ) ? "NULL" : "0x8140" } . \n ${ ( ICONJS _STRICT ) ? "This is console-accurate, so" : "An actual console does not do this." } I recommend patching your icon.sys files! ` ) ;
tmp _title16 [ index ] = ( ICONJS _STRICT ) ? 0 : 0x4081 ; // is a reference, so this'll edit int_title too
}
}
//:skip 4 -- Unless proven, keep 64 bytes for display name.
const int _filename _n = input . slice ( 0x104 , 0x143 ) ;
const int _filename _c = input . slice ( 0x144 , 0x183 ) ;
const int _filename _d = input . slice ( 0x184 , 0x1C3 ) ;
//;skip 512 -- rest of this is just padding
const rawTitle = ( new TextDecoder ( "shift-jis" ) ) . decode ( int _title ) ;
const title = [
stringScrubber ( rawTitle . substring ( 0 , ( titleOffset / 2 ) ) ) ,
( rawTitle . indexOf ( "\x00" ) < ( titleOffset / 2 ) ) ? "" : stringScrubber ( rawTitle . substring ( ( titleOffset / 2 ) ) )
] ;
const filenames = {
n : stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( int _filename _n ) ) ,
c : stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( int _filename _c ) ) ,
d : stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( int _filename _d ) )
}
if ( ICONJS _DEBUG ) {
console . log ( { header , titleOffset , bgAlpha , bgColors , lightIndices , lightColors , title , filenames } ) ;
}
return { filenames , title , background : { colors : bgColors , alpha : bgAlpha } , lighting : { points : lightIndices , colors : lightColors } } ;
}
function readIconFile ( input ) {
//!pattern ps2icon-hacked.hexpat
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) }
const f32le = function ( i ) { return view . getFloat32 ( i , 1 ) }
const f16le = function ( i ) { return ( view . getInt16 ( i , 1 ) / 4096 ) }
const u32 _rgba8 = function ( i ) { return {
r : ( i & 0xff ) ,
g : ( ( i & 0xff00 ) >> 8 ) ,
b : ( ( i & 0xff0000 ) >> 16 ) ,
2023-10-18 10:28:34 -04:00
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.
2023-10-12 02:49:07 -04:00
} } ;
const magic = u32le ( 0 ) ;
if ( magic !== 0x010000 ) {
// USER WARNING: APPARENTLY NOT ALL ICONS ARE 0x010000. THIS THROW WILL BE DROPPED LATER.
throw ` Not a PS2 icon file (was ${ magic } , expected ${ 0x010000 } ) ` ;
}
const numberOfShapes = u32le ( 4 ) ;
2023-10-12 16:59:39 -04:00
if ( numberOfShapes > 16 ) {
throw ` Too many defined shapes! Is this a valid file? (file reports ${ numberOfShapes } shapes) ` ;
}
2023-10-12 02:49:07 -04:00
const textureType = u32le ( 8 ) ;
const textureFormat = getTextureFormat ( textureType ) ;
//:skip 4
const numberOfVertexes = u32le ( 16 ) ;
2023-10-12 16:59:39 -04:00
if ( ! ! ( numberOfVertexes % 3 ) ) {
throw ` Not enough vertices to define a triangle ( ${ numberOfVertexes % 3 } vertices remained). ` ;
}
2023-10-12 02:49:07 -04:00
// format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk]
let offset = 20 ;
let vertices = new Array ( ) ;
const chunkLength = ( ( numberOfShapes * 8 ) + 16 ) ;
// for numberOfVertexes, copy chunkLength x times, then append with normal, uv and rgba8 color. Floats are 16-bit.
for ( let index = 0 ; index < numberOfVertexes ; index ++ ) {
let shapes = new Array ( ) ;
for ( let indey = 0 ; indey < numberOfShapes ; indey ++ ) {
shapes . push ( {
x : f16le ( offset + ( ( chunkLength * index ) + ( indey * 8 ) ) ) ,
y : f16le ( offset + ( ( chunkLength * index ) + ( indey * 8 ) ) + 2 ) ,
z : f16le ( offset + ( ( chunkLength * index ) + ( indey * 8 ) ) + 4 )
} ) ;
//:skip 2
}
let normal = {
x : f16le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) ) ,
y : f16le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) + 2 ) ,
z : f16le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) + 4 )
} ;
//:skip 2
let uv = {
u : f16le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) + 8 ) ,
v : f16le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) + 10 )
} ;
let color = u32 _rgba8 ( u32le ( offset + ( chunkLength * index ) + ( ( numberOfShapes * 8 ) ) + 12 ) ) ;
// keep original u32le color?
vertices . push ( { shapes , normal , uv , color } ) ;
}
offset = ( 20 + ( numberOfVertexes * chunkLength ) ) ;
animationHeader = { id : u32le ( offset ) , length : u32le ( offset + 4 ) , speed : f32le ( offset + 8 ) , "offset" : u32le ( offset + 12 ) , keyframes : u32le ( offset + 16 ) } ;
let animData = new Array ( ) ;
2023-10-12 16:59:39 -04:00
// now we have to enumerate values, so now we introduce an offset value.
2023-10-12 02:49:07 -04:00
// format for a keyframe: sssskkkk[ffffvvvv] where [ffffvvvv] repeat based on the value that kkkk(eys) has.
// sssskkkk[ffffvvvv] is repeated based on animationHeader.keyframes value.
offset += 20 ;
for ( let index = 0 ; index < animationHeader . keyframes ; index ++ ) {
let frameData = new Array ( ) ;
let shapeId = u32le ( offset ) ;
let keys = u32le ( offset + 4 ) ;
offset += 8 ;
for ( let indey = 0 ; indey < keys ; indey ++ ) {
frameData . push ( { frame : f32le ( offset ) , value : f32le ( offset + 4 ) } ) ;
offset += 8 ;
}
animData . push ( { shapeId , keys , frameData } ) ;
}
let texture = null ;
switch ( textureFormat ) {
case 'N' : {
break ;
}
case 'U' : {
//where every 16-bit entry is a BGR5A1 color 0b[bbbbbgggggrrrrra]
texture = new Uint16Array ( input . slice ( offset , ( offset + 0x8000 ) ) ) ;
break ;
}
case 'C' : {
2023-10-12 16:59:39 -04:00
// compression format is RLE-based, where first u32 is size, and format is defined as:
/ * *
* u16 rleType ;
* if ( rleType >= 0xff00 ) {
* //do a raw copy
2023-10-17 15:59:54 -04:00
* let length = ( 0x10000 - rleType ) ;
2023-10-12 16:59:39 -04:00
* byte data [ length ] ;
* } else {
* //repeat next byte rleType times
* data = new Uint16Array ( rleType ) ;
* for ( let index = 0 ; index < rleType ; index ++ ) {
* data [ index ] = u16 repeater @ + 4 ;
* }
* }
* * /
//output of this will be another u16[0x4000] of the decompressed texture
2023-10-17 15:59:54 -04:00
//after that just parse output as-if it was uncompressed.
2023-10-12 02:49:07 -04:00
size = u32le ( offset ) ;
texture = { size , data : input . slice ( offset + 4 , offset + ( 4 + size ) ) } ;
}
}
if ( ICONJS _DEBUG ) {
console . log ( { magic , numberOfShapes , textureType , textureFormat , numberOfVertexes , chunkLength , vertices , animationHeader , animData , texture } ) ;
}
return { numberOfShapes , vertices , textureFormat , texture , animData } ;
}
function readEntryBlock ( input ) {
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) } ;
const t64le = function ( i ) { return {
seconds : view . getUint8 ( i + 1 ) ,
minutes : view . getUint8 ( i + 2 ) ,
hours : view . getUint8 ( i + 3 ) ,
day : view . getUint8 ( i + 4 ) ,
month : view . getUint8 ( i + 5 ) ,
year : view . getUint16 ( i + 6 , 1 )
} } ; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly!
//!pattern psu_file.hexpat
const permissions = u32le ( 0 ) ;
let type ;
if ( permissions > 0xffff ) {
2023-10-12 02:54:59 -04:00
throw ` Not a EMS Memory Adapter (PSU) export file (was ${ permissions } , expected less than ${ 0xffff } ) ` ;
2023-10-12 02:49:07 -04:00
}
if ( ( permissions & 0b00100000 ) >= 1 ) {
type = "directory" ;
}
if ( ( permissions & 0b00010000 ) >= 1 ) {
type = "file" ;
}
if ( ( permissions & 0b0001100000000000 ) >= 1 ) {
throw ` I don't parse portable applications or legacy save data. ( ${ permissions } has bits 10 or 11 set) ` ;
}
const size = u32le ( 4 ) ;
const createdTime = t64le ( 8 ) ;
const sectorOffset = u32le ( 16 ) ;
const dirEntry = u32le ( 20 ) ;
const modifiedTime = t64le ( 24 ) ;
const specialSection = input . slice ( 0x20 , 0x40 ) ;
const int _filename = input . slice ( 0x40 , 512 ) ;
const filename = stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( int _filename ) ) ;
if ( ICONJS _DEBUG ) {
console . log ( { permissions , type , size , createdTime , sectorOffset , dirEntry , modifiedTime , specialSection , filename } ) ;
}
return { type , size , filename , createdTime , modifiedTime } ;
}
function readEmsPsuFile ( input ) {
const header = readEntryBlock ( input . slice ( 0 , 0x1ff ) ) ;
2023-10-15 15:28:59 -04:00
if ( header . size > 0x7f ) {
throw ` Directory is too large! (maximum size: ${ 0x7f } , was ${ header . size } ) `
}
2023-10-12 02:49:07 -04:00
let fsOut = { length : header . size , rootDirectory : header . filename , timestamps : { created : header . createdTime , modified : header . modifiedTime } } ;
let output = new Object ( ) ;
let offset = 512 ;
for ( let index = 0 ; index < header . size ; index ++ ) {
fdesc = readEntryBlock ( input . slice ( offset , offset + 512 ) ) ;
switch ( fdesc . type ) {
case "directory" : {
offset += 512 ;
output [ fdesc . filename ] = null ;
break ;
}
case "file" : {
if ( ICONJS _DEBUG ) {
console . log ( ` PARSING | F: " ${ fdesc . filename } " O: ${ offset } S: ${ fdesc . size } ` ) ;
}
offset += 512 ;
const originalOffset = offset ;
if ( ( fdesc . size % 1024 ) > 0 ) {
offset += ( ( fdesc . size & 0b11111111110000000000 ) + 1024 ) ;
} else {
offset += fdesc . size ;
// if we're already filling sectors fully, no to change anything about it
}
output [ fdesc . filename ] = {
size : fdesc . size ,
data : input . slice ( originalOffset , originalOffset + fdesc . size )
} ;
break ;
}
}
}
fsOut [ header . filename ] = output ;
return fsOut ;
}
2023-10-12 05:51:46 -04:00
function readPsvFile ( input ) {
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) } ;
2023-10-12 16:59:39 -04:00
const t64le = function ( i ) { return {
seconds : view . getUint8 ( i + 1 ) ,
minutes : view . getUint8 ( i + 2 ) ,
hours : view . getUint8 ( i + 3 ) ,
day : view . getUint8 ( i + 4 ) ,
month : view . getUint8 ( i + 5 ) ,
year : view . getUint16 ( i + 6 , 1 )
} } ;
2023-10-12 05:51:46 -04:00
//!pattern psv_file.hexpat
const magic = u32le ( 0 ) ;
if ( magic !== 0x50535600 ) {
throw ` Not a PS3 export (PSV) file (was ${ magic } , expected ${ 0x50535600 } ) ` ;
}
//:skip 4
//:skip 20 // key seed, console ignores this
2023-10-12 16:59:39 -04:00
//:skip 20 // sha1 hmac digest, useful for verifying that this is, indeed, save data.
2023-10-12 05:51:46 -04:00
//:skip 8
2023-10-12 16:59:39 -04:00
const type1 = u32le ( 56 ) ;
const type2 = u32le ( 60 ) ;
2023-10-12 05:51:46 -04:00
if ( type1 !== 0x2c && type2 !== 2 ) {
throw ` Not parsing, this is not a PS2 save export (was ${ type1 } : ${ type2 } , expected 44:2) ` ;
}
2023-10-12 16:59:39 -04:00
const displayedSize = u32le ( 64 ) ;
const ps2dOffset = u32le ( 68 ) ;
const ps2dSize = u32le ( 72 ) ; // don't know why this is included if its always 964
const nModelOffset = u32le ( 76 ) ;
const nModelSize = u32le ( 80 ) ;
const cModelOffset = u32le ( 84 ) ;
const cModelSize = u32le ( 88 ) ;
const dModelOffset = u32le ( 92 ) ;
const dModelSize = u32le ( 96 ) ;
const numberOfFiles = u32le ( 100 ) ; // in-case this library changes stance on other files
// file = {t64le created, t64le modified, u32 size, u32 permissions, byte[32] title}
// and if it's not the root directory, add another u32 for offset/location
const rootDirectoryData = input . slice ( 104 , 162 ) ;
const timestamps = { created : t64le ( 104 ) , modified : t64le ( 112 ) } ;
let offset = 162 ;
2023-10-12 05:51:46 -04:00
let fileData = new Array ( ) ;
for ( let index = 0 ; index < numberOfFiles ; index ++ ) {
fileData . push ( input . slice ( offset , offset + 0x3c ) ) ;
offset += 0x3c ;
} ;
//then file data after this but we already have pointers to the files we care about
const icons = {
n : input . slice ( nModelOffset , nModelOffset + nModelSize ) ,
c : input . slice ( cModelOffset , cModelOffset + cModelSize ) ,
d : input . slice ( dModelOffset , dModelOffset + dModelSize ) ,
}
if ( ICONJS _DEBUG ) {
console . log ( { magic , type1 , type2 , displayedSize , ps2dOffset , ps2dSize , nModelOffset , nModelSize , cModelOffset , cModelSize , dModelOffset , dModelSize , numberOfFiles , rootDirectoryData , fileData } )
}
2023-10-12 16:59:39 -04:00
return { icons , "icon.sys" : input . slice ( ps2dOffset , ps2dOffset + ps2dSize ) , timestamps } ;
2023-10-12 05:51:46 -04:00
}
2023-10-15 15:28:59 -04:00
function readSxpsDescriptor ( input ) {
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) } ;
const t64le = function ( i ) { return {
seconds : view . getUint8 ( i + 1 ) ,
minutes : view . getUint8 ( i + 2 ) ,
hours : view . getUint8 ( i + 3 ) ,
day : view . getUint8 ( i + 4 ) ,
month : view . getUint8 ( i + 5 ) ,
year : view . getUint16 ( i + 6 , 1 )
} } ; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly!
//!pattern sps-xps_file.hexpat
//:skip 2 // ... it's the file descriptor block size (including the bytes themselves, so 250)
const int _filename = input . slice ( 2 , 66 ) ;
const filename = stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( int _filename ) ) ;
const size = u32le ( 66 ) ;
const startSector = u32le ( 70 ) ;
const endSector = u32le ( 74 ) ;
const permissions = u32le ( 78 ) ; // the first two bytes are *swapped*. The comments that ensued were not kept.
let type ;
if ( permissions > 0xffff ) {
throw ` Not a SharkPort (SPS) or X-Port (XPS) export file (was ${ permissions } , expected less than ${ 0xffff } ) ` ;
}
if ( ( permissions & 0b0010000000000000 ) >= 1 ) {
type = "directory" ;
}
if ( ( permissions & 0b0001000000000000 ) >= 1 ) {
type = "file" ;
}
if ( ( permissions & 0b00011000 ) >= 1 ) {
throw ` I don't parse portable applications or legacy save data. ( ${ permissions } has bits 4 or 5 set) ` ;
}
const timestamps = { created : t64le ( 82 ) , modified : t64le ( 90 ) } ;
//:skip 4
//:skip 4 - u32 optional (98)
//:skip 8 - t64 optionalTime (102) // I don't know why this is here.
const int _asciiName = input . slice ( 114 , 178 ) ;
const int _shiftjisName = input . slice ( 178 , 242 ) ; // Because why parse a PS2D when you can hard-code it?
//:skip 8
if ( ICONJS _DEBUG ) {
console . log ( { int _filename , size , startSector , endSector , permissions , type , timestamps , int _asciiName , int _shiftjisName } ) ;
}
return { type , size , filename , timestamps } ;
}
function readSharkXPortSxpsFile ( input ) {
const view = new DataView ( input ) ;
const u32le = function ( i ) { return view . getUint32 ( i , 1 ) } ;
//!pattern sps-xps_file.hexpat
const identLength = u32le ( 0 ) ;
2023-10-17 15:59:54 -04:00
if ( identLength !== 13 ) {
throw ` Not a SharkPort (SPS) or X-Port (XPS) export file (was ${ identLength } , expected 13) ` ;
}
2023-10-15 15:28:59 -04:00
let offset = 4 ;
const ident = input . slice ( offset , offset + identLength ) ;
if ( ( new TextDecoder ( "utf-8" ) ) . decode ( ident ) !== "SharkPortSave" ) {
throw ` Unrecognized file identification string. Expected "SharkPortSave". `
}
offset += ( identLength + 4 ) ;
const titleLength = u32le ( offset ) ;
const title = input . slice ( offset + 4 , ( offset + 4 ) + titleLength ) ;
offset += ( titleLength + 4 ) ;
const descriptionLength = u32le ( offset ) ;
const description = input . slice ( offset + 4 , ( offset + 4 ) + descriptionLength ) ;
offset += ( descriptionLength + 8 ) ;
const comments = {
"game" : stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( title ) ) ,
"name" : stringScrubber ( ( new TextDecoder ( "utf-8" ) ) . decode ( description ) )
}
const totalSize = u32le ( offset ) ;
offset += 4 ;
const header = readSxpsDescriptor ( input . slice ( offset , offset + 250 ) ) ;
offset += 250 ;
// alright now lets parse some actual data
let fsOut = { length : header . size , rootDirectory : header . filename , timestamps : header . timestamps , comments } ;
let output = new Object ( ) ;
for ( let index = 0 ; index < ( header . size - 2 ) ; index ++ ) {
fdesc = readSxpsDescriptor ( input . slice ( offset , offset + 250 ) ) ;
switch ( fdesc . type ) {
case "directory" : {
offset += 250 ;
output [ fdesc . filename ] = null ;
break ;
}
case "file" : {
if ( ICONJS _DEBUG ) {
console . log ( ` PARSING | F: " ${ fdesc . filename } " O: ${ offset } S: ${ fdesc . size } ` ) ;
}
offset += 250 ;
output [ fdesc . filename ] = {
size : fdesc . size ,
data : input . slice ( offset , offset + fdesc . size )
} ;
offset += fdesc . size ;
break ;
}
}
}
fsOut [ header . filename ] = output ;
2023-10-17 15:59:54 -04:00
//:skip 4 // then here lies, at offset (the end of file), a u32 checksum.
2023-10-15 15:28:59 -04:00
return fsOut ;
}
2023-10-12 02:49:07 -04:00
if ( typeof module !== "undefined" ) {
// for C6JS
2023-10-15 15:28:59 -04:00
module . exports = { readIconFile , readPS2D , readEmsPsuFile , readPsvFile , readSharkXPortSxpsFile , setDebug , setStrictness } ;
2023-10-12 02:49:07 -04:00
}