diff --git a/src/formats.c b/src/formats.c
index 2e176c75..98a8c890 100644
--- a/src/formats.c
+++ b/src/formats.c
@@ -393,7 +393,7 @@ static const char* extension_list[] = {
"pona",
"pos",
"ps2stm", //fake extension for .stm (renamed? to be removed?)
- "psb", //txth/reserved [Legend of Mana (Switch), Senxin Aleste (AC)]
+ "psb",
"psf",
"psh", //fake extension for .vsv (to be removed)
"psnd",
@@ -1363,6 +1363,7 @@ static const meta_info meta_info_list[] = {
{meta_WXD_WXH, "Relic WXD+WXH header"},
{meta_BNK_RELIC, "Relic BNK header"},
{meta_XSH_XSD_XSS, "Treyarch XSH+XSD/XSS header"},
+ {meta_PSB, "M2 PSB header"},
};
void get_vgmstream_coding_description(VGMSTREAM* vgmstream, char* out, size_t out_size) {
diff --git a/src/libvgmstream.vcxproj b/src/libvgmstream.vcxproj
index 1dfd82c5..8c42ff29 100644
--- a/src/libvgmstream.vcxproj
+++ b/src/libvgmstream.vcxproj
@@ -170,6 +170,7 @@
+
@@ -244,6 +245,7 @@
+
@@ -712,6 +714,7 @@
+
diff --git a/src/libvgmstream.vcxproj.filters b/src/libvgmstream.vcxproj.filters
index 179448be..accf8b13 100644
--- a/src/libvgmstream.vcxproj.filters
+++ b/src/libvgmstream.vcxproj.filters
@@ -308,6 +308,9 @@
Header Files
+
+ Header Files
+
@@ -1597,6 +1600,9 @@
meta\Source Files
+
+ meta\Source Files
+
meta\Source Files
@@ -1930,5 +1936,8 @@
util\Source Files
+
+ util\Source Files
+
\ No newline at end of file
diff --git a/src/meta/meta.h b/src/meta/meta.h
index 25c232f1..807a6d75 100644
--- a/src/meta/meta.h
+++ b/src/meta/meta.h
@@ -962,4 +962,6 @@ VGMSTREAM* init_vgmstream_bnk_relic(STREAMFILE* sf);
VGMSTREAM* init_vgmstream_xsh_xsd_xss(STREAMFILE* sf);
+VGMSTREAM* init_vgmstream_psb(STREAMFILE* sf);
+
#endif /*_META_H*/
diff --git a/src/meta/psb.c b/src/meta/psb.c
new file mode 100644
index 00000000..0c9246e9
--- /dev/null
+++ b/src/meta/psb.c
@@ -0,0 +1,587 @@
+#include "meta.h"
+#include "../coding/coding.h"
+#include "../util/m2_psb.h"
+
+
+//todo prepare multichannel
+#define PSB_MAX_LAYERS 1
+
+typedef enum { PCM, RIFF_AT3, XMA2, MSADPCM, XWMA, DSP, OPUSNX, RIFF_AT9, VAG } psb_codec_t;
+typedef struct {
+ const char* id; /* format */
+ const char* spec; /* platform */
+ const char* ext; /* codec extension (not always) */
+ const char* voice; /* base name (mandatory) */
+ const char* file; /* original name, often but not always same as voice (optional?) */
+ const char* uniq; /* unique name, typically same as file without extension (optional) */
+ const char* wav; /* same as file (optional) */
+} psb_temp_t;
+typedef struct {
+ psb_temp_t* tmp;
+ psb_codec_t codec;
+ char readable_name[STREAM_NAME_SIZE];
+
+ int total_subsongs;
+ int target_subsong;
+
+ /* chunks references */
+ uint32_t stream_offset;
+ uint32_t stream_size;
+ uint32_t intro_offset;
+ uint32_t intro_size;
+ uint32_t fmt_offset;
+ uint32_t fmt_size;
+ uint32_t dpds_offset;
+ uint32_t dpds_size;
+
+ int layers;
+ int channels;
+ int format;
+ int sample_rate;
+ int block_size;
+ int avg_bitrate;
+ int bps;
+
+ int32_t num_samples;
+ int32_t intro_samples;
+ int32_t skip_samples;
+ int loop_flag;
+ int32_t loop_start;
+ int32_t loop_end;
+
+} psb_header_t;
+
+
+static int parse_psb(STREAMFILE* sf, psb_header_t* psb);
+
+
+/* PSB - M2 container [Sega Vintage Collection (multi), Legend of Mana (multi)] */
+VGMSTREAM* init_vgmstream_psb(STREAMFILE* sf) {
+ VGMSTREAM* vgmstream = NULL;
+ psb_header_t psb = {0};
+
+
+ /* checks */
+ if (!is_id32be(0x00,sf, "PSB\0"))
+ goto fail;
+ if (!check_extensions(sf, "psb"))
+ goto fail;
+
+ if (!parse_psb(sf, &psb))
+ goto fail;
+
+
+ /* handle subfiles */
+ {
+ const char* ext = NULL;
+ VGMSTREAM* (*init_vgmstream)(STREAMFILE* sf) = NULL;
+
+ switch(psb.codec) {
+ case RIFF_AT3: /* Sega Vintage Collection (PS3) */
+ ext = "at3";
+ init_vgmstream = init_vgmstream_riff;
+ break;
+
+ case VAG: /* Plastic Memories (Vita) */
+ ext = "vag";
+ init_vgmstream = init_vgmstream_vag;
+ break;
+
+ case RIFF_AT9: /* Plastic Memories (Vita) */
+ ext = "at9";
+ init_vgmstream = init_vgmstream_riff;
+ break;
+
+ default:
+ break;
+ }
+
+ if (init_vgmstream != NULL) {
+ STREAMFILE* temp_sf = setup_subfile_streamfile(sf, psb.stream_offset, psb.stream_size, ext);
+ if (!temp_sf) goto fail;
+
+ vgmstream = init_vgmstream(temp_sf);
+ close_streamfile(temp_sf);
+ if (!vgmstream) goto fail;
+
+ vgmstream->num_streams = psb.total_subsongs;
+ strncpy(vgmstream->stream_name, psb.readable_name, STREAM_NAME_SIZE);
+ return vgmstream;
+ }
+ }
+
+
+ /* build the VGMSTREAM */
+ vgmstream = allocate_vgmstream(psb.channels, psb.loop_flag);
+ if (!vgmstream) goto fail;
+
+ vgmstream->meta_type = meta_PSB;
+ vgmstream->sample_rate = psb.sample_rate;
+ vgmstream->num_samples = psb.num_samples;
+ vgmstream->loop_start_sample = psb.loop_start;
+ vgmstream->loop_end_sample = psb.loop_end;
+ vgmstream->num_streams = psb.total_subsongs;
+ vgmstream->stream_size = psb.stream_size;
+
+ switch(psb.codec) {
+ case PCM:
+ switch(psb.bps) {
+ case 16: vgmstream->coding_type = coding_PCM16LE; break; /* Legend of Mana (PC), Namco Museum Archives Vol.1 (PC) */
+ case 24: vgmstream->coding_type = coding_PCM24LE; break; /* Legend of Mana (PC) */
+ default:
+ vgm_logi("PSB: unknown bps %i (report)\n", psb.bps);
+ goto fail;
+ }
+ vgmstream->layout_type = layout_interleave;
+ vgmstream->interleave_block_size = psb.block_size / psb.channels;
+ if (!vgmstream->num_samples)
+ vgmstream->num_samples = pcm_bytes_to_samples(psb.stream_size, psb.channels, psb.bps);
+ break;
+
+ case MSADPCM: /* [Senxin Aleste (AC)] */
+ vgmstream->coding_type = coding_MSADPCM;
+ vgmstream->layout_type = layout_none;
+ vgmstream->frame_size = psb.block_size;
+ if (!vgmstream->num_samples)
+ vgmstream->num_samples = msadpcm_bytes_to_samples(psb.stream_size, psb.block_size, psb.channels);
+ break;
+
+#ifdef VGM_USE_FFMPEG
+ case XWMA: { /* [Senxin Aleste (AC)] */
+ vgmstream->codec_data = init_ffmpeg_xwma(sf, psb.stream_offset, psb.stream_size, psb.format, psb.channels, psb.sample_rate, psb.avg_bitrate, psb.block_size);
+ if (!vgmstream->codec_data) goto fail;
+ vgmstream->coding_type = coding_FFmpeg;
+ vgmstream->layout_type = layout_none;
+
+ if (!vgmstream->num_samples) {
+ vgmstream->num_samples = xwma_dpds_get_samples(sf, psb.dpds_offset, psb.dpds_size, psb.channels, 0);
+ }
+
+ break;
+ }
+
+ case XMA2: { /* Sega Vintage Collection (X360) */
+ uint8_t buf[0x100];
+ size_t bytes;
+
+ bytes = ffmpeg_make_riff_xma_from_fmt_chunk(buf, sizeof(buf), psb.fmt_offset, psb.fmt_size, psb.stream_size, sf, 1);
+ vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf, bytes, psb.stream_offset, psb.stream_size);
+ if (!vgmstream->codec_data) goto fail;
+ vgmstream->coding_type = coding_FFmpeg;
+ vgmstream->layout_type = layout_none;
+
+ xma_fix_raw_samples(vgmstream, sf, psb.stream_offset, psb.stream_size, psb.fmt_offset, 1,1);
+ break;
+ }
+#endif
+
+ case DSP: /* Legend of Mana (Switch) */
+ case OPUSNX: /* Legend of Mana (Switch) */
+ default:
+ vgm_logi("PSB: not implemented (ignore)\n");
+ goto fail;
+ }
+
+ strncpy(vgmstream->stream_name, psb.readable_name, STREAM_NAME_SIZE);
+
+ if (!vgmstream_open_stream(vgmstream, sf, psb.stream_offset))
+ goto fail;
+ return vgmstream;
+
+fail:
+ close_vgmstream(vgmstream);
+ return NULL;
+}
+
+/*****************************************************************************/
+
+static int prepare_fmt(STREAMFILE* sf, psb_header_t* psb) {
+ uint32_t offset = psb->fmt_offset;
+ if (!offset)
+ return 1; /* other codec, probably */
+
+ psb->format = read_u16le(offset + 0x00,sf);
+ if (psb->format == 0x6601) { /* X360 */
+ psb->format = read_u16be(offset + 0x00,sf);
+ psb->channels = read_u16be(offset + 0x02,sf);
+ psb->sample_rate = read_u32be(offset + 0x04,sf);
+ xma2_parse_fmt_chunk_extra(sf,
+ offset,
+ &psb->loop_flag,
+ &psb->num_samples,
+ &psb->loop_start,
+ &psb->loop_end,
+ 1);
+ }
+ else {
+ psb->channels = read_u16le(offset + 0x02,sf);
+ psb->sample_rate = read_u32le(offset + 0x04,sf);
+ psb->avg_bitrate = read_u32le(offset + 0x08,sf);
+ psb->block_size = read_u16le(offset + 0x0c,sf);
+ psb->bps = read_u16le(offset + 0x0e,sf);
+ /* 0x10+ varies */
+
+ switch(psb->format) {
+ case 0x0002:
+ if (!msadpcm_check_coefs(sf, offset + 0x14))
+ goto fail;
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ return 1;
+fail:
+ return 0;
+}
+
+static int prepare_codec(STREAMFILE* sf, psb_header_t* psb) {
+ const char* spec = psb->tmp->spec;
+ const char* ext = psb->tmp->ext;
+
+ /* try fmt (most common) */
+ if (psb->format != 0) {
+ switch(psb->format) {
+ case 0x01:
+ psb->codec = PCM;
+ break;
+ case 0x02:
+ psb->codec = MSADPCM;
+ break;
+ case 0x161:
+ psb->codec = XWMA;
+ break;
+ case 0x166:
+ psb->codec = XMA2;
+ break;
+ default:
+ goto fail;
+ }
+ return 1;
+ }
+
+ /* try console strings */
+ if (!spec)
+ goto fail;
+
+ if (strcmp(spec, "nx") == 0) {
+ if (!ext)
+ goto fail;
+
+ if (strcmp(ext, ".opus") == 0) {
+ psb->codec = OPUSNX;
+ return 1;
+ }
+
+ if (strcmp(ext, ".adpcm") == 0) {
+ psb->codec = DSP;
+ return 1;
+ }
+ }
+
+ if (strcmp(spec, "ps3") == 0) {
+ psb->codec = RIFF_AT3;
+ return 1;
+ }
+
+ if (strcmp(spec, "vita") == 0) {
+ if (is_id32be(psb->stream_offset, sf, "RIFF"))
+ psb->codec = RIFF_AT9;
+ else
+ psb->codec = VAG;
+ return 1;
+ }
+
+fail:
+ vgm_logi("PSB: unknown codec (report)\n");
+ return 0;
+}
+
+
+static int prepare_name(psb_header_t* psb) {
+ char* buf = psb->readable_name;
+ int buf_size = sizeof(psb->readable_name);
+ const char* main_name = psb->tmp->voice;
+ const char* sub_name = psb->tmp->uniq;
+ int main_len;
+
+ if (!sub_name)
+ sub_name = psb->tmp->wav;
+ if (!sub_name)
+ sub_name = psb->tmp->file;
+
+ if (!main_name) /* shouldn't happen */
+ return 1;
+
+ /* sometimes we have main="bgm01", sub="bgm01.wav" = detect and ignore */
+ main_len = strlen(main_name);
+ if (sub_name && strncmp(main_name, sub_name, main_len) == 0) {
+ if (sub_name[main_len] == '\0' || strcmp(sub_name + main_len, ".wav") == 0)
+ sub_name = NULL;
+ }
+
+ if (sub_name) {
+ snprintf(buf, buf_size, "%s/%s", main_name, sub_name);
+ }
+ else {
+ snprintf(buf, buf_size, "%s", main_name);
+ }
+
+ return 1;
+}
+
+static int prepare_psb_extra(STREAMFILE* sf, psb_header_t* psb) {
+ if (!prepare_fmt(sf, psb))
+ goto fail;
+ if (!prepare_codec(sf, psb))
+ goto fail;
+ if (!prepare_name(psb))
+ goto fail;
+ return 1;
+fail:
+ return 0;
+}
+
+
+/* channelList is an array (N layers, though typically only mono codecs like DSP) of objects:
+ * - archData: resource offset (RIFF) or sub-object
+ * - data/fmt/loop/wav
+ * - data/ext/samprate
+ * - body/channelCount/ext/intro/loop/samprate [Legend of Mana (Switch)]
+ * - body: data/sampleCount/skipSampleCount, intro: data/sampleCount
+ * - data/dpds/fmt/wav/loop
+ * - pan: array [N.0 .. 0.N] (when N layers, in practice just a wonky L/R definition)
+ */
+static int parse_psb_channels(psb_header_t* psb, psb_node_t* nchans) {
+ int i;
+ psb_node_t nchan, narch, nsub, node;
+
+ psb->layers = psb_node_get_count(nchans);
+ if (psb->layers == 0) goto fail;
+ if (psb->layers > PSB_MAX_LAYERS) goto fail;
+
+ for (i = 0; i < psb->layers; i++) {
+ psb_data_t data;
+ psb_type_t type;
+
+ psb_node_by_index(nchans, i, &nchan);
+
+ /* try to get possible keys (without overwritting), results will be handled and validated later as combos get complex */
+ psb_node_by_key(&nchan, "archData", &narch);
+ type = psb_node_get_type(&narch);
+ switch (type) {
+ case PSB_TYPE_DATA: /* Sega Vintage Collection (PS3) */
+ data = psb_node_get_result(&narch).data;
+ psb->stream_offset = data.offset;
+ psb->stream_size = data.size;
+ break;
+
+ case PSB_TYPE_OBJECT: /* rest */
+ /* typically:
+ * - data + fmt + others
+ * - body {data + fmt} + intro {data + fmt} + others [Legend of Mana (Switch)]
+ */
+
+ data = psb_node_get_data(&narch, "data");
+ if (data.offset) {
+ psb->stream_offset = data.offset;
+ psb->stream_size = data.size;
+ }
+
+ data = psb_node_get_data(&narch, "fmt");
+ if (data.offset) {
+ psb->fmt_offset = data.offset;
+ psb->fmt_size = data.size;
+ }
+
+ if (psb_node_by_key(&narch, "loop", &node)) {
+ /* can be found as "false" with body+intro */
+ if (psb_node_get_type(&node) == PSB_TYPE_ARRAY) {
+ //todo improve
+ psb_node_by_index(&node, 0, &nsub);
+ psb->loop_start = psb_node_get_result(&nsub).num;
+
+ psb_node_by_index(&node, 1, &nsub);
+ psb->loop_end = psb_node_get_result(&nsub).num + psb->loop_start; /* duration */
+ }
+ }
+
+#if 0
+ if (psb_node_by_key(&narch, "body", &node)) {
+ data = psb_node_get_data(&node, "data");
+ psb->stream_offset = data.offset;
+ psb->stream_size = data.size;
+ psb->num_samples = psb_node_get_integer(&node, "sampleCount");
+ psb->skip_samples = psb_node_get_integer(&node, "skipSampleCount");
+ }
+
+ if (psb_node_by_key(&narch, "intro", &node)) {
+ data = psb_node_get_data(&node, "data");
+ psb->stream_offset = data.offset;
+ psb->stream_size = data.size;
+ psb->num_samples = psb_node_get_integer(&node, "sampleCount");
+ psb->skip_samples = psb_node_get_integer(&node, "skipSampleCount");
+ }
+#endif
+
+ data = psb_node_get_data(&narch, "dpds");
+ if (data.offset) {
+ psb->dpds_offset = data.offset;
+ psb->dpds_size = data.size;
+ }
+
+ psb->sample_rate = (int)psb_node_get_float(&narch, "samprate");
+
+ psb->tmp->wav = psb_node_get_string(&narch, "wav");
+
+ /* background: false?
+ */
+ break;
+
+ default:
+ goto fail;
+ }
+ }
+ return 1;
+fail:
+ VGM_LOG("psb: can't parse channel\n");
+ return 0;
+}
+
+
+/* parse a single archive, that can contain extra info here or inside channels */
+static int parse_psb_voice(psb_header_t* psb, psb_node_t* nvoice) {
+ psb_node_t nsong, nchans;
+
+
+ psb->total_subsongs = psb_node_get_count(nvoice);
+ if (psb->target_subsong == 0) psb->target_subsong = 1;
+ if (psb->total_subsongs <= 0 || psb->target_subsong > psb->total_subsongs) goto fail;
+
+
+ /* target voice and stream info */
+ if (!psb_node_by_index(nvoice, psb->target_subsong - 1, &nsong))
+ goto fail;
+ psb->tmp->voice = psb_node_get_key(nvoice, psb->target_subsong - 1);
+
+ psb_node_by_key(&nsong, "channelList", &nchans);
+ if (!parse_psb_channels(psb, &nchans))
+ goto fail;
+
+
+ /* unsure of meaning but must exist (usually 0/1) */
+ if (psb_node_exists(&nsong, "device") <= 0)
+ goto fail;
+
+ /* names (optional) */
+ psb->tmp->file = psb_node_get_string(&nsong, "file");
+ psb->tmp->uniq = psb_node_get_string(&nsong, "uniq");
+
+ /* optional loop flag (loop points go in channels, or implicit in fmt/RIFF) */
+ if (!psb->loop_flag) {
+ psb->loop_flag = psb_node_get_integer(&nsong, "loop") > 1;
+ /* There is also loopstr/loopStr = "all" when "loop"=2 and "none" when "loop"=0
+ * SFX set loop=0, and sometimes songs that look like they could do full loops do too */
+ }
+
+ /* other optional keys:
+ * - quality: ? (1=MSADPCM, 2=OPUSNX/PCM)
+ * - priority: f32, -1.0, 1.0 or 10.0 = max?
+ * - type: 0/1? (internal classification?)
+ * - volume: 0.0 .. 1.0
+ * - group?
+ */
+
+ return 1;
+fail:
+ VGM_LOG("psb: can't parse voice\n");
+ return 0;
+}
+
+/* .psb is binary JSON-like structure that can be used to hold various formats, we want audio data:
+ * - (root): (object)
+ * - "id": (format string)
+ * - "spec": (platform string)
+ * - "version": (float)
+ * - "voice": (objects, one per subsong)
+ * - (voice name 1): (object)
+ * - "channelList": (array of N objects)
+ * - "archData": (main audio part, varies per game/platform/codec)
+ * - "device": ?
+ * ...
+ * - (voice name N): ...
+ * From decompilations, audio code reads common keys up to "archData", then depends on game (not unified).
+ * Keys are (seemingly) stored in text order.
+ */
+static int parse_psb(STREAMFILE* sf, psb_header_t* psb) {
+ psb_temp_t tmp;
+ psb_context_t* ctx = NULL;
+ psb_node_t nroot, nvoice;
+ float version;
+
+ psb->tmp = &tmp;
+ psb->target_subsong = sf->stream_index;
+
+ ctx = psb_init(sf);
+ if (!ctx) goto fail;
+ //psb_print(ctx);
+
+ /* main process */
+ psb_get_root(ctx, &nroot);
+
+ /* format definition, non-audio IDs include "motion", "font", or no "id" at all */
+ psb->tmp->id = psb_node_get_string(&nroot, "id");
+ if (!psb->tmp->id || strcmp(psb->tmp->id, "sound_archive") != 0) {
+ /* "sound" is just a list of available "sound_archive" */
+ if (psb->tmp->id && strcmp(psb->tmp->id, "sound") == 0)
+ vgm_logi("PSB: empty archive type '%s' (ignore)\n", psb->tmp->id);
+ else
+ vgm_logi("PSB: unsupported archive type '%s' (ignore?)\n", psb->tmp->id);
+ goto fail;
+ }
+
+ /* platform: x360/ps3/win/nx/etc */
+ psb->tmp->spec = psb_node_get_string(&nroot, "spec");
+
+ /* enforced by M2 code */
+ version = psb_node_get_float(&nroot, "version");
+ if (version < 1.02f || version > 1.02f) {
+ vgm_logi("PSB: unsupported version %f (report)\n", version);
+ goto fail;
+ }
+
+ /* main subsong */
+ psb_node_by_key(&nroot, "voice", &nvoice);
+ if (!parse_psb_voice(psb, &nvoice))
+ goto fail;
+
+ /* post stuff before closing PSB */
+ if (!prepare_psb_extra(sf, psb))
+ goto fail;
+
+ psb->tmp = NULL;
+ psb_close(ctx);
+ return 1;
+fail:
+ psb_close(ctx);
+ VGM_LOG("psb: can't parse PSB\n");
+ return 0;
+}
+
+#if 0
+typedef struct {
+ void* init;
+ const char* id32;
+ const char* exts;
+} metadef_t;
+
+metadef_t md_psb = {
+ .init = init_vgmstream_psb,
+ .exts = "psb",
+ .id32 = "PSB\0", //24b/masked IDs?
+ .id32 = get_id32be("PSB\0"), //???
+ .idfn = psb_check_id,
+}
+#endif
diff --git a/src/util/m2_psb.c b/src/util/m2_psb.c
new file mode 100644
index 00000000..e1fc35bf
--- /dev/null
+++ b/src/util/m2_psb.c
@@ -0,0 +1,847 @@
+#include
+#include "m2_psb.h"
+#include "../util.h"
+#include "log.h"
+
+/* Code below roughly follows original m2lib internal API b/c why not. Rather than pre-parsing the tree
+ * to struct/memory, seems it re-reads bytes from buf as needed (there might be some compiler optims going on too).
+ * Always LE even on X360.
+ *
+ * Info from: decompiled exes and parts (mainly key decoding) from exm2lib by asmodean (http://asmodean.reverse.net/),
+ * also https://github.com/number201724/psbfile and https://github.com/UlyssesWu/FreeMote
+ *
+ * PSB defines a header with offsets to sections within the header, binary format being type-value (where type could be
+ * int8/int16/float/array/object/etc). Example:
+ * 21 // object: root (x2 info lists + items)
+ * 0D 04 0D 06,0B,0D,0E // list8[4]: key indexes ("id/spec/version/voice")
+ * 0D 04 0D 00,02,04,09 // list8[4]: byte offsets of next 4 items
+ * 15 02 // 0 string8: string#2 ("spec")
+ * 1E 5C8F823F // 1 float32: 1.02
+ * 05 02 // 2 int8: 2
+ * 21 // 3 object
+ * 0D 02 0D 02,05 // list8[2]: key indexes
+ * 0D 02 0D 00,02 // list8[2]: byte offsets
+ * 19 00 // 0 resource8: resource#0 (subfile)
+ * 20 // 1 array: loops
+ * 0D 02 0D 00,04 // list8[2]
+ * 07 D69107 // 0 int24
+ * 07 31A45C // 1 int24
+ */
+//TODO: som
+//TODO: add validations on buf over max size
+//TODO: validate strings table ends with null (buf[max - 1] = '\0')
+
+/******************************************************************************/
+/* DEFS */
+
+#define PSB_VERSION2 2 /* older (x360/ps3) games */
+#define PSB_VERSION3 3 /* current games */
+#define PSB_MAX_HEADER 0x40000 /* max seen ~0x1000 */
+
+
+/* Internal type used in binary data, that defines bytes used to store value.
+ * A common optimization is (type - base-1) to convert to used bytes (like NUMBER_16 - 0x04 = 2).
+ * Often M2 code seems to ignore max sizes and casts to int32, no concept of signed/unsigned either.
+ * Sometimes M2 code converts to external type to do general checks too. */
+typedef enum {
+ PSB_ITYPE_NONE = 0x0,
+
+ PSB_ITYPE_NULL = 0x1,
+
+ PSB_ITYPE_TRUE = 0x2,
+ PSB_ITYPE_FALSE = 0x3,
+
+ PSB_ITYPE_INTEGER_0 = 0x4,
+ PSB_ITYPE_INTEGER_8 = 0x5,
+ PSB_ITYPE_INTEGER_16 = 0x6,
+ PSB_ITYPE_INTEGER_24 = 0x7,
+ PSB_ITYPE_INTEGER_32 = 0x8,
+ PSB_ITYPE_INTEGER_40 = 0x9, /* assumed, decomp does same as 32b due to int cast (compiler over-optimization?) */
+ PSB_ITYPE_INTEGER_48 = 0xA, /* same */
+ PSB_ITYPE_INTEGER_56 = 0xB,
+ PSB_ITYPE_INTEGER_64 = 0xC,
+
+ PSB_ITYPE_LIST_8 = 0xD,
+ PSB_ITYPE_LIST_16 = 0xE,
+ PSB_ITYPE_LIST_24 = 0xF,
+ PSB_ITYPE_LIST_32 = 0x10,
+ PSB_ITYPE_LIST_40 = 0x11, /* assumed, no refs in code (same up to 64) */
+ PSB_ITYPE_LIST_48 = 0x12,
+ PSB_ITYPE_LIST_56 = 0x13,
+ PSB_ITYPE_LIST_64 = 0x14,
+
+ PSB_ITYPE_STRING_8 = 0x15,
+ PSB_ITYPE_STRING_16 = 0x16,
+ PSB_ITYPE_STRING_24 = 0x17,
+ PSB_ITYPE_STRING_32 = 0x18,
+
+ PSB_ITYPE_DATA_8 = 0x19,
+ PSB_ITYPE_DATA_16 = 0x1A,
+ PSB_ITYPE_DATA_24 = 0x1B,
+ PSB_ITYPE_DATA_32 = 0x1C,
+ PSB_ITYPE_DATA_40 = 0x22, /* assumed, some refs in code (same up to 64) */
+ PSB_ITYPE_DATA_48 = 0x23,
+ PSB_ITYPE_DATA_56 = 0x24,
+ PSB_ITYPE_DATA_64 = 0x25,
+
+ PSB_ITYPE_FLOAT_0 = 0x1D,
+ PSB_ITYPE_FLOAT_32 = 0x1E,
+ PSB_ITYPE_DOUBLE_64 = 0x1F,
+
+ PSB_ITYPE_ARRAY = 0x20,
+ PSB_ITYPE_OBJECT = 0x21,
+} psb_itype_t;
+
+
+typedef struct {
+ int bytes; /* total bytes (including headers) to skip this list */
+ int count; /* number of entries */
+ int esize; /* size per entry */
+ uint8_t* edata; /* start of entries */
+} list_t;
+
+struct psb_context_t {
+ uint32_t header_id;
+ uint16_t version;
+ uint16_t encrypt_value;
+ uint32_t encrypt_offset;
+ uint32_t keys_offset;
+
+ uint32_t strings_list_offset;
+ uint32_t strings_data_offset;
+ uint32_t data_offsets_offset; //todo resources
+ uint32_t data_sizes_offset;
+
+ uint32_t data_offset; //todo resources
+ uint32_t root_offset;
+ uint32_t unknown; /* hash/crc? (v3) */
+
+ /* main buf and derived stuff*/
+ uint8_t* buf;
+ list_t strings_list;
+ uint8_t* strings_data;
+
+ list_t data_offsets_list;
+ list_t data_sizes_list;
+
+ /* keys buf */
+ char* keys;
+ int* keys_pos;
+ int keys_count;
+};
+
+/******************************************************************************/
+/* COMMON */
+
+/* output seems to be signed but some of M2 code casts to unsigned, not sure if important for indexes (known cases never get too high) */
+static uint32_t item_get_int(int size, uint8_t* buf) {
+ switch (size) {
+ case 1:
+ return get_u8(buf);
+ case 2:
+ return get_u16le(buf);
+ case 3:
+ return (get_u16le(buf+0x01) << 8) | get_u8(buf);
+ //return get_u24le(buf+0x01);
+ case 4:
+ return get_u32le(buf);
+ default:
+ return 0;
+ }
+}
+
+static int list_get_count(uint8_t* buf) {
+ uint8_t itype = buf[0];
+ switch (itype) {
+ case PSB_ITYPE_LIST_8:
+ case PSB_ITYPE_LIST_16:
+ case PSB_ITYPE_LIST_24:
+ case PSB_ITYPE_LIST_32: {
+ int size = itype - PSB_ITYPE_LIST_8 + 1;
+ return item_get_int(size, &buf[1]);
+ }
+ default:
+ return 0;
+ }
+}
+
+static uint32_t list_get_entry(list_t* lst, uint32_t index) {
+ uint8_t* buf = &lst->edata[index * lst->esize];
+ return item_get_int(lst->esize, buf);
+}
+
+static int list_init(list_t* lst, uint8_t* buf) {
+ int count_size, count, entry_size;
+ uint8_t count_itype, entry_itype;
+
+ /* ex. 0D 04 0D 00,01,02,03 */
+
+ /* get count info (0D + 04) */
+ count_itype = buf[0];
+ switch (count_itype) {
+ case PSB_ITYPE_LIST_8:
+ case PSB_ITYPE_LIST_16:
+ case PSB_ITYPE_LIST_24:
+ case PSB_ITYPE_LIST_32:
+ count_size = count_itype - PSB_ITYPE_LIST_8 + 1;
+ count = item_get_int(count_size, &buf[1]);
+ break;
+ default:
+ goto fail;
+ }
+
+ /* get entry info (0D + 00,01,02,03) */
+ entry_itype = buf[1 + count_size];
+ switch (entry_itype) {
+ case PSB_ITYPE_LIST_8:
+ case PSB_ITYPE_LIST_16:
+ case PSB_ITYPE_LIST_24:
+ case PSB_ITYPE_LIST_32:
+ entry_size = entry_itype - PSB_ITYPE_LIST_8 + 1;
+ break;
+ default:
+ goto fail;
+ }
+
+ lst->bytes = 1 + count_size + 1 + entry_size * count;
+ lst->count = count;
+ lst->esize = entry_size;
+ lst->edata = &buf[1 + count_size + 1];
+ return 1;
+fail:
+ memset(lst, 0, sizeof(list_t));
+ return 0;
+}
+
+/* when a function that should modify p_out fails, memset just in case wasn't init and p_out is chained */
+static void node_error(psb_node_t* p_out) {
+ if (!p_out)
+ return;
+ p_out->ctx = NULL;
+ p_out->data = NULL;
+}
+
+
+/******************************************************************************/
+/* INIT */
+
+
+/* Keys seems to use a kind of linked list where each element points to next and char is encoded
+ * with a distance-based metric. Notice it's encoded in reverse order, so it's tuned to save
+ * common prefixes (like bgmXXX in big archives). Those aren't that common, and to encode N chars
+ * often needs x2/x3 bytes (and it's slower) so it's probably more of a form of obfuscation. */
+int decode_key(list_t* kidx1, list_t* kidx2, list_t* kidx3, char* str, int str_len, int index) {
+ int i;
+
+ uint32_t entry_point = list_get_entry(kidx3, index);
+ uint32_t point = list_get_entry(kidx2, entry_point);
+
+ for (i = 0; i < str_len; i++) {
+ uint32_t next = list_get_entry(kidx2, point);
+ uint32_t diff = list_get_entry(kidx1, next);
+ uint32_t curr = point - diff;
+
+ str[i] = (char)curr;
+
+ point = next;
+ if (!point)
+ break;
+ }
+
+ if (i == str_len) {
+ vgm_logi("PSBLIB: truncated key (report)\n");
+ }
+ else {
+ i++;
+ }
+
+ str[i] = '\0';
+ return i;
+}
+
+/* Keys are packed in a particular format (see get_key_string), and M2 code seems to do some unknown
+ * pre-parse, so for now do a simple copy to string buf to simplify handling and returning. */
+int init_keys(psb_context_t* ctx) {
+ list_t kidx1, kidx2, kidx3;
+ uint8_t* buf = &ctx->buf[ctx->keys_offset];
+ int i, j, pos;
+ char key[256]; /* ~50 aren't too uncommon (used in names) */
+ int keys_size;
+
+
+ /* character/diff table */
+ if (!list_init(&kidx1, &buf[0]))
+ goto fail;
+ /* next point table */
+ if (!list_init(&kidx2, &buf[kidx1.bytes]))
+ goto fail;
+ /* entry point table */
+ if (!list_init(&kidx3, &buf[kidx1.bytes + kidx2.bytes]))
+ goto fail;
+
+ ctx->keys_count = kidx3.count;
+ ctx->keys_pos = malloc(sizeof(int) * ctx->keys_count);
+ if (!ctx->keys_pos) goto fail;
+
+
+ /* packed lists are usually *bigger* than final raw strings, but put some extra size just in case */
+ keys_size = (kidx1.bytes + kidx2.bytes + kidx3.bytes) * 2;
+ ctx->keys = malloc(keys_size);
+ if (!ctx->keys) goto fail;
+
+ pos = 0;
+ for (i = 0; i < kidx3.count; i++) {
+ int key_len = decode_key(&kidx1, &kidx2, &kidx3, key, sizeof(key), i);
+
+ /* could realloc but meh */
+ if (pos + key_len > keys_size)
+ goto fail;
+
+ /* copy key in reverse (strrev + memcpy C99 only) */
+ for (j = 0; j < key_len; j++) {
+ ctx->keys[pos + key_len - 1 - j] = key[j];
+ }
+ ctx->keys[pos + key_len] = '\0';
+
+ ctx->keys_pos[i] = pos;
+
+ pos += key_len + 1;
+ }
+
+ return 1;
+fail:
+ vgm_logi("PSBLIB: failed getting keys\n");
+ return 0;
+}
+
+psb_context_t* psb_init(STREAMFILE* sf) {
+ psb_context_t* ctx;
+ uint8_t header[0x2c];
+ int bytes;
+ uint32_t buf_len;
+
+ ctx = calloc(1, sizeof(psb_context_t));
+ if (!ctx) goto fail;
+
+ bytes = read_streamfile(header, 0x00, sizeof(header), sf);
+ if (bytes != sizeof(header)) goto fail;
+
+ ctx->header_id = get_u32be(header + 0x00);
+ ctx->version = get_u16le(header + 0x04);
+ ctx->encrypt_value = get_u32le(header + 0x06);
+ ctx->encrypt_offset = get_u32le(header + 0x08);
+ ctx->keys_offset = get_u32le(header + 0x0c);
+
+ ctx->strings_list_offset = get_u32le(header + 0x10);
+ ctx->strings_data_offset = get_u32le(header + 0x14);
+ ctx->data_offsets_offset = get_u32le(header + 0x18);
+ ctx->data_sizes_offset = get_u32le(header + 0x1c);
+
+ ctx->data_offset = get_u32le(header + 0x20);
+ ctx->root_offset = get_u32le(header + 0x24);
+ if (ctx->version >= PSB_VERSION3)
+ ctx->unknown = get_u32le(header + 0x28);
+
+ /* some validations, not sure if checked by M2 */
+ if (ctx->header_id != get_id32be("PSB\0"))
+ goto fail;
+ if (ctx->version != PSB_VERSION2 && ctx->version != PSB_VERSION3)
+ goto fail;
+
+ /* not seen */
+ if (ctx->encrypt_value != 0)
+ goto fail;
+ /* 0 in some v2 */
+ if (ctx->encrypt_offset != 0 && ctx->encrypt_offset != ctx->keys_offset)
+ goto fail;
+
+ /* data should be last as it's used to read buf */
+ if (ctx->keys_offset >= ctx->data_offset ||
+ ctx->strings_list_offset >= ctx->data_offset ||
+ ctx->strings_data_offset >= ctx->data_offset ||
+ ctx->data_offsets_offset >= ctx->data_offset ||
+ ctx->data_sizes_offset >= ctx->data_offset ||
+ ctx->root_offset >= ctx->data_offset)
+ goto fail;
+
+ /* copy data for easier access */
+ buf_len = ctx->data_offset;
+ if (buf_len > PSB_MAX_HEADER)
+ goto fail;
+
+ ctx->buf = malloc(buf_len);
+ if (!ctx->buf) goto fail;
+
+ bytes = read_streamfile(ctx->buf, 0x00, buf_len, sf);
+ if (bytes != buf_len) goto fail;
+
+ if (!list_init(&ctx->strings_list, &ctx->buf[ctx->strings_list_offset]))
+ goto fail;
+ ctx->strings_data = &ctx->buf[ctx->strings_data_offset];
+
+ if (!list_init(&ctx->data_offsets_list, &ctx->buf[ctx->data_offsets_offset]))
+ goto fail;
+ if (!list_init(&ctx->data_sizes_list, &ctx->buf[ctx->data_sizes_offset]))
+ goto fail;
+
+ if (!init_keys(ctx))
+ goto fail;
+
+ return ctx;
+fail:
+ psb_close(ctx);
+ vgm_logi("PSBLIB: init error (report)\n");
+ return NULL;
+}
+
+void psb_close(psb_context_t* ctx) {
+ if (!ctx)
+ return;
+
+ free(ctx->keys_pos);
+ free(ctx->keys);
+ free(ctx->buf);
+ free(ctx);
+}
+
+int psb_get_root(psb_context_t* ctx, psb_node_t* p_root) {
+ if (!ctx || !p_root)
+ return 0;
+ p_root->ctx = ctx;
+ p_root->data = &ctx->buf[ctx->root_offset];
+
+ return 1;
+}
+
+
+/******************************************************************************/
+/* NODES */
+
+psb_type_t psb_node_get_type(const psb_node_t* node) {
+ uint8_t* buf;
+ uint8_t itype;
+
+ if (!node || !node->data)
+ goto fail;
+
+ buf = node->data;
+ itype = buf[0];
+ switch (itype) {
+ case PSB_ITYPE_NULL:
+ return PSB_TYPE_NULL;
+
+ case PSB_ITYPE_TRUE:
+ case PSB_ITYPE_FALSE:
+ return PSB_TYPE_BOOL;
+
+ case PSB_ITYPE_INTEGER_0:
+ case PSB_ITYPE_INTEGER_8:
+ case PSB_ITYPE_INTEGER_16:
+ case PSB_ITYPE_INTEGER_24:
+ case PSB_ITYPE_INTEGER_32:
+ case PSB_ITYPE_INTEGER_40:
+ case PSB_ITYPE_INTEGER_48:
+ case PSB_ITYPE_INTEGER_56:
+ case PSB_ITYPE_INTEGER_64:
+ return PSB_TYPE_INTEGER;
+
+ case PSB_ITYPE_STRING_8:
+ case PSB_ITYPE_STRING_16:
+ case PSB_ITYPE_STRING_24:
+ case PSB_ITYPE_STRING_32:
+ return PSB_TYPE_STRING;
+
+ case PSB_ITYPE_DATA_8:
+ case PSB_ITYPE_DATA_16:
+ case PSB_ITYPE_DATA_24:
+ case PSB_ITYPE_DATA_32:
+ case PSB_ITYPE_DATA_40:
+ case PSB_ITYPE_DATA_48:
+ case PSB_ITYPE_DATA_56:
+ case PSB_ITYPE_DATA_64:
+ return PSB_TYPE_DATA;
+
+ case PSB_ITYPE_FLOAT_0:
+ case PSB_ITYPE_FLOAT_32:
+ case PSB_ITYPE_DOUBLE_64:
+ return PSB_TYPE_FLOAT;
+
+ case PSB_ITYPE_ARRAY:
+ return PSB_TYPE_ARRAY;
+
+ case PSB_ITYPE_OBJECT:
+ return PSB_TYPE_OBJECT;
+
+ /* M2 just aborts for other internal types (like lists) */
+ default:
+ goto fail;
+ }
+
+fail:
+ return PSB_TYPE_UNKNOWN;
+}
+
+int psb_node_get_count(const psb_node_t* node) {
+ uint8_t* buf;
+
+ if (!node || !node->data)
+ goto fail;
+
+ buf = node->data;
+ switch (buf[0]) {
+ case PSB_ITYPE_ARRAY:
+ case PSB_ITYPE_OBJECT:
+ /* both start with a list, that can be used as count */
+ return list_get_count(&buf[1]);
+ default:
+ return 0;
+ }
+fail:
+ return -1;
+}
+
+int psb_node_by_index(const psb_node_t* node, int index, psb_node_t* p_out) {
+ uint8_t* buf;
+
+ if (!node || !node->data)
+ goto fail;
+
+ buf = node->data;
+ switch (buf[0]) {
+ case PSB_ITYPE_ARRAY: {
+ list_t offsets;
+ int skip;
+
+ list_init(&offsets, &buf[1]);
+ skip = list_get_entry(&offsets, index);
+
+ p_out->ctx = node->ctx;
+ p_out->data = &buf[1 + offsets.bytes + skip];
+ return 1;
+ }
+
+ case PSB_ITYPE_OBJECT: {
+ list_t keys, offsets;
+ int skip;
+
+ list_init(&keys, &buf[1]);
+ list_init(&offsets, &buf[1 + keys.bytes]);
+ skip = list_get_entry(&offsets, index);
+
+ p_out->ctx = node->ctx;
+ p_out->data = &buf[1 + keys.bytes + offsets.bytes + skip];
+ return 1;
+ }
+
+ default:
+ goto fail;
+ }
+fail:
+ vgm_logi("PSBLIB: cannot get node at index %i\n", index);
+ node_error(p_out);
+ return 0;
+}
+
+
+int psb_node_by_key(const psb_node_t* node, const char* key, psb_node_t* p_out) {
+ int i;
+ int max;
+
+ if (!node || !node->ctx)
+ goto fail;
+
+ max = psb_node_get_count(node);
+ if (max < 0 || max > node->ctx->keys_count)
+ goto fail;
+
+ for (i = 0; i < max; i++) {
+ const char* key_test = psb_node_get_key(node, i);
+ if (!key_test)
+ goto fail;
+
+ //todo could improve by getting strlen(key) + ctx->key_len + check + strncmp
+ if (strcmp(key_test, key) == 0)
+ return psb_node_by_index(node, i, p_out);
+ }
+
+fail:
+ //VGM_LOG("psblib: cannot get node at key '%s'\n", key); /* not uncommon to query */
+ node_error(p_out);
+ return 0;
+}
+
+
+const char* psb_node_get_key(const psb_node_t* node, int index) {
+ uint8_t* buf;
+ int pos;
+
+ if (!node || !node->ctx || !node->data)
+ goto fail;
+
+ buf = node->data;
+ switch (buf[0]) {
+ case PSB_ITYPE_OBJECT: {
+ list_t keys;
+ int keys_index;
+
+ list_init(&keys, &buf[1]);
+ keys_index = list_get_entry(&keys, index);
+ if (keys_index < 0 || keys_index > node->ctx->keys_count)
+ goto fail;
+
+ pos = node->ctx->keys_pos[keys_index];
+ return &node->ctx->keys[pos];
+ }
+
+ default:
+ goto fail;
+ }
+
+fail:
+ vgm_logi("PSBLIB: cannot get key at index '%i'\n", index);
+ return NULL;
+}
+
+
+psb_result_t psb_node_get_result(psb_node_t* node) {
+ uint8_t* buf;
+ uint8_t itype;
+ psb_result_t res = {0};
+ int size, index, skip;
+
+ if (!node || !node->ctx || !node->data)
+ goto fail;
+
+ buf = node->data;
+ itype = buf[0];
+ switch (itype) {
+ case PSB_ITYPE_NULL:
+ break;
+
+ case PSB_ITYPE_TRUE:
+ case PSB_ITYPE_FALSE:
+ res.bln = (itype == PSB_ITYPE_TRUE);
+ break;
+
+ case PSB_ITYPE_INTEGER_0:
+ res.num = 0;
+ break;
+
+ case PSB_ITYPE_INTEGER_8:
+ case PSB_ITYPE_INTEGER_16:
+ case PSB_ITYPE_INTEGER_24:
+ case PSB_ITYPE_INTEGER_32:
+ size = itype - PSB_ITYPE_INTEGER_8 + 1;
+
+ res.num = item_get_int(size, &buf[1]);
+ break;
+
+ case PSB_ITYPE_INTEGER_40:
+ case PSB_ITYPE_INTEGER_48:
+ case PSB_ITYPE_INTEGER_56:
+ case PSB_ITYPE_INTEGER_64:
+ vgm_logi("PSBLIB: not implemented (report)\n");
+ break;
+
+ case PSB_ITYPE_STRING_8:
+ case PSB_ITYPE_STRING_16:
+ case PSB_ITYPE_STRING_24:
+ case PSB_ITYPE_STRING_32: {
+ size = itype - PSB_ITYPE_STRING_8 + 1;
+ index = item_get_int(size, &buf[1]);
+ skip = list_get_entry(&node->ctx->strings_list, index);
+
+ res.str = (const char*)&node->ctx->strings_data[skip]; /* null-terminated */
+ //todo test max strlen to see if it's null-terminated
+ break;
+ }
+
+ case PSB_ITYPE_DATA_8:
+ case PSB_ITYPE_DATA_16:
+ case PSB_ITYPE_DATA_24:
+ case PSB_ITYPE_DATA_32:
+ size = itype - PSB_ITYPE_DATA_8 + 1;
+ index = item_get_int(size, &buf[1]);
+
+ res.data.offset = list_get_entry(&node->ctx->data_offsets_list, index);
+ res.data.size = list_get_entry(&node->ctx->data_sizes_list, index);
+
+ res.data.offset += node->ctx->data_offset;
+ break;
+
+ case PSB_ITYPE_DATA_40:
+ case PSB_ITYPE_DATA_48:
+ case PSB_ITYPE_DATA_56:
+ case PSB_ITYPE_DATA_64:
+ vgm_logi("PSBLIB: not implemented (report)\n");
+ break;
+
+ case PSB_ITYPE_FLOAT_0:
+ res.flt = 0.0f;
+ break;
+
+ case PSB_ITYPE_FLOAT_32:
+ res.flt = get_f32le(&buf[1]);
+ break;
+
+ case PSB_ITYPE_DOUBLE_64:
+ res.dbl = get_d64le(&buf[1]);
+ res.flt = (float)res.dbl; /* doubles seem ignored */
+ break;
+
+ case PSB_ITYPE_ARRAY:
+ case PSB_ITYPE_OBJECT:
+ res.count = list_get_count(&buf[1]);
+ break;
+
+ default:
+ goto fail;
+ }
+
+ return res;
+fail:
+ return res; /* should be all null */
+
+}
+
+/******************************************************************************/
+/* HELPERS */
+
+static int get_expected_node(const psb_node_t* node, const char* key, psb_node_t* p_out, psb_type_t expected) {
+ if (!psb_node_by_key(node, key, p_out))
+ goto fail;
+ if (psb_node_get_type(p_out) != expected)
+ goto fail;
+ return 1;
+fail:
+ return 0;
+}
+
+
+/* M2 coerces values (like float to bool) but it's kinda messy so whatevs */
+const char* psb_node_get_string(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!get_expected_node(node, key, &out, PSB_TYPE_STRING))
+ return NULL;
+ return psb_node_get_result(&out).str;
+}
+
+float psb_node_get_float(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!get_expected_node(node, key, &out, PSB_TYPE_FLOAT))
+ return 0.0f;
+ return psb_node_get_result(&out).flt;
+}
+
+int32_t psb_node_get_integer(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!get_expected_node(node, key, &out, PSB_TYPE_INTEGER))
+ return 0;
+ return psb_node_get_result(&out).num;
+}
+
+int psb_node_get_bool(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!get_expected_node(node, key, &out, PSB_TYPE_BOOL))
+ return 0;
+ return psb_node_get_result(&out).bln;
+}
+
+psb_data_t psb_node_get_data(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!get_expected_node(node, key, &out, PSB_TYPE_DATA)) {
+ psb_data_t data = {0};
+ return data;
+ }
+ return psb_node_get_result(&out).data;
+
+}
+int psb_node_exists(const psb_node_t* node, const char* key) {
+ psb_node_t out;
+ if (!psb_node_by_key(node, key, &out))
+ return 0;
+ return 1;
+}
+
+
+/******************************************************************************/
+/* ETC */
+
+#define PSB_DEPTH_STEP 2
+
+static void print_internal(psb_node_t* curr, int depth) {
+ int i;
+ psb_node_t node;
+ const char* key;
+ psb_type_t type;
+ psb_result_t res;
+
+ if (!curr)
+ return;
+
+ type = psb_node_get_type(curr);
+ res = psb_node_get_result(curr);
+ switch (type) {
+ case PSB_TYPE_NULL:
+ printf("%s,\n", "null");
+ break;
+
+ case PSB_TYPE_BOOL:
+ printf("%s,\n", (res.bln == 1 ? "true" : "false"));
+ break;
+
+ case PSB_TYPE_INTEGER:
+ printf("%i,\n", res.num);
+ break;
+
+ case PSB_TYPE_FLOAT:
+ printf("%f,\n", res.flt);
+ break;
+
+ case PSB_TYPE_STRING:
+ printf("\"%s\",\n", res.str);
+ break;
+
+ case PSB_TYPE_DATA:
+ printf("<0x%08x,0x%08x>\n", res.data.offset, res.data.size);
+ break;
+
+ case PSB_TYPE_ARRAY:
+ printf("[\n");
+
+ for (i = 0; i < res.count; i++) {
+ psb_node_by_index(curr, i, &node);
+
+ printf("%*s", depth + PSB_DEPTH_STEP, "");
+ print_internal(&node, depth + PSB_DEPTH_STEP);
+ }
+
+ printf("%*s],\n", depth, "");
+ break;
+
+ case PSB_TYPE_OBJECT:
+ printf("{\n");
+
+ for (i = 0; i < res.count; i++) {
+ key = psb_node_get_key(curr, i);
+ psb_node_by_index(curr, i, &node);
+
+ printf("%*s\"%s\": ", depth + PSB_DEPTH_STEP, "", key);
+ print_internal(&node, depth + PSB_DEPTH_STEP);
+ }
+
+ printf("%*s},\n", depth, "");
+ break;
+
+ default:
+ printf("???,\n");
+ break;
+ }
+}
+
+void psb_print(psb_context_t* ctx) {
+ psb_node_t node;
+
+ psb_get_root(ctx, &node);
+ print_internal(&node, 0);
+}
diff --git a/src/util/m2_psb.h b/src/util/m2_psb.h
new file mode 100644
index 00000000..fe0d209b
--- /dev/null
+++ b/src/util/m2_psb.h
@@ -0,0 +1,88 @@
+#ifndef _M2_PSB_H_
+#define _M2_PSB_H_
+
+#include "../streamfile.h"
+
+/* M2's PSB (Packaged Struct Binary) is binary format similar to JSON with a tree-like structure of
+ * string keys = multitype values (objects, arrays, bools, strings, ints, raw data and so on)
+ * but better packed (like support of ints of all sizes).
+ *
+ * It's used to access values in different M2 formats, including audio containers (MSound::SoundArchive)
+ * so rather than data accessing by offsets they just use "key" = values.
+ */
+
+
+/* opaque struct */
+typedef struct psb_context_t psb_context_t;
+
+/* represents an object in the tree */
+typedef struct {
+ psb_context_t* ctx;
+ void* data;
+} psb_node_t;
+
+
+/* open a PSB */
+psb_context_t* psb_init(STREAMFILE* sf);
+void psb_close(psb_context_t* ctx);
+
+/* get base root object */
+int psb_get_root(psb_context_t* ctx, psb_node_t* p_root);
+
+typedef enum {
+ PSB_TYPE_NULL = 0x0,
+ PSB_TYPE_BOOL = 0x1,
+ PSB_TYPE_INTEGER = 0x2,
+ PSB_TYPE_FLOAT = 0x3,
+ PSB_TYPE_STRING = 0x4,
+ PSB_TYPE_DATA = 0x5, /* possibly "userdata" */
+ PSB_TYPE_ARRAY = 0x6,
+ PSB_TYPE_OBJECT = 0x7, /* also "table" */
+ PSB_TYPE_UNKNOWN = 0x8, /* error */
+} psb_type_t;
+
+/* get current type */
+psb_type_t psb_node_get_type(const psb_node_t* node);
+
+/* get item count (valid for 'array/object' nodes) */
+int psb_node_get_count(const psb_node_t* node);
+
+/* get key string of sub-node N (valid for 'object' node) */
+const char* psb_node_get_key(const psb_node_t* node, int index);
+
+/* get sub-node from node at index (valid for 'array/object') */
+int psb_node_by_index(const psb_node_t* node, int index, psb_node_t* p_out);
+
+/* get sub-node from node at key (valid for 'object') */
+int psb_node_by_key(const psb_node_t* node, const char* key, psb_node_t* p_out);
+
+typedef struct {
+ uint32_t offset;
+ uint32_t size;
+} psb_data_t;
+
+typedef union {
+ int bln;
+ int32_t num;
+ double dbl;
+ float flt;
+ const char* str;
+ int count;
+ psb_data_t data;
+} psb_result_t;
+
+/* generic result (returns all to 0 on failure) */
+psb_result_t psb_node_get_result(psb_node_t* node);
+
+/* helpers */
+const char* psb_node_get_string(const psb_node_t* node, const char* key);
+float psb_node_get_float(const psb_node_t* node, const char* key);
+int32_t psb_node_get_integer(const psb_node_t* node, const char* key);
+int psb_node_get_bool(const psb_node_t* node, const char* key);
+psb_data_t psb_node_get_data(const psb_node_t* node, const char* key);
+int psb_node_exists(const psb_node_t* node, const char* key);
+
+/* print in JSON-style (for debugging) */
+void psb_print(psb_context_t* ctx);
+
+#endif
diff --git a/src/vgmstream.c b/src/vgmstream.c
index aef983f3..fbebc29c 100644
--- a/src/vgmstream.c
+++ b/src/vgmstream.c
@@ -528,6 +528,7 @@ VGMSTREAM* (*init_vgmstream_functions[])(STREAMFILE* sf) = {
init_vgmstream_wxd_wxh,
init_vgmstream_bnk_relic,
init_vgmstream_xsh_xsd_xss,
+ init_vgmstream_psb,
/* lowest priority metas (should go after all metas, and TXTH should go before raw formats) */
init_vgmstream_txth, /* proper parsers should supersede TXTH, once added */
diff --git a/src/vgmstream.h b/src/vgmstream.h
index 81133467..8f536cbf 100644
--- a/src/vgmstream.h
+++ b/src/vgmstream.h
@@ -746,6 +746,7 @@ typedef enum {
meta_WXD_WXH,
meta_BNK_RELIC,
meta_XSH_XSD_XSS,
+ meta_PSB,
} meta_t;