From f4a5125440503e7a8af8928909479ca15b665621 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 3 Jan 2021 16:02:15 +0100 Subject: [PATCH] Fix .xws with multibanks [Ninja Gaiden 3 RE (PS3), DOA 5 LR (PC)] --- src/meta/kwb.c | 438 ++++++++++++++++++++++++++++++------------------- 1 file changed, 269 insertions(+), 169 deletions(-) diff --git a/src/meta/kwb.c b/src/meta/kwb.c index d40aac1d..746b57a6 100644 --- a/src/meta/kwb.c +++ b/src/meta/kwb.c @@ -7,6 +7,7 @@ typedef struct { int big_endian; int total_subsongs; int target_subsong; + int found; kwb_codec codec; int channels; @@ -26,6 +27,7 @@ typedef struct { static int parse_kwb(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b); static int parse_xws(kwb_header* kwb, STREAMFILE* sf); +static VGMSTREAM* init_vgmstream_koei_wavebank(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b); /* KWB - WaveBank from Koei games */ @@ -33,7 +35,6 @@ VGMSTREAM* init_vgmstream_kwb(STREAMFILE* sf) { VGMSTREAM* vgmstream = NULL; STREAMFILE *sf_h = NULL, *sf_b = NULL; kwb_header kwb = {0}; - int32_t (*read_s32)(off_t,STREAMFILE*) = NULL; int target_subsong = sf->stream_index; @@ -70,89 +71,11 @@ VGMSTREAM* init_vgmstream_kwb(STREAMFILE* sf) { if (!parse_kwb(&kwb, sf_h, sf_b)) goto fail; - read_s32 = kwb.big_endian ? read_s32be : read_s32le; - - /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(kwb.channels, kwb.loop_flag); + vgmstream = init_vgmstream_koei_wavebank(&kwb, sf_h, sf_b); if (!vgmstream) goto fail; - vgmstream->meta_type = meta_KWB; - vgmstream->sample_rate = kwb.sample_rate; - vgmstream->num_samples = kwb.num_samples; - vgmstream->stream_size = kwb.stream_size; - vgmstream->num_streams = kwb.total_subsongs; - - switch(kwb.codec) { - case PCM16: /* PCM */ - vgmstream->coding_type = coding_PCM16LE; - vgmstream->layout_type = layout_interleave; - vgmstream->interleave_block_size = 0x02; - break; - - case MSADPCM: - vgmstream->coding_type = coding_MSADPCM; - vgmstream->layout_type = layout_none; - vgmstream->frame_size = kwb.block_size; - break; - - case DSP_HEAD: - case DSP_BODY: - if (kwb.channels > 1) goto fail; - vgmstream->coding_type = coding_NGC_DSP; /* subinterleave? */ - vgmstream->layout_type = layout_interleave; - vgmstream->interleave_block_size = 0x08; - if (kwb.codec == DSP_HEAD) { - dsp_read_coefs(vgmstream, sf_h, kwb.dsp_offset + 0x1c, 0x60, kwb.big_endian); - dsp_read_hist (vgmstream, sf_h, kwb.dsp_offset + 0x40, 0x60, kwb.big_endian); - } - else { - /* typical DSP header + data */ - vgmstream->num_samples = read_s32(kwb.stream_offset + 0x00, sf_b); - dsp_read_coefs(vgmstream, sf_b, kwb.stream_offset + 0x1c, 0x60, kwb.big_endian); - dsp_read_hist (vgmstream, sf_b, kwb.stream_offset + 0x40, 0x60, kwb.big_endian); - kwb.stream_offset += 0x60; - } - - break; - -#ifdef VGM_USE_ATRAC9 - case AT9: { - atrac9_config cfg = {0}; - - { - size_t extra_size = read_u32le(kwb.stream_offset + 0x00, sf_b); - uint32_t config_data = read_u32be(kwb.stream_offset + 0x04, sf_b); - /* 0x0c: encoder delay? */ - /* 0x0e: encoder padding? */ - /* 0x10: samples per frame */ - /* 0x12: frame size */ - - cfg.channels = vgmstream->channels; - cfg.config_data = config_data; - - kwb.stream_offset += extra_size; - kwb.stream_size -= extra_size; - } - - vgmstream->codec_data = init_atrac9(&cfg); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_ATRAC9; - vgmstream->layout_type = layout_none; - - //TODO: check encoder delay - vgmstream->num_samples = atrac9_bytes_to_samples_cfg(kwb.stream_size, cfg.config_data); - break; - } -#endif - default: - goto fail; - } - if (sf_h != sf) close_streamfile(sf_h); - - if (!vgmstream_open_stream(vgmstream, sf_b, kwb.stream_offset)) - goto fail; return vgmstream; fail: @@ -164,7 +87,6 @@ fail: /* XWS - WaveStream? from Koei games */ VGMSTREAM* init_vgmstream_xws(STREAMFILE* sf) { VGMSTREAM* vgmstream = NULL; - STREAMFILE* temp_sf = NULL; kwb_header kwb = {0}; int target_subsong = sf->stream_index; @@ -179,37 +101,138 @@ VGMSTREAM* init_vgmstream_xws(STREAMFILE* sf) { if (!parse_xws(&kwb, sf)) goto fail; - if (kwb.codec == MSF) { - if (kwb.stream_offset == 0) { - vgmstream = init_vgmstream_silence(0,0,0); /* dummy, whatevs */ - if (!vgmstream) goto fail; - } - else { - kwb.stream_size = read_u32be(kwb.stream_offset + 0x0c, sf) + 0x40; + vgmstream = init_vgmstream_koei_wavebank(&kwb, sf, sf); + if (!vgmstream) goto fail; - temp_sf = setup_subfile_streamfile(sf, kwb.stream_offset, kwb.stream_size, "msf"); - if (!temp_sf) goto fail; - - vgmstream = init_vgmstream_msf(temp_sf); - if (!vgmstream) goto fail; - } - - vgmstream->num_streams = kwb.total_subsongs; - } - else { - goto fail; - } - - close_streamfile(temp_sf); return vgmstream; fail: - close_streamfile(temp_sf); + close_vgmstream(vgmstream); return NULL; } +static VGMSTREAM* init_vgmstream_koei_wavebank(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b) { + VGMSTREAM* vgmstream = NULL; + uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL; + int32_t (*read_s32)(off_t,STREAMFILE*) = NULL; -static int parse_type_kwb2(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { + + read_u32 = kwb->big_endian ? read_u32be : read_u32le; + read_s32 = kwb->big_endian ? read_s32be : read_s32le; + + /* container */ + if (kwb->codec == MSF) { + if (kwb->stream_offset == 0) { + vgmstream = init_vgmstream_silence(0,0,0); /* dummy, whatevs */ + if (!vgmstream) goto fail; + } + else { + STREAMFILE* temp_sf = NULL; + + kwb->stream_size = read_u32(kwb->stream_offset + 0x0c, sf_h) + 0x40; + + temp_sf = setup_subfile_streamfile(sf_h, kwb->stream_offset, kwb->stream_size, "msf"); + if (!temp_sf) goto fail; + + vgmstream = init_vgmstream_msf(temp_sf); + close_streamfile(temp_sf); + if (!vgmstream) goto fail; + } + + vgmstream->num_streams = kwb->total_subsongs; + return vgmstream; + } + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(kwb->channels, kwb->loop_flag); + if (!vgmstream) goto fail; + + vgmstream->meta_type = meta_KWB; + vgmstream->sample_rate = kwb->sample_rate; + vgmstream->num_samples = kwb->num_samples; + vgmstream->stream_size = kwb->stream_size; + vgmstream->num_streams = kwb->total_subsongs; + + switch(kwb->codec) { + case PCM16: /* PCM */ + vgmstream->coding_type = coding_PCM16LE; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = 0x02; + break; + + case MSADPCM: + vgmstream->coding_type = coding_MSADPCM; + vgmstream->layout_type = layout_none; + vgmstream->frame_size = kwb->block_size; + break; + + case DSP_HEAD: + case DSP_BODY: + if (kwb->channels > 1) goto fail; + vgmstream->coding_type = coding_NGC_DSP; /* subinterleave? */ + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = 0x08; + if (kwb->codec == DSP_HEAD) { + dsp_read_coefs(vgmstream, sf_h, kwb->dsp_offset + 0x1c, 0x60, kwb->big_endian); + dsp_read_hist (vgmstream, sf_h, kwb->dsp_offset + 0x40, 0x60, kwb->big_endian); + } + else { + /* typical DSP header + data */ + vgmstream->num_samples = read_s32(kwb->stream_offset + 0x00, sf_b); + dsp_read_coefs(vgmstream, sf_b, kwb->stream_offset + 0x1c, 0x60, kwb->big_endian); + dsp_read_hist (vgmstream, sf_b, kwb->stream_offset + 0x40, 0x60, kwb->big_endian); + kwb->stream_offset += 0x60; + } + + break; + +#ifdef VGM_USE_ATRAC9 + case AT9: { + atrac9_config cfg = {0}; + + { + size_t extra_size = read_u32le(kwb->stream_offset + 0x00, sf_b); + uint32_t config_data = read_u32be(kwb->stream_offset + 0x04, sf_b); + /* 0x0c: encoder delay? */ + /* 0x0e: encoder padding? */ + /* 0x10: samples per frame */ + /* 0x12: frame size */ + + cfg.channels = vgmstream->channels; + cfg.config_data = config_data; + + kwb->stream_offset += extra_size; + kwb->stream_size -= extra_size; + } + + vgmstream->codec_data = init_atrac9(&cfg); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_ATRAC9; + vgmstream->layout_type = layout_none; + + //TODO: check encoder delay + vgmstream->num_samples = atrac9_bytes_to_samples_cfg(kwb->stream_size, cfg.config_data); + break; + } +#endif + default: + goto fail; + } + + + if (!vgmstream_open_stream(vgmstream, sf_b, kwb->stream_offset)) + goto fail; + return vgmstream; + +fail: + close_vgmstream(vgmstream); + return NULL; +} + +/* ************************************************************************* */ + +static int parse_type_kwb2(kwb_header* kwb, off_t offset, off_t body_offset, STREAMFILE* sf_h) { int i, j, sounds; /* 00: KWB2/KWBN id */ @@ -220,12 +243,15 @@ static int parse_type_kwb2(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { /* 10: null or 1 */ /* 14: offset to HDDB table (from type), can be null */ + //;VGM_LOG("KWB2: sounds %i, o=%lx\n", sounds, offset); + /* offset table to entries */ for (i = 0; i < sounds; i++) { off_t sound_offset = read_u32le(offset + 0x18 + i*0x04, sf_h); int subsounds, subsound_start, subsound_size; uint16_t version; + //;VGM_LOG("KWB2: entry %i, o=%lx, so=%lx\n", i, offset + 0x18 + i*0x04, sound_offset); if (sound_offset == 0) /* common... */ continue; @@ -258,6 +284,8 @@ static int parse_type_kwb2(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { kwb->total_subsongs++; if (kwb->total_subsongs != kwb->target_subsong) continue; + kwb->found = 1; + subsound_offset = subsound_start + j*subsound_size; kwb->sample_rate = read_u16le(subsound_offset + 0x00, sf_h); @@ -272,6 +300,8 @@ static int parse_type_kwb2(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { /* when size > 0x48 */ /* 0x48: subsound entry size */ /* rest: reserved per codec? (usually null) */ + + kwb->stream_offset += body_offset; switch(codec) { case 0x00: @@ -304,16 +334,14 @@ static int parse_type_kwb2(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { then name table (null terminated and one after other) */ - if (kwb->target_subsong < 0 || kwb->target_subsong > kwb->total_subsongs || kwb->total_subsongs < 1) goto fail; - return 1; fail: return 0; } -static int parse_type_k4hd(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { +static int parse_type_k4hd(kwb_header* kwb, off_t offset, off_t body_offset, STREAMFILE* sf_h) { off_t ppva_offset, header_offset; - int entries; + int entries, current_subsongs, relative_subsong; size_t entry_size; @@ -344,10 +372,14 @@ static int parse_type_k4hd(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { goto fail; } - kwb->total_subsongs = entries; - if (kwb->target_subsong < 0 || kwb->target_subsong > kwb->total_subsongs || kwb->total_subsongs < 1) goto fail; + current_subsongs = kwb->total_subsongs; + kwb->total_subsongs += entries; + if (kwb->target_subsong - 1 < current_subsongs || kwb->target_subsong > kwb->total_subsongs) + return 1; + kwb->found = 1; - header_offset = ppva_offset + 0x20 + (kwb->target_subsong-1) * entry_size; + relative_subsong = kwb->target_subsong - current_subsongs; + header_offset = ppva_offset + 0x20 + (relative_subsong-1) * entry_size; kwb->stream_offset = read_u32le(header_offset + 0x00, sf_h); kwb->sample_rate = read_u32le(header_offset + 0x04, sf_h); @@ -363,21 +395,22 @@ static int parse_type_k4hd(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { kwb->codec = AT9; kwb->channels = 1; /* always, devs use dual subsongs to fake stereo (like as hd3+bd3) */ + kwb->stream_offset += body_offset; return 1; fail: return 0; } -static int parse_type_sdsd(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { +static int parse_type_sdsd(kwb_header* kwb, off_t offset, off_t body_offset, STREAMFILE* sf_h) { /* has Vers, Head, Prog, Smpl sections (like Sony VABs) unknown codec, blocked with some common start, variable sized */ return 0; } -static int parse_type_sdwi(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { +static int parse_type_sdwi(kwb_header* kwb, off_t offset, off_t body_offset, STREAMFILE* sf_h) { off_t smpl_offset, header_offset; - int entries; + int entries, current_subsongs, relative_subsong; size_t entry_size; @@ -404,10 +437,14 @@ static int parse_type_sdwi(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { entries = read_u32le(smpl_offset + 0x0c, sf_h); /* LE! */ entry_size = 0x40; - kwb->total_subsongs = entries; - if (kwb->target_subsong < 0 || kwb->target_subsong > kwb->total_subsongs || kwb->total_subsongs < 1) goto fail; + current_subsongs = kwb->total_subsongs; + kwb->total_subsongs += entries; + if (kwb->target_subsong - 1 < current_subsongs || kwb->target_subsong > kwb->total_subsongs) + return 1; + kwb->found = 1; - header_offset = smpl_offset + 0x10 + (kwb->target_subsong-1) * entry_size; + relative_subsong = kwb->target_subsong - current_subsongs; + header_offset = smpl_offset + 0x10 + (relative_subsong-1) * entry_size; /* 00: "SS" + ID (0..N) */ kwb->stream_offset = read_u32be(header_offset + 0x04, sf_h); @@ -427,11 +464,14 @@ static int parse_type_sdwi(kwb_header* kwb, off_t offset, STREAMFILE* sf_h) { kwb->codec = DSP_BODY; kwb->channels = 1; + kwb->stream_offset += body_offset; + return 1; fail: return 0; } + static int parse_kwb(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b) { off_t head_offset, body_offset, start; uint32_t type; @@ -485,22 +525,22 @@ static int parse_kwb(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b) { switch(type) { case 0x4B574232: /* "KWB2" [Bladestorm Nightmare (PC), Dissidia NT (PC)] */ case 0x4B57424E: /* "KWBN" [Fire Emblem Warriors (Switch)] */ - if (!parse_type_kwb2(kwb, head_offset, sf_h)) + if (!parse_type_kwb2(kwb, head_offset, body_offset, sf_h)) goto fail; break; case 0x4B344844: /* "K4HD" [Dissidia NT (PS4), (Vita) */ - if (!parse_type_k4hd(kwb, head_offset, sf_h)) + if (!parse_type_k4hd(kwb, head_offset, body_offset, sf_h)) goto fail; break; case 0x53447364: /* "SDsd" (PS3? leftover files) */ - if (!parse_type_sdsd(kwb, head_offset, sf_h)) + if (!parse_type_sdsd(kwb, head_offset, body_offset, sf_h)) goto fail; break; case 0x53445769: /* "SDWi" [Fatal Frame 5 (WiiU)] */ - if (!parse_type_sdwi(kwb, head_offset, sf_h)) + if (!parse_type_sdwi(kwb, head_offset, body_offset, sf_h)) goto fail; break; @@ -508,7 +548,8 @@ static int parse_kwb(kwb_header* kwb, STREAMFILE* sf_h, STREAMFILE* sf_b) { goto fail; } - kwb->stream_offset += body_offset; + if (!kwb->found) + goto fail; return 1; fail: @@ -517,17 +558,112 @@ fail: static int parse_type_msfbank(kwb_header* kwb, off_t offset, STREAMFILE* sf) { /* this is just like XWSF, abridged: */ + int entries, current_subsongs, relative_subsong; off_t header_offset; + + entries = read_u32be(offset + 0x14, sf); - kwb->total_subsongs = read_u32be(offset + 0x14, sf); - if (kwb->target_subsong < 0 || kwb->target_subsong > kwb->total_subsongs || kwb->total_subsongs < 1) goto fail; + current_subsongs = kwb->total_subsongs; + kwb->total_subsongs += entries; + if (kwb->target_subsong - 1 < current_subsongs || kwb->target_subsong > kwb->total_subsongs) + return 1; + kwb->found = 1; - header_offset = offset + 0x30 + (kwb->target_subsong-1) * 0x04; + relative_subsong = kwb->target_subsong - current_subsongs; + header_offset = offset + 0x30 + (relative_subsong-1) * 0x04; /* just a dumb table pointing to MSF, entries can be dummy */ - kwb->stream_offset = read_u32be(header_offset, sf); + kwb->stream_offset = read_u32be(header_offset, sf); kwb->codec = MSF; + kwb->stream_offset += offset; + + return 1; +//fail: +// return 0; +} + +static int parse_type_xwsfile(kwb_header* kwb, off_t offset, STREAMFILE* sf) { + off_t table1_offset, table2_offset; + int i, chunks, chunks2; + uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL; + + + if (!(is_id32be(offset + 0x00, sf, "XWSF") && is_id32be(offset + 0x04, sf, "ILE\0")) && + !(is_id32be(offset + 0x00, sf, "tdpa") && is_id32be(offset + 0x04, sf, "ck\0\0"))) + goto fail; + + kwb->big_endian = read_u8(offset + 0x08, sf) == 0xFF; + /* 0x0a: version? (0100: NG2/NG3 PS3, 0101: DoA LR PC) */ + + read_u32 = kwb->big_endian ? read_u32be : read_u32le; + + /* 0x0c: tables start */ + /* 0x10: file size */ + chunks = read_u32(offset + 0x14, sf); + chunks2 = read_u32(offset + 0x18, sf); + /* 0x1c: null */ + if (chunks != chunks2) + goto fail; + + table1_offset = read_u32(offset + 0x20, sf); /* offsets */ + table2_offset = read_u32(offset + 0x24, sf); /* sizes */ + /* 0x28: null */ + /* 0x2c: null */ + + + i = 0; + while (i < chunks) { + uint32_t entry_type, head_offset, body_offset, head_size; + //;VGM_LOG("XWS: entry %i/%i\n", i, chunks); + + /* NG2/NG3 PS3 have table1+2, DoA LR PC removes table2 and includes body offset in entries */ + if (table2_offset) { + head_offset = read_u32(offset + table1_offset + i * 0x04 + 0x00, sf); + head_size = read_u32(offset + table2_offset + i * 0x04 + 0x00, sf); + body_offset = head_offset; + i += 1; + + /* sometimes has file end offset as entry with no size*/ + if (!head_size) + continue; + } + else { + head_offset = read_u32(offset + table1_offset + i * 0x04 + 0x00, sf); + body_offset = read_u32(offset + table1_offset + i * 0x04 + 0x04, sf); + i += 2; + } + + if (!head_offset) /* just in case */ + continue; + + + head_offset += offset; + body_offset += offset; + entry_type = read_u32be(head_offset + 0x00, sf); + //;VGM_LOG("XWS: head=%x, body=%x\n", head_offset, body_offset); + + if (entry_type == get_id32be("XWSF")) { /* + "ILE\0" */ + if (!parse_type_xwsfile(kwb, head_offset, sf)) + goto fail; + } + else if (entry_type == get_id32be("CUEB") || entry_type < 0x100) { + ; /* CUE-like info (may start with 0 or a low number instead) */ + } + else if (entry_type == get_id32be("MSFB")) { /* + "ANK\0" */ + if (!parse_type_msfbank(kwb, head_offset, sf)) + goto fail; + } + else if (entry_type == get_id32be("KWB2")) { + if (!parse_type_kwb2(kwb, head_offset, body_offset, sf)) + goto fail; + } + else { + VGM_LOG("XWS: unknown type %x at head=%x, body=%x\n", entry_type, head_offset, body_offset); + goto fail; + } + } + return 1; fail: return 0; @@ -535,59 +671,23 @@ fail: static int parse_xws(kwb_header* kwb, STREAMFILE* sf) { - off_t head_offset, body_offset, start; - uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL; - int chunks, chunks2; - off_t msfb_offset; - /* format is similar to WHD1 with some annoyances of its own - * variations: - * - tdpack: points to N XWSFILE - * - XWSFILE w/ 4 chunks: CUEBANK offset, ? offset, MSFBANK offset, end offset (PS3) + /* Format is similar to WHD1 with some annoyances of its own. Variations: + * - XWSFILE w/ N chunks: CUE offsets + 1 MSFBANK offset * [Ninja Gaiden Sigma 2 (PS3), Ninja Gaiden 3 Razor's Edge (PS3)] * - XWSFILE w/ 2*N chunks: KWB2 offset + data offset * N (ex. 3 pairs = 6 chunks) * [Dead or Alive 5 Last Round (PC)] + * - tdpack: same but points to N XWSFILE + * [Ninja Gaiden 3 Razor's Edge (PS3)] * - * for now basic support for the second case, others we'd have to map subsong N to internal bank M + * Needs to call sub-parts multiple times to fill total subsongs when parsing xwsfile. */ - - if (read_u32be(0x00, sf) != 0x58575346 || /* "XWSF" */ - read_u32be(0x04, sf) != 0x494C4500) /* "ILE\0" */ + if (!parse_type_xwsfile(kwb, 0x00, sf)) goto fail; - kwb->big_endian = read_u8(0x08, sf) == 0xFF; - /* 0x0a: version? */ - - read_u32 = kwb->big_endian ? read_u32be : read_u32le; - - start = read_u32(0x0c, sf); - /* 0x10: file size */ - chunks = read_u32(0x14, sf); - chunks2 = read_u32(0x18, sf); - /* 0x1c: null */ - /* 0x20: some size? */ - /* 0x24: some size? */ - if (chunks != chunks2) + if (!kwb->found) goto fail; - if (chunks != 4) - goto fail; - - msfb_offset = read_u32(start + 0x08, sf); - if (read_u32be(msfb_offset, sf) == 0x4D534642) { /* "MSFB" + "ANK\0" */ - head_offset = msfb_offset; - body_offset = msfb_offset; /* relative to start */ - - if (!parse_type_msfbank(kwb, head_offset, sf)) - goto fail; - } - else { - goto fail; - } - - - kwb->stream_offset += body_offset; - return 1; fail: return 0;