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;