import { DIRECTIONS, INPUT_BITS } from './defs.js'; import * as format_base from './format-base.js'; const TW_DIRECTION_TO_INPUT_BITS = [ INPUT_BITS.up, INPUT_BITS.left, INPUT_BITS.down, INPUT_BITS.right, INPUT_BITS.up | INPUT_BITS.left, INPUT_BITS.down | INPUT_BITS.left, INPUT_BITS.up | INPUT_BITS.right, INPUT_BITS.down | INPUT_BITS.right, ]; // doc: http://www.muppetlabs.com/~breadbox/software/tworld/tworldff.html#3 export function parse_solutions(bytes) { let buf; if (bytes.buffer) { buf = bytes.buffer; } else { buf = bytes; bytes = new Uint8Array(buf); } let view = new DataView(buf); let magic = view.getUint32(0, true); if (magic !== 0x999b3335) return; // 1 for lynx, 2 for ms; also extended to 3 for cc2, 4 for ll let ruleset = bytes[4]; let extra_bytes = bytes[7]; let ret = { ruleset: ruleset, levels: [], }; let p = 8 + extra_bytes; let is_first = true; while (p < buf.byteLength) { let len = view.getUint32(p, true); p += 4; if (len === 0xffffffff) break; if (len === 0) { // Empty, do nothing } else if (len < 6) { // This should never happen // TODO gripe? } else if (bytes[p] === 0 && bytes[p + 1] === 0 && bytes[p + 2] === 0 && bytes[p + 3] === 0 && bytes[p + 5] === 0 && bytes[p + 6] === 0) { // This record is special and contains the name of the set; it's optional but, if present, must be first if (! is_first) { // TODO gripe? } } else if (len === 6) { // Short record; password only, no replay } else { // Long record let number = view.getUint16(p, true); // 2-5: password, don't care // 6: flags, always zero let initial_state = bytes[p + 7]; let step_parity = initial_state >> 3; let initial_rff = ['north', 'west', 'south', 'east'][initial_state & 0x7]; // In CC2 replays, the initial RFF direction is the one you'll actually start with; // however, in Lynx, the direction is rotated BEFORE it takes effect, so to compensate // we have to rotate this once ahead of time initial_rff = DIRECTIONS[initial_rff].right; let initial_rng = view.getUint32(p + 8, true); let total_duration = view.getUint32(p + 12, true); // TODO split this off though let inputs = []; let q = p + 16; while (q < p + len) { // There are four formats for packing solutions, identified by the lowest two bits, // except that format 3 is actually two formats. Be aware that the documentation // refers to them in a different order than suggested by the identifying nybble. let fmt = bytes[q] & 0x3; let fmt2 = (bytes[q] >> 4) & 0x1; if (fmt === 0) { // "Third format": three consecutive moves packed into one byte let val = bytes[q]; q += 1; let input1 = TW_DIRECTION_TO_INPUT_BITS[(val >> 2) & 0x3]; let input2 = TW_DIRECTION_TO_INPUT_BITS[(val >> 4) & 0x3]; let input3 = TW_DIRECTION_TO_INPUT_BITS[(val >> 6) & 0x3]; inputs.push( 0, 0, 0, input1, 0, 0, 0, input2, 0, 0, 0, input3, ); } else if (fmt === 1 || fmt === 2 || (fmt === 3 && fmt2 === 0)) { // "First format" and "second format": one, two, or four bytes containing a // direction and a number of tics let val; if (fmt === 1) { val = bytes[q]; q += 1; } else if (fmt === 2) { val = view.getUint16(q, true); q += 2; } else { val = view.getUint32(q, true); q += 4; } let input = TW_DIRECTION_TO_INPUT_BITS[(val >> 2) & 0x7]; let duration = val >> 5; for (let i = 0; i < duration; i++) { inputs.push(0); } inputs.push(input); } else { // low nybble is 3, and bit 4 is set // "Fourth format": 2 to 5 bytes, containing an exceptionally long direction // field and time field, mostly used for MSCC mouse moves let n = ((bytes[q] >> 2) & 0x3) + 2; if (q + n - 1 >= bytes.length) throw new Error(`Malformed TWS file: expected ${n} bytes starting at ${q}, but only found ${bytes.length - q}`); // Up to 5 bytes is an annoying amount, but we can cut it down to 1-4 by // extracting the direction first let input = (bytes[q] >> 5) | ((bytes[q + 1] & 0x3f) << 3); let duration = bytes[q + 1] >> 6; for (let i = 3; i <= n; i++) { duration |= bytes[q + i - 1] << (2 + (i - 3) * 8); } // Mouse moves are encoded as 16 + ((y + 9) * 19) + (x + 9), but I extremely do // not support them at the moment (and may never), so replace them with blank // input for now (and possibly forever) if (input >= 16) { input = 0; } // And now queue it up for (let i = 0; i < duration; i++) { inputs.push(input); } q += n; } } ret.levels[number - 1] = new format_base.Replay(initial_rff, 0, inputs, step_parity, initial_rng); } is_first = false; p += len; } return ret; }