diff --git a/README.md b/README.md index 1e87054..c145bbe 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,17 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le - Load levels directly from the BBC set list - Mouse support -### Noble aspirations +## For developers -- New exclusive puzzle elements?? Embrace extend extinguish baby +It's all static JS; there's no build system. If you want to run it locally, just throw your favorite HTTP server at a checkout and open a browser. (Browsers won't allow XHR from `file:///` URLs, alas. If you don't have a favorite HTTP server, try `python -m http.server`.) + +If you have Node installed, you can test the solutions included with the bundled level packs without needing a web browser: + +``` +node js/headless/bulktest.mjs +``` + +Note that solution playback is still not perfect, so don't be alarmed if you don't get 100% — only if you make a change and something regresses. ## Special thanks diff --git a/js/defs.js b/js/defs.js index e81e35d..1c60a45 100644 --- a/js/defs.js +++ b/js/defs.js @@ -114,3 +114,159 @@ export const PICKUP_PRIORITIES = { player: 1, // players and doppelgangers; red keys (ignored by everything else) real_player: 0, }; + +export const COMPAT_RULESET_LABELS = { + lexy: "Lexy", + steam: "Steam/CC2", + 'steam-strict': "Steam/CC2 (strict)", + lynx: "Lynx", + ms: "Microsoft", + custom: "Custom", +}; +export const COMPAT_RULESET_ORDER = ['lexy', 'steam', 'steam-strict', 'lynx', 'ms', 'custom']; +// FIXME some of the names of the flags themselves kinda suck +export const COMPAT_FLAGS = [ +// Level loading +{ + key: 'no_auto_convert_ccl_popwalls', + label: "Recessed walls under actors in CCL levels are left alone", + rulesets: new Set(['steam-strict', 'lynx', 'ms']), +}, { + key: 'no_auto_convert_ccl_blue_walls', + label: "Blue walls under blocks in CCL levels are left alone", + rulesets: new Set(['steam-strict', 'lynx']), +}, + +// Core +{ + key: 'use_lynx_loop', + label: "Game uses the Lynx-style update loop", + rulesets: new Set(['steam', 'steam-strict', 'lynx', 'ms']), +}, { + key: 'player_moves_last', + label: "Player always moves last", + rulesets: new Set(['lynx', 'ms']), +}, { + key: 'emulate_60fps', + label: "Game runs at 60 FPS", + rulesets: new Set(['steam', 'steam-strict']), +}, { + key: 'reuse_actor_slots', + label: "Game reuses slots in the actor list", + rulesets: new Set(['lynx']), +}, { + key: 'force_lynx_animation_lengths', + label: "Animations use Lynx duration", + rulesets: new Set(['lynx']), +}, + +// Tiles +{ + // XXX this is goofy + key: 'tiles_react_instantly', + label: "Tiles react when approached", + rulesets: new Set(['ms']), +}, { + key: 'rff_actually_random', + label: "Random force floors are actually random", + rulesets: new Set(['ms']), +}, { + key: 'no_backwards_override', + label: "Player can't override backwards on a force floor", + rulesets: new Set(['lynx']), +}, { + key: 'traps_like_lynx', + label: "Traps eject faster, and even when already open", + rulesets: new Set(['lynx']), +}, + +// Items +{ + key: 'no_immediate_detonate_bombs', + label: "Mines under non-player actors don't explode at level start", + rulesets: new Set(['lynx', 'ms']), +}, { + key: 'detonate_bombs_under_players', + label: "Mines under players explode at level start", + rulesets: new Set(['steam', 'steam-strict']), +}, { + key: 'monsters_ignore_keys', + label: "Monsters completely ignore keys", + rulesets: new Set(['ms']), +}, { + key: 'monsters_blocked_by_items', + label: "Monsters can't step on items to get the player", + rulesets: new Set(['lynx']), +}, + +// Blocks +{ + key: 'no_early_push', + label: "Player pushes blocks at move time", + rulesets: new Set(['lynx', 'ms']), +}, { + key: 'use_legacy_hooking', + label: "Pulling blocks with the hook happens at decision time", + rulesets: new Set(['steam', 'steam-strict']), +}, { + // FIXME this is kind of annoying, there are some collision rules too + key: 'tanks_teeth_push_ice_blocks', + label: "Ice blocks emulate pgchip rules", + rulesets: new Set(['ms']), +}, { + key: 'emulate_spring_mining', + label: "Spring mining is possible", + rulesets: new Set(['steam-strict']), +/* XXX not implemented +}, { + key: 'emulate_flicking', + label: "Flicking is possible", + rulesets: new Set(['ms']), +*/ +}, + +// Monsters +{ + // TODO? in lynx they ignore the button while in motion too + // TODO what about in a trap, in every game?? + // TODO what does ms do when a tank is on ice or a ff? wiki's description is wacky + // TODO yellow tanks seem to have memory too?? + key: 'tanks_always_obey_button', + label: "Blue tanks always obey blue buttons", + rulesets: new Set(['steam-strict']), +}, { + key: 'tanks_ignore_button_while_moving', + label: "Blue tanks ignore blue buttons while moving", + rulesets: new Set(['lynx']), +}, { + key: 'blobs_use_tw_prng', + label: "Blobs use the Tile World RNG", + rulesets: new Set(['lynx']), +}, { + key: 'teeth_target_internal_position', + label: "Teeth target the player's internal position", + rulesets: new Set(['lynx']), +}, { + key: 'rff_blocks_monsters', + label: "Random force floors block monsters", + rulesets: new Set(['ms']), +}, { + key: 'bonking_isnt_instant', + label: "Bonking while sliding doesn't apply instantly", + rulesets: new Set(['lynx', 'ms']), +}, { + key: 'fire_allows_monsters', + label: "Fire doesn't block monsters", + rulesets: new Set(['ms']), +}, +]; + +export function compat_flags_for_ruleset(ruleset) { + let compat = {}; + for (let compatdef of COMPAT_FLAGS) { + if (compatdef.rulesets.has(ruleset)) { + compat[compatdef.key] = true; + } + } + return compat; +} diff --git a/js/format-base.js b/js/format-base.js index f9aaa18..e661cef 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -163,6 +163,10 @@ export class StoredPack { // error: any error received while loading the level // bytes: Uint8Array of the encoded level data this.level_metadata = []; + + // Sparse/optional array of Replays, generally from an ancillary file like a TWS + // TODO unclear if this is a good API for this + this.level_replays = []; } // TODO this may or may not work sensibly when correctly following a c2g @@ -177,10 +181,13 @@ export class StoredPack { // The editor stores inflated levels at times, so respect that return meta.stored_level; } - else { - // Otherwise, attempt to load the level - return this._level_loader(meta); + + // Otherwise, attempt to load the level + let stored_level = this._level_loader(meta); + if (! stored_level.has_replay && this.level_replays[index]) { + stored_level._replay = this.level_replays[index]; } + return stored_level; } } diff --git a/js/format-c2g.js b/js/format-c2g.js index 2259636..9f0d372 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -409,8 +409,14 @@ const TILE_ENCODING = { }, 0x44: { name: 'cloner', - // TODO visual directions bitmask, no gameplay impact, possible editor impact - modifier: null, + modifier: { + decode(tile, mod) { + tile.arrows = mod; + }, + encode(tile) { + return tile.arrows; + }, + }, }, 0x45: { name: 'hint', diff --git a/js/format-tws.js b/js/format-tws.js index 824f8d4..e7f3085 100644 --- a/js/format-tws.js +++ b/js/format-tws.js @@ -71,7 +71,6 @@ export function parse_solutions(bytes) { let step_parity = initial_state >> 3; let initial_rff = ['north', 'west', 'south', 'east'][initial_state & 0x7]; let initial_rng = view.getUint32(p + 8, true); // FIXME how is this four bytes?? lynx rng doesn't even have four bytes of STATE - console.log(number, initial_state.toString(16), initial_rng.toString(16)); let total_duration = view.getUint32(p + 12, true); // TODO split this off though diff --git a/js/headless/bulktest.mjs b/js/headless/bulktest.mjs new file mode 100644 index 0000000..599d544 --- /dev/null +++ b/js/headless/bulktest.mjs @@ -0,0 +1,401 @@ +import { compat_flags_for_ruleset } from '../defs.js'; +import { Level } from '../game.js'; +import * as format_c2g from '../format-c2g.js'; +import * as format_dat from '../format-dat.js'; +import * as format_tws from '../format-tws.js'; +import * as util from '../util.js'; + +import { stdout } from 'process'; +import { opendir, readFile } from 'fs/promises'; +import { performance } from 'perf_hooks'; + +// TODO arguments: +// - custom pack to test, possibly its solutions, possibly its ruleset (or default to steam-strict/lynx) +// - filter existing packs +// - verbose: ? +// - quiet: hide failure reasons +// - support for xfails somehow? +// TODO use this for a test suite + +export class LocalDirectorySource extends util.FileSource { + constructor(root) { + super(); + this.root = root; + this.files = {}; + this._loaded_promise = this._scan_dir('/'); + } + + async _scan_dir(path) { + let dir = await opendir(this.root + path); + for await (let dirent of dir) { + if (dirent.isDirectory()) { + await this._scan_dir(path + dirent.name + '/'); + } + else { + let filepath = path + dirent.name; + this.files[filepath.toLowerCase()] = filepath; + if (this.files.size > 2000) + throw `way, way too many files in local directory source ${this.root}`; + } + } + } + + async get(path) { + let realpath = this.files[path.toLowerCase()]; + if (realpath) { + return (await readFile(this.root + realpath)).buffer; + } + else { + throw new Error(`No such file: ${path}`); + } + } +} + +function pad(s, n) { + return s.substring(0, n).padEnd(n, " "); +} + +const RESULT_TYPES = { + 'no-replay': { + color: "\x1b[90m", + symbol: "-", + }, + success: { + color: "\x1b[92m", + symbol: ".", + }, + early: { + color: "\x1b[96m", + symbol: "?", + }, + failure: { + color: "\x1b[91m", + symbol: "#", + }, + 'short': { + color: "\x1b[93m", + symbol: "#", + }, + error: { + color: "\x1b[95m", + symbol: "X", + }, +}; +const ANSI_RESET = "\x1b[39m"; + +async function test_pack(pack, ruleset) { + let dummy_sfx = { + set_player_position() {}, + play() {}, + play_once() {}, + }; + let compat = compat_flags_for_ruleset(ruleset); + + // TODO factor out the common parts maybe? + stdout.write(pad(`${pack.title} (${ruleset})`, 20) + " "); + let num_levels = pack.level_metadata.length; + let num_passed = 0; + let num_missing = 0; + let total_tics = 0; + let t0 = performance.now(); + let last_pause = t0; + let failures = []; + for (let i = 0; i < num_levels; i++) { + let stored_level, level; + let level_start_time = performance.now(); + let record_result = (token, short_status, include_canvas, comment) => { + let result_stuff = RESULT_TYPES[token]; + stdout.write(result_stuff.color + result_stuff.symbol); + if (token === 'failure' || token === 'short') { + failures.push({ + token, + short_status, + comment, + level, + stored_level, + index: i, + fail_reason: level ? level.fail_reason : null, + time_elapsed: performance.now() - level_start_time, + time_expected: stored_level ? stored_level.replay.duration / 20 : null, + title: stored_level.title ?? "[error]", + time_simulated: level.tic_counter / 20, + }); + } + if (level) { + /* + mk('td.-clock', util.format_duration(level.tic_counter / TICS_PER_SECOND)), + mk('td.-delta', util.format_duration((level.tic_counter - stored_level.replay.duration) / TICS_PER_SECOND, 2)), + mk('td.-speed', ((level.tic_counter / TICS_PER_SECOND) / (performance.now() - level_start_time) * 1000).toFixed(2) + '×'), + */ + } + else { + } + + // FIXME allegedly it's possible to get a canvas working in node... + /* + if (include_canvas && level) { + try { + let tileset = this.conductor.choose_tileset_for_level(level.stored_level); + this.renderer.set_tileset(tileset); + let canvas = mk('canvas', { + width: Math.min(this.renderer.canvas.width, level.size_x * tileset.size_x), + height: Math.min(this.renderer.canvas.height, level.size_y * tileset.size_y), + }); + this.renderer.set_level(level); + this.renderer.set_active_player(level.player); + this.renderer.draw(); + canvas.getContext('2d').drawImage( + this.renderer.canvas, 0, 0, + this.renderer.canvas.width, this.renderer.canvas.height); + tbody.append(mk('tr', mk('td.-full', {colspan: 5}, canvas))); + } + catch (e) { + console.error(e); + tbody.append(mk('tr', mk('td.-full', {colspan: 5}, + `Internal error while trying to capture screenshot: ${e}`))); + } + } + */ + + if (level) { + total_tics += level.tic_counter; + } + }; + + try { + stored_level = pack.load_level(i); + if (! stored_level.has_replay) { + record_result('no-replay', "No replay"); + num_missing += 1; + continue; + } + + // TODO? this.current_status.textContent = `Testing level ${i + 1}/${num_levels} ${stored_level.title}...`; + + let replay = stored_level.replay; + level = new Level(stored_level, compat); + level.sfx = dummy_sfx; + level.undo_enabled = false; // slight performance boost + replay.configure_level(level); + + while (true) { + let input = replay.get(level.tic_counter); + level.advance_tic(input); + + if (level.state === 'success') { + if (level.tic_counter < replay.duration - 10) { + // Early exit is dubious (e.g. this happened sometimes before multiple + // players were implemented correctly) + record_result('early', "Won early", true); + } + else { + record_result('success', "Won"); + } + num_passed += 1; + break; + } + else if (level.state === 'failure') { + record_result('failure', "Lost", true); + break; + } + else if (level.tic_counter >= replay.duration + 200) { + record_result('short', "Out of input", true); + break; + } + + if (level.tic_counter % 20 === 1) { + // XXX + /* + if (handle.cancel) { + record_result('interrupted', "Interrupted"); + this.current_status.textContent = `Interrupted on level ${i + 1}/${num_levels}; ${num_passed} passed`; + return; + } + */ + + // Don't run for more than 100ms at a time, to avoid janking the browser... + // TOO much. I mean, we still want it to reflow the stuff we've added, but + // we also want to be pretty aggressive so this finishes quickly + // XXX unnecessary headless + /* + let now = performance.now(); + if (now - last_pause > 100) { + await util.sleep(4); + last_pause = now; + } + */ + } + } + } + catch (e) { + //console.error(e); + record_result( + 'error', "Error", true, + `Replay failed due to internal error (see console for traceback): ${e}`); + } + } + + let total_real_elapsed = (performance.now() - t0) / 1000; + + stdout.write(`${ANSI_RESET} ${num_passed}/${num_levels - num_missing}\n`); + for (let failure of failures) { + let short_status = failure.short_status; + if (failure.token === 'failure') { + short_status += ": "; + short_status += failure.fail_reason; + } + + let parts = [ + String(failure.index + 1).padStart(5), + pad(failure.title.replace(/[\r\n]+/, " "), 32), + RESULT_TYPES[failure.token].color + pad(short_status, 20) + ANSI_RESET, + "ran for" + util.format_duration(failure.time_simulated).padStart(6, " "), + ]; + if (failure.token === 'failure') { + parts.push(" with" + util.format_duration(failure.time_expected - failure.time_simulated).padStart(6, " ") + " still to go"); + } + stdout.write(parts.join(" ") + "\n"); + } + + return { + num_passed, + num_missing, + num_failed: num_levels - num_passed - num_missing, + time_elapsed: total_real_elapsed, + time_simulated: total_tics / 20, + }; +} + + +const TESTABLE_PACKS = [{ +// FIXME lynx cc1... +/* + title: "CC1", + pack_path: 'levels/CC1.ccl', + solutions_path: 'levels/public_CHIPS-lynx.dac.tws', + ruleset: 'lynx', +}, { +*/ +// FIXME the solution files aren't committed yet +/* + title: "CCLXP2", + pack_path: 'levels/CCLXP2.ccl', + solutions_path: 'levels/public_CCLXP2.dac.tws', + ruleset: 'lynx', +}, { + title: "CCLP3", + pack_path: 'levels/CCLP3.ccl', + solutions_path: 'levels/public_CCLP3-lynx.dac.tws', + ruleset: 'lynx', +}, { + title: "CCLP4", + pack_path: 'levels/CCLP4.ccl', + solutions_path: 'levels/public_CCLP4-lynx.dac.tws', + ruleset: 'lynx', +}, { + title: "CCLP1", + pack_path: 'levels/CCLP1.ccl', + solutions_path: 'levels/public_CCLP1-lynx.dac.tws', + ruleset: 'lynx', +}, { +*/ + title: "CC2LP1", + pack_path: 'levels/CC2LP1.zip', + ruleset: 'steam-strict', +// FIXME add steam cc1, cc2, but optionally and from command line or something +// (these are just local symlinks to my steam directory lol) + /* +}, { + title: "CC1 (Steam)", + pack_path: 'levels/cc1', + ruleset: 'steam-strict', + isdir: true, +}, { + title: "CC2", + pack_path: 'levels/cc2', + ruleset: 'steam-strict', + isdir: true, +}, { + title: "CC2LP1-Voting", + pack_path: '_local-levels/CC2LP1-Voting', + ruleset: 'steam-strict', + isdir: true, + */ +}]; +async function _scan_source(source) { + // FIXME copied wholesale from Splash.search_multi_source; need a real filesystem + searching api! + + // TODO not entiiirely kosher, but not sure if we should have an api for this or what + if (source._loaded_promise) { + await source._loaded_promise; + } + + let paths = Object.keys(source.files); + // TODO should handle having multiple candidates, but this is good enough for now + paths.sort((a, b) => a.length - b.length); + for (let path of paths) { + let m = path.match(/[.]([^./]+)$/); + if (! m) + continue; + + let ext = m[1]; + // TODO this can't load an individual c2m, hmmm + if (ext === 'c2g') { + let buf = await source.get(path); + //await this.conductor.parse_and_load_game(buf, source, path); + // FIXME and this is from parse_and_load_game!! + let dir; + if (! path.match(/[/]/)) { + dir = ''; + } + else { + dir = path.replace(/[/][^/]+$/, ''); + } + return await format_c2g.parse_game(buf, source, dir); + } + } + // TODO else...? complain we couldn't find anything? list what we did find?? idk +} +async function main() { + let overall = { + num_passed: 0, + num_missing: 0, + num_failed: 0, + time_elapsed: 0, + time_simulated: 0, + }; + for (let testdef of TESTABLE_PACKS) { + let pack; + if (testdef.isdir) { + let source = new LocalDirectorySource(testdef.pack_path); + pack = await _scan_source(source); + } + else { + let pack_data = await readFile(testdef.pack_path); + if (testdef.pack_path.match(/[.]zip$/)) { + let source = new util.ZipFileSource(pack_data.buffer); + pack = await _scan_source(source); + } + else { + pack = format_dat.parse_game(pack_data.buffer); + + let solutions_data = await readFile(testdef.solutions_path); + let solutions = format_tws.parse_solutions(solutions_data.buffer); + pack.level_replays = solutions.levels; + } + } + + pack.title = testdef.title; + let result = await test_pack(pack, testdef.ruleset); + for (let key of Object.keys(overall)) { + overall[key] += result[key]; + } + } + + let num_levels = overall.num_passed + overall.num_failed + overall.num_missing; + stdout.write("\n"); + stdout.write(`${overall.num_passed}/${num_levels} = ${(overall.num_passed / num_levels * 100).toFixed(1)}% passed (${overall.num_failed} failed, ${overall.num_missing} missing replay)\n`); + stdout.write(`Simulated ${util.format_duration(overall.time_simulated)} of game time in ${util.format_duration(overall.time_elapsed)}, speed of ${(overall.time_simulated / overall.time_elapsed).toFixed(1)}×\n`); + +} +main(); diff --git a/js/main-editor.js b/js/main-editor.js index f442b2b..fbe90a1 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -1,4 +1,4 @@ -import * as fflate from 'https://cdn.skypack.dev/fflate?min'; +import * as fflate from './vendor/fflate.mjs'; import { DIRECTIONS, LAYERS, TICS_PER_SECOND } from './defs.js'; import { TILES_WITH_PROPS } from './editor-tile-overlays.js'; diff --git a/js/main.js b/js/main.js index f3e2bc7..6542080 100644 --- a/js/main.js +++ b/js/main.js @@ -1,8 +1,8 @@ // TODO bugs and quirks i'm aware of: // - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor -import * as fflate from 'https://cdn.skypack.dev/fflate?min'; +import * as fflate from './vendor/fflate.mjs'; -import { DIRECTIONS, INPUT_BITS, TICS_PER_SECOND } from './defs.js'; +import { COMPAT_FLAGS, COMPAT_RULESET_LABELS, COMPAT_RULESET_ORDER, DIRECTIONS, INPUT_BITS, TICS_PER_SECOND, compat_flags_for_ruleset } from './defs.js'; import * as c2g from './format-c2g.js'; import * as dat from './format-dat.js'; import * as format_base from './format-base.js'; @@ -2953,150 +2953,6 @@ class OptionsOverlay extends DialogOverlay { super.close(); } } -const COMPAT_RULESETS = [ - ['lexy', "Lexy"], - ['steam', "Steam/CC2"], - ['steam-strict', "Steam/CC2 (strict)"], - ['lynx', "Lynx"], - ['ms', "Microsoft"], - ['custom', "Custom"], -]; -// FIXME some of the names of the flags themselves kinda suck -const COMPAT_FLAGS = [ -// Level loading -{ - key: 'no_auto_convert_ccl_popwalls', - label: "Recessed walls under actors in CCL levels are left alone", - rulesets: new Set(['steam-strict', 'lynx', 'ms']), -}, { - key: 'no_auto_convert_ccl_blue_walls', - label: "Blue walls under blocks in CCL levels are left alone", - rulesets: new Set(['steam-strict', 'lynx']), -}, - -// Core -{ - key: 'use_lynx_loop', - label: "Game uses the Lynx-style update loop", - rulesets: new Set(['steam', 'steam-strict', 'lynx', 'ms']), -}, { - key: 'player_moves_last', - label: "Player always moves last", - rulesets: new Set(['lynx', 'ms']), -}, { - key: 'emulate_60fps', - label: "Game runs at 60 FPS", - rulesets: new Set(['steam', 'steam-strict']), -}, { - key: 'reuse_actor_slots', - label: "Game reuses slots in the actor list", - rulesets: new Set(['lynx']), -}, { - key: 'force_lynx_animation_lengths', - label: "Animations use Lynx duration", - rulesets: new Set(['lynx']), -}, - -// Tiles -{ - // XXX this is goofy - key: 'tiles_react_instantly', - label: "Tiles react when approached", - rulesets: new Set(['ms']), -}, { - key: 'rff_actually_random', - label: "Random force floors are actually random", - rulesets: new Set(['ms']), -}, { - key: 'no_backwards_override', - label: "Player can't override backwards on a force floor", - rulesets: new Set(['lynx']), -}, { - key: 'traps_like_lynx', - label: "Traps eject faster, and even when already open", - rulesets: new Set(['lynx']), -}, - -// Items -{ - key: 'no_immediate_detonate_bombs', - label: "Mines under non-player actors don't explode at level start", - rulesets: new Set(['lynx', 'ms']), -}, { - key: 'detonate_bombs_under_players', - label: "Mines under players explode at level start", - rulesets: new Set(['steam', 'steam-strict']), -}, { - key: 'monsters_ignore_keys', - label: "Monsters completely ignore keys", - rulesets: new Set(['ms']), -}, { - key: 'monsters_blocked_by_items', - label: "Monsters can't step on items to get the player", - rulesets: new Set(['lynx']), -}, - -// Blocks -{ - key: 'no_early_push', - label: "Player pushes blocks at move time", - rulesets: new Set(['lynx', 'ms']), -}, { - key: 'use_legacy_hooking', - label: "Pulling blocks with the hook happens at decision time", - rulesets: new Set(['steam', 'steam-strict']), -}, { - // FIXME this is kind of annoying, there are some collision rules too - key: 'tanks_teeth_push_ice_blocks', - label: "Ice blocks emulate pgchip rules", - rulesets: new Set(['ms']), -}, { - key: 'emulate_spring_mining', - label: "Spring mining is possible", - rulesets: new Set(['steam-strict']), -/* XXX not implemented -}, { - key: 'emulate_flicking', - label: "Flicking is possible", - rulesets: new Set(['ms']), -*/ -}, - -// Monsters -{ - // TODO? in lynx they ignore the button while in motion too - // TODO what about in a trap, in every game?? - // TODO what does ms do when a tank is on ice or a ff? wiki's description is wacky - // TODO yellow tanks seem to have memory too?? - key: 'tanks_always_obey_button', - label: "Blue tanks always obey blue buttons", - rulesets: new Set(['steam-strict']), -}, { - key: 'tanks_ignore_button_while_moving', - label: "Blue tanks ignore blue buttons while moving", - rulesets: new Set(['lynx']), -}, { - key: 'blobs_use_tw_prng', - label: "Blobs use the Tile World RNG", - rulesets: new Set(['lynx']), -}, { - key: 'teeth_target_internal_position', - label: "Teeth target the player's internal position", - rulesets: new Set(['lynx']), -}, { - key: 'rff_blocks_monsters', - label: "Random force floors block monsters", - rulesets: new Set(['ms']), -}, { - key: 'bonking_isnt_instant', - label: "Bonking while sliding doesn't apply instantly", - rulesets: new Set(['lynx', 'ms']), -}, { - key: 'fire_allows_monsters', - label: "Fire doesn't block monsters", - rulesets: new Set(['ms']), -}, -]; class CompatOverlay extends DialogOverlay { constructor(conductor) { super(conductor); @@ -3113,13 +2969,13 @@ class CompatOverlay extends DialogOverlay { ); let button_set = mk('div.radio-faux-button-set'); - for (let [ruleset, label] of COMPAT_RULESETS) { + for (let ruleset of COMPAT_RULESET_ORDER) { button_set.append(mk('label', mk('input', {type: 'radio', name: '__ruleset__', value: ruleset}), mk('span.-button', mk('img.compat-icon', {src: `icons/compat-${ruleset}.png`}), mk('br'), - label, + COMPAT_RULESET_LABELS[ruleset], ), )); } @@ -3140,7 +2996,7 @@ class CompatOverlay extends DialogOverlay { mk('input', {type: 'checkbox', name: compat.key}), mk('span.-desc', compat.label), ); - for (let [ruleset, _] of COMPAT_RULESETS) { + for (let ruleset of COMPAT_RULESET_ORDER) { if (ruleset === 'lexy' || ruleset === 'custom') continue; @@ -3263,12 +3119,12 @@ class PackTestDialog extends DialogOverlay { }); let ruleset_dropdown = mk('select', {name: 'ruleset'}); - for (let [ruleset, label] of COMPAT_RULESETS) { + for (let ruleset of COMPAT_RULESET_ORDER) { if (ruleset === 'custom') { ruleset_dropdown.append(mk('option', {value: ruleset, selected: 'selected'}, "Current ruleset")); } else { - ruleset_dropdown.append(mk('option', {value: ruleset}, label)); + ruleset_dropdown.append(mk('option', {value: ruleset}, COMPAT_RULESET_LABELS[ruleset])); } } this.main.append( @@ -3302,12 +3158,7 @@ class PackTestDialog extends DialogOverlay { compat = this.conductor.compat; } else { - compat = {}; - for (let compatdef of COMPAT_FLAGS) { - if (compatdef.rulesets.has(ruleset)) { - compat[compatdef.key] = true; - } - } + compat = compat_flags_for_ruleset(ruleset); } for (let tbody of this.results.querySelectorAll('tbody')) { @@ -3690,11 +3541,7 @@ class Conductor { this._compat_ruleset = 'custom'; // Only used by the compat dialog if (typeof this.stash.compat === 'string') { this._compat_ruleset = this.stash.compat; - for (let compat of COMPAT_FLAGS) { - if (compat.rulesets.has(this.stash.compat)) { - this.compat[compat.key] = true; - } - } + this.compat = compat_flags_for_ruleset(this.stash.compat); } else { Object.extend(this.compat, this.stash.compat); @@ -4000,8 +3847,7 @@ class Conductor { this._compat_ruleset = ruleset; } - let label = COMPAT_RULESETS.filter(item => item[0] === ruleset)[0][1]; - document.querySelector('#main-compat output').textContent = label; + document.querySelector('#main-compat output').textContent = COMPAT_RULESET_LABELS[ruleset]; this.compat = flags; } diff --git a/js/tiletypes.js b/js/tiletypes.js index 7644dcf..06c65db 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1445,6 +1445,12 @@ const TILE_TYPES = { cloner: { layer: LAYERS.terrain, blocks_collision: COLLISION.real_player | COLLISION.block_cc1 | COLLISION.monster_solid, + populate_defaults(me) { + me.arrows = 0; // bitmask of glowing arrows (visual, no gameplay impact) + }, + on_ready(me, level) { + me.arrows ??= 0; + }, traps(me, actor) { return ! actor._clone_release; }, diff --git a/js/util.js b/js/util.js index 6fe3628..02bc957 100644 --- a/js/util.js +++ b/js/util.js @@ -1,4 +1,4 @@ -import * as fflate from 'https://cdn.skypack.dev/fflate?min'; +import * as fflate from './vendor/fflate.mjs'; // Base class for custom errors export class LLError extends Error {} @@ -320,8 +320,6 @@ export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) { } } } -window.walk_grid = walk_grid; -// console.table(Array.from(walk_grid(13, 27.133854389190674, 12.90625, 27.227604389190674))) // Root class to indirect over where we might get files from // - a pool of uploaded in-memory files @@ -330,7 +328,7 @@ window.walk_grid = walk_grid; // - HTTP (but only for files we choose ourselves, not arbitrary ones, due to CORS) // Note that where possible, these classes lowercase all filenames, in keeping with C2G's implicit // requirement that filenames are case-insensitive :/ -class FileSource { +export class FileSource { constructor() {} // Get a file's contents as an ArrayBuffer diff --git a/js/vendor/fflate.mjs b/js/vendor/fflate.mjs new file mode 100644 index 0000000..2cc002f --- /dev/null +++ b/js/vendor/fflate.mjs @@ -0,0 +1,3 @@ +// fflate 0.6.7, obtained from https://cdn.skypack.dev/fflate?min +// original project at https://github.com/101arrowz/fflate +var gn={},bn=function(n,r,t,e,i){var a=gn[r]||(gn[r]=URL.createObjectURL(new Blob([n],{type:"text/javascript"}))),o=new Worker(a);return o.onerror=function(f){return i(f.error,null)},o.onmessage=function(f){return i(null,f.data)},o.postMessage(t,e),o},A=Uint8Array,R=Uint16Array,nr=Uint32Array,ur=new A([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),lr=new A([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Mr=new A([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),wn=function(n,r){for(var t=new R(31),e=0;e<31;++e)t[e]=r+=1<>>1|(T&21845)<<1;ir=(ir&52428)>>>2|(ir&13107)<<2,ir=(ir&61680)>>>4|(ir&3855)<<4,Ur[T]=((ir&65280)>>>8|(ir&255)<<8)>>>1}for(var V=function(n,r,t){for(var e=n.length,i=0,a=new R(r);i>>h]=s}else for(f=new R(e),i=0;i>>15-n[i]);return f},tr=new A(288),T=0;T<144;++T)tr[T]=8;for(var T=144;T<256;++T)tr[T]=9;for(var T=256;T<280;++T)tr[T]=7;for(var T=280;T<288;++T)tr[T]=8;for(var vr=new A(32),T=0;T<32;++T)vr[T]=5;var xn=V(tr,9,0),An=V(tr,9,1),Dn=V(vr,5,0),Mn=V(vr,5,1),Gr=function(n){for(var r=n[0],t=1;tr&&(r=n[t]);return r},X=function(n,r,t){var e=r/8|0;return(n[e]|n[e+1]<<8)>>(r&7)&t},Or=function(n,r){var t=r/8|0;return(n[t]|n[t+1]<<8|n[t+2]<<16)>>(r&7)},Cr=function(n){return(n/8|0)+(n&7&&1)},$=function(n,r,t){(r==null||r<0)&&(r=0),(t==null||t>n.length)&&(t=n.length);var e=new(n instanceof R?R:n instanceof nr?nr:A)(t-r);return e.set(n.subarray(r,t)),e},Fr=function(n,r,t){var e=n.length;if(!e||t&&!t.l&&e<5)return r||new A(0);var i=!r||t,a=!t||t.i;t||(t={}),r||(r=new A(e*3));var o=function(Tr){var Dr=r.length;if(Tr>Dr){var hr=new A(Math.max(Dr*2,Tr));hr.set(r),r=hr}},f=t.f||0,h=t.p||0,s=t.b||0,u=t.l,l=t.d,c=t.m,m=t.n,p=e*8;do{if(!u){t.f=f=X(n,h,1);var g=X(n,h+1,3);if(h+=3,g)if(g==1)u=An,l=Mn,c=9,m=5;else if(g==2){var y=X(n,h,31)+257,D=X(n,h+10,15)+4,F=y+X(n,h+5,31)+1;h+=14;for(var U=new A(F),x=new A(19),v=0;v>>4;if(w<16)U[v++]=w;else{var S=0,B=0;for(w==16?(B=3+X(n,h,3),h+=2,S=U[v-1]):w==17?(B=3+X(n,h,7),h+=3):w==18&&(B=11+X(n,h,127),h+=7);B--;)U[v++]=S}}var G=U.subarray(0,y),Z=U.subarray(y);c=Gr(G),m=Gr(Z),u=V(G,c,1),l=V(Z,m,1)}else throw"invalid block type";else{var w=Cr(h)+4,M=n[w-4]|n[w-3]<<8,z=w+M;if(z>e){if(a)throw"unexpected EOF";break}i&&o(s+M),r.set(n.subarray(w,z),s),t.b=s+=M,t.p=h=z*8;continue}if(h>p){if(a)throw"unexpected EOF";break}}i&&o(s+131072);for(var O=(1<>>4;if(h+=S&15,h>p){if(a)throw"unexpected EOF";break}if(!S)throw"invalid length/literal";if(Q<256)r[s++]=Q;else if(Q==256){j=h,u=null;break}else{var W=Q-254;if(Q>264){var v=Q-257,d=ur[v];W=X(n,h,(1<>>4;if(!_)throw"invalid distance";h+=_&15;var Z=zn[J];if(J>3){var d=lr[J];Z+=Or(n,h)&(1<p){if(a)throw"unexpected EOF";break}i&&o(s+131072);for(var q=s+W;s>>8},cr=function(n,r,t){t<<=r&7;var e=r/8|0;n[e]|=t,n[e+1]|=t>>>8,n[e+2]|=t>>>16},Er=function(n,r){for(var t=[],e=0;ec&&(c=a[e].s);var m=new R(c+1),p=Pr(t[u-1],m,0);if(p>r){var e=0,g=0,w=p-r,M=1<r)g+=M-(1<>>=w;g>0;){var y=a[e].s;m[y]=0&&g;--e){var D=a[e].s;m[D]==r&&(--m[D],++g)}p=r}return[new A(m),p]},Pr=function(n,r,t){return n.s==-1?Math.max(Pr(n.l,r,t+1),Pr(n.r,r,t+1)):r[n.s]=t},Vr=function(n){for(var r=n.length;r&&!n[--r];);for(var t=new R(++r),e=0,i=n[0],a=1,o=function(h){t[e++]=h},f=1;f<=r;++f)if(n[f]==i&&f!=r)++a;else{if(!i&&a>2){for(;a>138;a-=138)o(32754);a>2&&(o(a>10?a-11<<5|28690:a-3<<5|12305),a=0)}else if(a>3){for(o(i),--a;a>6;a-=6)o(8304);a>2&&(o(a-3<<5|8208),a=0)}for(;a--;)o(i);a=1,i=n[f]}return[t.subarray(0,e),r]},pr=function(n,r){for(var t=0,e=0;e>>8,n[i+2]=n[i]^255,n[i+3]=n[i+1]^255;for(var a=0;a4&&!k[Mr[I-1]];--I);var S=s+5<<3,B=pr(i,tr)+pr(a,vr)+o,G=pr(i,c)+pr(a,g)+o+14+3*I+pr(x,k)+(2*x[16]+3*x[17]+7*x[18]);if(S<=B&&S<=G)return kr(r,u,n.subarray(h,h+s));var Z,O,H,j;if(b(r,u,1+(G15&&(b(r,u,_[v]>>>5&127),u+=_[v]>>>12)}}else Z=xn,O=tr,H=Dn,j=vr;for(var v=0;v255){var J=e[v]>>>18&31;cr(r,u,Z[J+257]),u+=O[J+257],J>7&&(b(r,u,e[v]>>>23&31),u+=ur[J]);var q=e[v]&31;cr(r,u,H[q]),u+=j[q],q>3&&(cr(r,u,e[v]>>>5&8191),u+=lr[q])}else cr(r,u,Z[e[v]]),u+=O[e[v]];return cr(r,u,Z[256]),u+O[256]},Un=new nr([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),er=new A(0),Cn=function(n,r,t,e,i,a){var o=n.length,f=new A(e+o+5*(1+Math.ceil(o/7e3))+i),h=f.subarray(e,f.length-i),s=0;if(!r||o<8)for(var u=0;u<=o;u+=65535){var l=u+65535;l>>13,p=c&8191,g=(1<7e3||k>24576)&&Z>423){s=Xr(n,h,0,F,U,x,P,k,I,u-I,s),k=v=P=0,I=u;for(var O=0;O<286;++O)U[O]=0;for(var O=0;O<30;++O)x[O]=0}var H=2,j=0,Q=p,W=B-G&32767;if(Z>2&&S==D(u-W))for(var d=Math.min(m,Z)-1,_=Math.min(32767,u),J=Math.min(258,Z);W<=_&&--Q&&B!=G;){if(n[u+H]==n[u+H-W]){for(var q=0;qH){if(H=q,j=W,q>d)break;for(var Tr=Math.min(W,q-2),Dr=0,O=0;ODr&&(Dr=vn,G=hr)}}}B=G,G=w[B],W+=B-G+32768&32767}if(j){F[k++]=268435456|Ir[H]<<18|Qr[j];var cn=Ir[H]&31,pn=Qr[j]&31;P+=ur[cn]+lr[pn],++U[257+cn],++x[pn],N=u+H,++v}else F[k++]=n[u],++U[n[u]]}}s=Xr(n,h,a,F,U,x,P,k,I,u-I,s),!a&&s&7&&(s=kr(h,s+1,er))}return $(f,0,e+Cr(s)+i)},Fn=function(){for(var n=new nr(256),r=0;r<256;++r){for(var t=r,e=9;--e;)t=(t&1&&3988292384)^t>>>1;n[r]=t}return n}(),gr=function(){var n=-1;return{p:function(r){for(var t=n,e=0;e>>8;n=t},d:function(){return~n}}},$r=function(){var n=1,r=0;return{p:function(t){for(var e=n,i=r,a=t.length,o=0;o!=a;){for(var f=Math.min(o+2655,a);o>16),i=(i&65535)+15*(i>>16)}n=e,r=i},d:function(){return n%=65521,r%=65521,(n&255)<<24|n>>>8<<16|(r&255)<<8|r>>>8}}},sr=function(n,r,t,e,i){return Cn(n,r.level==null?6:r.level,r.mem==null?Math.ceil(Math.max(8,Math.min(13,Math.log(n.length)))*1.5):12+r.mem,t,e,!i)},Sr=function(n,r){var t={};for(var e in n)t[e]=n[e];for(var e in r)t[e]=r[e];return t},kn=function(n,r,t){for(var e=n(),i=n.toString(),a=i.slice(i.indexOf("[")+1,i.lastIndexOf("]")).replace(/ /g,"").split(","),o=0;o>>0},dr=function(n,r){return E(n,r)+E(n,r+4)*4294967296},C=function(n,r,t){for(;t;++r)n[r]=t,t>>>=8},_r=function(n,r){var t=r.filename;if(n[0]=31,n[1]=139,n[2]=8,n[8]=r.level<2?4:r.level==9?2:0,n[9]=3,r.mtime!=0&&C(n,4,Math.floor(new Date(r.mtime||Date.now())/1e3)),t){n[3]=8;for(var e=0;e<=t.length;++e)n[e+10]=t.charCodeAt(e)}},br=function(n){if(n[0]!=31||n[1]!=139||n[2]!=8)throw"invalid gzip data";var r=n[3],t=10;r&4&&(t+=n[10]|(n[11]<<8)+2);for(var e=(r>>3&1)+(r>>4&1);e>0;e-=!n[t++]);return t+(r&2)},Gn=function(n){var r=n.length;return(n[r-4]|n[r-3]<<8|n[r-2]<<16|n[r-1]<<24)>>>0},rn=function(n){return 10+(n.filename&&n.filename.length+1||0)},nn=function(n,r){var t=r.level,e=t==0?0:t<6?1:t==9?3:2;n[0]=120,n[1]=e<<6|(e?32-2*e:1)},On=function(n){if((n[0]&15)!=8||n[0]>>>4>7||(n[0]<<8|n[1])%31)throw"invalid zlib data";if(n[1]&32)throw"invalid zlib data: preset dictionaries not supported"};function tn(n,r){return!r&&typeof n=="function"&&(r=n,n={}),this.ondata=r,n}var rr=function(){function n(r,t){!t&&typeof r=="function"&&(t=r,r={}),this.ondata=t,this.o=r||{}}return n.prototype.p=function(r,t){this.ondata(sr(r,this.o,0,0,!t),t)},n.prototype.push=function(r,t){if(this.d)throw"stream finished";if(!this.ondata)throw"no stream handler";this.d=t,this.p(r,t||!1)},n}(),En=function(){function n(r,t){zr([yr,function(){return[L,rr]}],this,tn.call(this,r,t),function(e){var i=new rr(e.data);onmessage=L(i)},6)}return n}();function Pn(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[yr],function(e){return ar(Zr(e.data[0],e.data[1]))},0,t)}function Zr(n,r){return sr(n,r||{},0,0)}var K=function(){function n(r){this.s={},this.p=new A(0),this.ondata=r}return n.prototype.e=function(r){if(this.d)throw"stream finished";if(!this.ondata)throw"no stream handler";var t=this.p.length,e=new A(t+r.length);e.set(this.p),e.set(r,t),this.p=e},n.prototype.c=function(r){this.d=this.s.i=r||!1;var t=this.s.b,e=Fr(this.p,this.o,this.s);this.ondata($(e,t,this.s.b),this.d),this.o=$(e,this.s.b-32768),this.s.b=this.o.length,this.p=$(this.p,this.s.p/8|0),this.s.p&=7},n.prototype.push=function(r,t){this.e(r),this.c(t)},n}(),en=function(){function n(r){this.ondata=r,zr([wr,function(){return[L,K]}],this,0,function(){var t=new K;onmessage=L(t)},7)}return n}();function an(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[wr],function(e){return ar(xr(e.data[0],Lr(e.data[1])))},1,t)}function xr(n,r){return Fr(n,r)}var qr=function(){function n(r,t){this.c=gr(),this.l=0,this.v=1,rr.call(this,r,t)}return n.prototype.push=function(r,t){rr.prototype.push.call(this,r,t)},n.prototype.p=function(r,t){this.c.p(r),this.l+=r.length;var e=sr(r,this.o,this.v&&rn(this.o),t&&8,!t);this.v&&(_r(e,this.o),this.v=0),t&&(C(e,e.length-8,this.c.d()),C(e,e.length-4,this.l)),this.ondata(e,t)},n}(),Rn=function(){function n(r,t){zr([yr,Zn,function(){return[L,rr,qr]}],this,tn.call(this,r,t),function(e){var i=new qr(e.data);onmessage=L(i)},8)}return n}();function qn(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[yr,Zn,function(){return[Hr]}],function(e){return ar(Hr(e.data[0],e.data[1]))},2,t)}function Hr(n,r){r||(r={});var t=gr(),e=n.length;t.p(n);var i=sr(n,r,rn(r),8),a=i.length;return _r(i,r),C(i,a-8,t.d()),C(i,a-4,e),i}var Wr=function(){function n(r){this.v=1,K.call(this,r)}return n.prototype.push=function(r,t){if(K.prototype.e.call(this,r),this.v){var e=this.p.length>3?br(this.p):4;if(e>=this.p.length&&!t)return;this.p=this.p.subarray(e),this.v=0}if(t){if(this.p.length<8)throw"invalid gzip stream";this.p=this.p.subarray(0,-8)}K.prototype.c.call(this,t)},n}(),Hn=function(){function n(r){this.ondata=r,zr([wr,Bn,function(){return[L,K,Wr]}],this,0,function(){var t=new Wr;onmessage=L(t)},9)}return n}();function Wn(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[wr,Bn,function(){return[Yr]}],function(e){return ar(Yr(e.data[0]))},3,t)}function Yr(n,r){return Fr(n.subarray(br(n),-8),r||new A(Gn(n)))}var on=function(){function n(r,t){this.c=$r(),this.v=1,rr.call(this,r,t)}return n.prototype.push=function(r,t){rr.prototype.push.call(this,r,t)},n.prototype.p=function(r,t){this.c.p(r);var e=sr(r,this.o,this.v&&2,t&&4,!t);this.v&&(nn(e,this.o),this.v=0),t&&C(e,e.length-4,this.c.d()),this.ondata(e,t)},n}(),nt=function(){function n(r,t){zr([yr,Tn,function(){return[L,rr,on]}],this,tn.call(this,r,t),function(e){var i=new on(e.data);onmessage=L(i)},10)}return n}();function tt(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[yr,Tn,function(){return[fn]}],function(e){return ar(fn(e.data[0],e.data[1]))},4,t)}function fn(n,r){r||(r={});var t=$r();t.p(n);var e=sr(n,r,2,4);return nn(e,r),C(e,e.length-4,t.d()),e}var jr=function(){function n(r){this.v=1,K.call(this,r)}return n.prototype.push=function(r,t){if(K.prototype.e.call(this,r),this.v){if(this.p.length<2&&!t)return;this.p=this.p.subarray(2),this.v=0}if(t){if(this.p.length<4)throw"invalid zlib stream";this.p=this.p.subarray(0,-4)}K.prototype.c.call(this,t)},n}(),Yn=function(){function n(r){this.ondata=r,zr([wr,In,function(){return[L,K,jr]}],this,0,function(){var t=new jr;onmessage=L(t)},11)}return n}();function jn(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return mr(n,r,[wr,In,function(){return[Jr]}],function(e){return ar(Jr(e.data[0],Lr(e.data[1])))},5,t)}function Jr(n,r){return Fr((On(n),n.subarray(2,-4)),r)}var Jn=function(){function n(r){this.G=Wr,this.I=K,this.Z=jr,this.ondata=r}return n.prototype.push=function(r,t){if(!this.ondata)throw"no stream handler";if(this.s)this.s.push(r,t);else{if(this.p&&this.p.length){var e=new A(this.p.length+r.length);e.set(this.p),e.set(r,this.p.length)}else this.p=r;if(this.p.length>2){var i=this,a=function(){i.ondata.apply(i,arguments)};this.s=this.p[0]==31&&this.p[1]==139&&this.p[2]==8?new this.G(a):(this.p[0]&15)!=8||this.p[0]>>4>7||(this.p[0]<<8|this.p[1])%31?new this.I(a):new this.Z(a),this.s.push(this.p,t),this.p=null}}},n}(),et=function(){function n(r){this.G=Hn,this.I=en,this.Z=Yn,this.ondata=r}return n.prototype.push=function(r,t){Jn.prototype.push.call(this,r,t)},n}();function it(n,r,t){if(t||(t=r,r={}),typeof t!="function")throw"no callback";return n[0]==31&&n[1]==139&&n[2]==8?Wn(n,r,t):(n[0]&15)!=8||n[0]>>4>7||(n[0]<<8|n[1])%31?an(n,r,t):jn(n,r,t)}function at(n,r){return n[0]==31&&n[1]==139&&n[2]==8?Yr(n,r):(n[0]&15)!=8||n[0]>>4>7||(n[0]<<8|n[1])%31?xr(n,r):Jr(n,r)}var sn=function(n,r,t,e){for(var i in n){var a=n[i],o=r+i;a instanceof A?t[o]=[a,e]:Array.isArray(a)?t[o]=[a[0],Sr(e,a[1])]:sn(a,o+"/",t,e)}},Kn=typeof TextEncoder!="undefined"&&new TextEncoder,hn=typeof TextDecoder!="undefined"&&new TextDecoder,Nn=0;try{hn.decode(er,{stream:!0}),Nn=1}catch(n){}var Qn=function(n){for(var r="",t=0;;){var e=n[t++],i=(e>127)+(e>223)+(e>239);if(t+i>n.length)return[r,$(n,t-1)];i?i==3?(e=((e&15)<<18|(n[t++]&63)<<12|(n[t++]&63)<<6|n[t++]&63)-65536,r+=String.fromCharCode(55296|e>>10,56320|e&1023)):i&1?r+=String.fromCharCode((e&31)<<6|n[t++]&63):r+=String.fromCharCode((e&15)<<12|(n[t++]&63)<<6|n[t++]&63):r+=String.fromCharCode(e)}},ot=function(){function n(r){this.ondata=r,Nn?this.t=new TextDecoder:this.p=er}return n.prototype.push=function(r,t){if(!this.ondata)throw"no callback";if(t=!!t,this.t){if(this.ondata(this.t.decode(r,{stream:!0}),t),t){if(this.t.decode().length)throw"invalid utf-8 data";this.t=null}return}if(!this.p)throw"stream finished";var e=new A(this.p.length+r.length);e.set(this.p),e.set(r,this.p.length);var i=Qn(e),a=i[0],o=i[1];if(t){if(o.length)throw"invalid utf-8 data";this.p=null}else this.p=o;this.ondata(a,t)},n}(),ft=function(){function n(r){this.ondata=r}return n.prototype.push=function(r,t){if(!this.ondata)throw"no callback";if(this.d)throw"stream finished";this.ondata(or(r),this.d=t||!1)},n}();function or(n,r){if(r){for(var t=new A(n.length),e=0;e>1)),o=0,f=function(u){a[o++]=u},e=0;ea.length){var h=new A(o+8+(i-e<<1));h.set(a),a=h}var s=n.charCodeAt(e);s<128||r?f(s):s<2048?(f(192|s>>6),f(128|s&63)):s>55295&&s<57344?(s=65536+(s&1023<<10)|n.charCodeAt(++e)&1023,f(240|s>>18),f(128|s>>12&63),f(128|s>>6&63),f(128|s&63)):(f(224|s>>12),f(128|s>>6&63),f(128|s&63))}return $(a,0,o)}function un(n,r){if(r){for(var t="",e=0;e65535)throw"extra field too long";r+=e+4}return r},Ar=function(n,r,t,e,i,a,o,f){var h=e.length,s=t.extra,u=f&&f.length,l=fr(s);C(n,r,o!=null?33639248:67324752),r+=4,o!=null&&(n[r++]=20,n[r++]=t.os),n[r]=20,r+=2,n[r++]=t.flag<<1|(a==null&&8),n[r++]=i&&8,n[r++]=t.compression&255,n[r++]=t.compression>>8;var c=new Date(t.mtime==null?Date.now():t.mtime),m=c.getFullYear()-1980;if(m<0||m>119)throw"date not in range 1980-2099";if(C(n,r,m<<25|c.getMonth()+1<<21|c.getDate()<<16|c.getHours()<<11|c.getMinutes()<<5|c.getSeconds()>>>1),r+=4,a!=null&&(C(n,r,t.crc),C(n,r+4,a),C(n,r+8,t.size)),C(n,r+12,h),C(n,r+14,l),r+=16,o!=null&&(C(n,r,u),C(n,r+6,t.attrs),C(n,r+10,o),r+=14),n.set(e,r),r+=h,l)for(var p in s){var g=s[p],w=g.length;C(n,r,+p),C(n,r+2,w),n.set(g,r+4),r+=4+w}return u&&(n.set(f,r),r+=u),r},ln=function(n,r,t,e,i){C(n,r,101010256),C(n,r+8,t),C(n,r+10,t),C(n,r+12,e),C(n,r+16,i)},Br=function(){function n(r){this.filename=r,this.c=gr(),this.size=0,this.compression=0}return n.prototype.process=function(r,t){this.ondata(null,r,t)},n.prototype.push=function(r,t){if(!this.ondata)throw"no callback - add to ZIP archive before pushing";this.c.p(r),this.size+=r.length,t&&(this.crc=this.c.d()),this.process(r,t||!1)},n}(),st=function(){function n(r,t){var e=this;t||(t={}),Br.call(this,r),this.d=new rr(t,function(i,a){e.ondata(null,i,a)}),this.compression=8,this.flag=Vn(t.level)}return n.prototype.process=function(r,t){try{this.d.push(r,t)}catch(e){this.ondata(e,null,t)}},n.prototype.push=function(r,t){Br.prototype.push.call(this,r,t)},n}(),ht=function(){function n(r,t){var e=this;t||(t={}),Br.call(this,r),this.d=new En(t,function(i,a,o){e.ondata(i,a,o)}),this.compression=8,this.flag=Vn(t.level),this.terminate=this.d.terminate}return n.prototype.process=function(r,t){this.d.push(r,t)},n.prototype.push=function(r,t){Br.prototype.push.call(this,r,t)},n}(),ut=function(){function n(r){this.ondata=r,this.u=[],this.d=1}return n.prototype.add=function(r){var t=this;if(this.d&2)throw"stream finished";var e=or(r.filename),i=e.length,a=r.comment,o=a&&or(a),f=i!=r.filename.length||o&&a.length!=o.length,h=i+fr(r.extra)+30;if(i>65535)throw"filename too long";var s=new A(h);Ar(s,0,r,e,f);var u=[s],l=function(){for(var w=0,M=u;w65535&&S("filename too long",null),!I)S(null,z);else if(F<16e4)try{S(null,Zr(z,y))}catch(B){S(B,null)}else u.push(Pn(z,y,S))},p=0;p65535)throw"filename too long";var M=u?Zr(h,s):h,z=M.length,y=gr();y.p(h),e.push(Sr(s,{size:h.length,crc:y.d(),c:M,f:l,m:p,u:c!=o.length||p&&m.length!=g,o:i,compression:u})),i+=30+c+w+z,a+=76+2*(c+w)+(g||0)+z}for(var D=new A(a+22),F=i,U=a-i,x=0;x0){var i=Math.min(this.c,r.length),a=r.subarray(0,i);if(this.c-=i,this.d?this.d.push(a,!this.c):this.k[0].push(a),r=r.subarray(i),r.length)return this.push(r,t)}else{var o=0,f=0,h=void 0,s=void 0;this.p.length?r.length?(s=new A(this.p.length+r.length),s.set(this.p),s.set(r,this.p.length)):s=this.p:s=r;for(var u=s.length,l=this.c,c=l&&this.d,m=function(){var M,z=E(s,f);if(z==67324752){o=1,h=f,p.d=null,p.c=0;var y=Y(s,f+6),D=Y(s,f+8),F=y&2048,U=y&8,x=Y(s,f+26),v=Y(s,f+28);if(u>f+30+x+v){var P=[];p.k.unshift(P),o=2;var k=E(s,f+18),N=E(s,f+22),I=un(s.subarray(f+30,f+=30+x),!F);k==4294967295?(M=U?[-2]:Ln(s,f),k=M[0],N=M[1]):U&&(k=-1),f+=v,p.c=k;var S={name:I,compression:D,start:function(){if(!S.ondata)throw"no callback";if(!k)S.ondata(null,er,!0);else{var B=e.o[D];if(!B)throw"unknown compression type "+D;var G=k<0?new B(I):new B(I,k,N);G.ondata=function(j,Q,W){S.ondata(j,Q,W)};for(var Z=0,O=P;Z=0&&(S.size=k,S.originalSize=N),p.onfile(S)}return"break"}else if(l){if(z==134695760)return h=f+=12+(l==-2&&8),o=3,p.c=0,"break";if(z==33639248)return h=f-=4,o=3,p.c=0,"break"}},p=this;f65558){r("invalid zip file",null);return}var o=Y(n,a+8);o||r(null,{});var f=o,h=E(n,a+16),s=h==4294967295;if(s){if(a=E(n,a-12),E(n,a)!=101075792){r("invalid zip file",null);return}f=o=E(n,a+32),h=E(n,a+48)}for(var u=function(c){var m=$n(n,h,s),p=m[0],g=m[1],w=m[2],M=m[3],z=m[4],y=m[5],D=Xn(n,y);h=z;var F=function(x,v){x?(e(),r(x,null)):(i[M]=v,--o||r(null,i))};if(!p)F(null,$(n,D,D+g));else if(p==8){var U=n.subarray(D,D+g);if(g<32e4)try{F(null,xr(U,new A(w)))}catch(x){F(x,null)}else t.push(an(U,{size:w},F))}else F("unknown compression type "+p,null)},l=0;l65558)throw"invalid zip file";var e=Y(n,t+8);if(!e)return{};var i=E(n,t+16),a=i==4294967295;if(a){if(t=E(n,t-12),E(n,t)!=101075792)throw"invalid zip file";e=E(n,t+32),i=E(n,t+48)}for(var o=0;o