From ba8649186745b219f362b56f0b43d63d80a94a66 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:05:47 -0700 Subject: [PATCH 01/12] Add design spec and mockups for Zork 1-3 arcade integration Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/2026-07-07-zork-arcade-design.md | 171 ++++++++++++++++++ mockups/zork-arcade-menu.svg | 58 ++++++ mockups/zork-terminal.svg | 66 +++++++ 3 files changed, 295 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-07-zork-arcade-design.md create mode 100644 mockups/zork-arcade-menu.svg create mode 100644 mockups/zork-terminal.svg diff --git a/docs/superpowers/specs/2026-07-07-zork-arcade-design.md b/docs/superpowers/specs/2026-07-07-zork-arcade-design.md new file mode 100644 index 0000000000..cd85b40bc9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-zork-arcade-design.md @@ -0,0 +1,171 @@ +# Zork 1–3 in the New Tokyo Arcade — Design + +**Date:** 2026-07-07 +**Author:** Andrew Faust (with Claude) +**Status:** Approved direction: source mod, targeting upstream acceptance + +## Goal + +Add Zork I, Zork II, and Zork III as playable games in the New Tokyo Arcade. The player +walks to the Arcade, picks a Zork cabinet from the menu, and plays the real game in a +terminal UI that matches Bitburner's theme. The implementation must be clean enough to +submit as a PR to mainline Bitburner. + +## Decisions (settled with the user) + +- **Route:** Source mod in this fork (`bitburner-zork`), not user-mode scripts. +- **Upstream intent:** The work targets acceptance into mainline Bitburner. That drives + every technical choice: no new npm dependencies, no jQuery, no WASM, small bundle + impact, all third-party material isolated in one folder. +- **Licensing:** Bitburner is Apache 2.0 + Commons Clause. All MIT-licensed material + (interpreter code and game files) lives in a single subfolder with its own + `LICENSE.md` scoped to that folder only. +- **Gating/reward:** None. The cabinets are pure flavor — always playable, no unlock, + no in-game reward. +- **Saves:** Zork's `save`/`restore` commands persist to browser storage + (localStorage), keyed per game. Works identically in web and Electron (same + mechanism Bitburner's own saves use). Not part of the Bitburner save file; does not + travel with save export or Steam Cloud. This matches the persistence guarantees + Bitburner's own web save already has. + +## Architecture + +Parchment's modern engine for Z-machine games is Bocfel (a ~500KB WASM blob). We do +not use it. The Parchment project also maintains **ZVM** (from ifvms.js, pure JS, MIT), +which fully supports Z-machine v3 — the format of all three Zork files. Pure JS is the +right call for an upstream PR. + +Verified interpreter stack (all MIT): + +``` +zork1.z3 (Uint8Array, ~87KB each) + → ZVM (ifvms 1.1.6, dist/zvm.js, 104K — the Z-machine) + → glkapi.js (GlkOte 2.3.7, 224K — Glk API layer, zero deps, no DOM) + → GlkOte display (OURS — React component implementing the GlkOte interface) + → Dialog (OURS — minimal file-prompt layer backed by localStorage) +``` + +Key verified facts: + +- ZVM's API (`src/zvm.js` in the ifvms package): `prepare(storydata, {Glk})` then + `init()/start()`. It requires a Glk API object — it makes `glk_*` calls, it does not + speak the GlkOte JSON protocol directly. +- `glkapi.js` implements that Glk API and translates to GlkOte display updates. It has + **no dependencies** — no jQuery, no DOM. jQuery only appears in `glkote.js` (the + stock display layer), which we replace entirely with React. +- Zork v3 needs a small slice of the display protocol: one grid window (the status + line), one buffer window (scrolling story text), and line input. Char input + ("press any key") is also supported for completeness. +- Save/restore flows through `glk_fileref_create_by_prompt` into the Dialog layer, + producing Quetzal-format blobs of a few KB. + +## File layout + +Everything new lives under `src/Arcade/Zork/`: + +``` +src/Arcade/Zork/ + LICENSE.md # MIT licenses: ifvms.js, GlkOte (glkapi.js), Zork I-III + README.md # what this folder is, where the pieces came from, versions + vendor/ + zvm.js # verbatim from ifvms 1.1.6 dist/ (MIT header intact) + glkapi.js # verbatim from GlkOte 2.3.7 (MIT header intact) + games/ + zork1.z3 # compiled game files (MIT, Microsoft open-source release) + zork2.z3 + zork3.z3 + ZorkRoot.tsx # cabinet component: loads a .z3, wires ZVM+Glk+display + ZorkTerminal.tsx # React terminal: status grid, buffer, input line + GlkOteReact.ts # GlkOte-interface implementation that drives React state + ZorkDialog.ts # Dialog implementation; saves to localStorage + metadata.ts # game list: title, filename, per-game save key +``` + +Changed existing files (kept minimal for PR review): + +- `src/Arcade/ui/ArcadeRoot.tsx` — add `Page.Zork1/Zork2/Zork3` enum values, three + menu buttons, three switch cases rendering ``. +- `webpack.config.js` — add `.z3` to the `asset/resource` rule (one-line change) so + game files bundle as hashed assets. +- Vendored files excluded from ESLint/Prettier (config ignore entries) — they are + third-party code kept verbatim. + +## Component design + +**ZorkRoot.tsx** — mounts per cabinet. Fetches the bundled `.z3` asset as an +ArrayBuffer, instantiates ZVM, `prepare()`s it with the Glk object from `glkapi.js`, +provides our GlkOteReact display and ZorkDialog, and starts the machine. Unmount +disposes listeners. Engine errors are caught and surfaced via Bitburner's alert +mechanism with a "cabinet resets" flavor, returning to the arcade menu. + +**GlkOteReact.ts** — implements the GlkOte interface `glkapi.js` expects (`init`, +`update`, `error`, `getinterface`, ...). `update(state)` parses window/content/input +updates into a plain state object and pushes it to React via a subscription. Input +events (line submit, keypress) go back through the `accept` callback. + +**ZorkTerminal.tsx** — renders that state: + +- Status line: inverse-video bar (theme primary as background, black text) showing the + grid window contents (room name, score/moves) — exactly how Infocom terps did it. +- Buffer: scrolling story text, auto-scrolled to bottom, text styles (bold/italic) + honored. +- Input: inline text field at the prompt with a blinking block cursor. Keyboard events + are captured (`stopPropagation`) while the cabinet has focus so Bitburner hotkeys + don't fire mid-game. Up/down arrow recalls command history (session-scoped). +- All colors from `Settings.theme` (primary, background, etc.) so player themes apply. +- A "Back" button (matching Megabyte cabinet convention) leaves the game; in-game + `quit` also returns to the arcade menu. + +**ZorkDialog.ts** — implements the minimal Dialog contract glkapi uses for +save/restore/transcript prompts. Saves serialize to base64 in localStorage under +`zork..`; restore lists/reads them back. Transcript and command-record +streams are declined politely (not supported). + +## Licensing plan + +`src/Arcade/Zork/LICENSE.md` contains, clearly separated: + +1. The MIT license for ifvms.js (The ifvms.js team) — covers `vendor/zvm.js`. +2. The MIT license for GlkOte (Andrew Plotkin) — covers `vendor/glkapi.js`. +3. The MIT license for Zork I/II/III (Microsoft's open-source release) — covers + `games/*.z3`. + +The file opens with a scope statement: the license applies **only** to the contents of +`src/Arcade/Zork/`; the rest of Bitburner remains under its own license (see root +`license.txt`). The folder README records upstream versions/commits so future +maintainers can refresh the vendored files. + +## Error handling + +- Story file fails to load (asset fetch error): alert "This machine is broken." (the + established arcade idiom) and return to menu. +- ZVM runtime fault: catch, log to console, same "machine is broken" alert, return to + menu. No crash may escape into Bitburner's React tree (error boundary around the + cabinet). +- Corrupt save blob on restore: ZVM reports restore failure in-game (native behavior); + we additionally guard JSON/base64 decode and treat failures as "no such save". + +## Testing + +- **Headless engine test (jest):** load `zork1.z3` in ZVM with a scripted GlkOte stub, + feed `open mailbox` / `read leaflet`, assert expected output text appears. This + proves the vendored interpreter + our glue works without a browser. +- **Display state test:** feed recorded GlkOte update payloads into GlkOteReact's + parser, assert the resulting terminal state (status line text, buffer lines). +- **Save round-trip test:** save → localStorage entry exists → restore succeeds. +- Manual: play all three games' openings in web + Electron dev builds; verify theme + switching, keyboard capture, Back button, save/restore across an app restart. + +## Non-goals + +- No achievements, unlocks, stats, or any mechanical coupling to Bitburner systems. +- No support for other Z-machine games or a general IF player (YAGNI — but nothing + prevents it later; the Zork folder boundary keeps that door open). +- No transcript/scripting support, no sound, no graphics (v3 Zork uses none). +- Zork saves are not in the Bitburner save file (revisitable before the PR if + upstream prefers it). + +## Mockups + +See `mockups/zork-arcade-menu.svg` (arcade menu with three new cabinets) and +`mockups/zork-terminal.svg` (Zork I running: status bar, story buffer, prompt). diff --git a/mockups/zork-arcade-menu.svg b/mockups/zork-arcade-menu.svg new file mode 100644 index 0000000000..2b3252a2f8 --- /dev/null +++ b/mockups/zork-arcade-menu.svg @@ -0,0 +1,58 @@ + + + + + + + + + Hacking + Terminal + Script Editor + Active Scripts + Character + Stats + Factions + Augmentations + World + City + Travel + Job + Help + Documentation + Achievements + Options + + + + + < Return to World + Arcade + New Tokyo + + + + Megabyte burner 2000 + + + + Zork I: The Great Underground Empire + + + Zork II: The Wizard of Frobozz + + + Zork III: The Dungeon Master + + + A dusty row of cabinets hums in the corner. Three of them glow with + an ancient phosphor light, as if they have been running since 1980. + + + + + + ← new buttons added to + ArcadeRoot.tsx menu + + diff --git a/mockups/zork-terminal.svg b/mockups/zork-terminal.svg new file mode 100644 index 0000000000..6f265217f7 --- /dev/null +++ b/mockups/zork-terminal.svg @@ -0,0 +1,66 @@ + + + + + + + + Back + + + *** ZORK I: THE GREAT UNDERGROUND EMPIRE *** + + + + + + + West of House + Score: 0 Moves: 2 + + + + ZORK I: The Great Underground Empire + Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved. + ZORK is a registered trademark of Infocom, Inc. + Revision 88 / Serial number 840726 + + West of House + You are standing in an open field west of a white house, with a + boarded front door. + There is a small mailbox here. + + >open mailbox + Opening the small mailbox reveals a leaflet. + + >read leaflet + (Taken) + "WELCOME TO ZORK! + ZORK is a game of adventure, danger, and low cunning. In it you + will explore some of the most amazing territory ever seen by + mortals. No computer should be without one!" + + + > + + + + + + + Type 'save' to save your progress. Type 'quit' to return to the arcade. + + + + + ← status line: Z-machine + grid window (room, + score, moves) + ← scrolling buffer + window, colors from + Settings.theme + ← inline input, blinking + cursor; keys captured + so hotkeys don't fire + + From 7241198d81aab3afc79426d4c22c043a4be78fb5 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:13:27 -0700 Subject: [PATCH 02/12] Add Zork arcade implementation plan Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-07-zork-arcade.md | 1374 +++++++++++++++++ 1 file changed, 1374 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-zork-arcade.md diff --git a/docs/superpowers/plans/2026-07-07-zork-arcade.md b/docs/superpowers/plans/2026-07-07-zork-arcade.md new file mode 100644 index 0000000000..af855d9b46 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-zork-arcade.md @@ -0,0 +1,1374 @@ +# Zork 1–3 Arcade Integration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Zork I, II, and III playable from the New Tokyo Arcade menu via the Parchment project's pure-JS ZVM interpreter, in a Bitburner-themed React terminal. + +**Architecture:** Vendored `zvm.js` (ifvms 1.1.6) makes `glk_*` calls into vendored `glkapi.js` (GlkOte 2.3.7); we implement the GlkOte display interface (`GlkOteReact`) as a state parser that feeds a React terminal component, and a minimal Dialog (`ZorkDialog`) that persists saves to localStorage. `ArcadeRoot.tsx` gets three new menu entries. Spec: `docs/superpowers/specs/2026-07-07-zork-arcade-design.md`. + +**Tech Stack:** React 17, TypeScript 5.9, MUI 5, webpack 5, jest 29 (jsdom). No new npm dependencies. + +## Global Constraints + +- No new entries in `package.json` dependencies or devDependencies. +- Vendored files (`src/Arcade/Zork/vendor/zvm.js`, `src/Arcade/Zork/vendor/glkapi.js`) are kept **verbatim** — never lint, format, or hand-edit them. +- All MIT-licensed material lives under `src/Arcade/Zork/`; `LICENSE.md` there scopes MIT to that folder only. +- All UI colors come from `Settings.theme` (import: `import { Settings } from "../../Settings/Settings";`) — no hardcoded colors except black text on the inverse status bar. +- No gating, no rewards: cabinets always playable. +- Existing commands must stay green: `npm run lint:report`, `npm run format:report`, `npm run test`. +- Commit after every task (no `--no-verify`). + +## Reference: verified third-party APIs + +Facts verified by reading the packages (do not re-derive): + +- **ZVM** (`ifvms@1.1.6`, `dist/zvm.js`, CommonJS export of a constructor): + `const vm = new ZVM(); vm.prepare(storyBytes, options); Glk.init(options)` where + `options = { vm, Glk, GlkOte, Dialog }`. `Glk.init` sets `options.accept` and calls + `GlkOte.init(options)`; the display then sends `{type:"init", gen:0, metrics}` through + `accept`, which starts the VM. Everything runs synchronously until the VM blocks on + `glk_select`, at which point `GlkOte.update(state)` is called. +- **glkapi.js** (GlkOte 2.3.7): exports `{ Glk, GlkClass }` (CommonJS, `new GlkClass()` + per session). Calls on the display object: `init(iface)`, `update(state)`, `log(msg)`, + `warning(msg)`, `error(msg)`, `getlibrary(name)` (asked for `"Dialog"` and `"Blorb"`), + `save_allstate()`. Calls on Dialog (non-streaming, `Dialog.streaming === false`): + `file_construct_ref(filename, usage, gameid)`, `file_construct_temp_ref(usage)`, + `file_ref_exists(ref)`, `file_remove_ref(ref)`, `file_write(ref, content, israw)`, + `file_read(ref, israw)`, `file_clean_fixed_name(filename, usage)`. Save/restore + prompts arrive at the display as `update.specialinput = + {type:"fileref_prompt", filemode, filetype, gameid}`; the display answers + `accept({type:"specialresponse", gen, response:"fileref_prompt", value})` where + `value` is `{filename, usage}` or `null` for cancel. +- **GlkOte update payload:** `{type:"update", gen, windows?, content?, input?, specialinput?, disable?}`. + `windows`: `[{id, type:"grid"|"buffer", rock, gridwidth?, gridheight?}]`. + Grid content: `{id, lines:[{line, content}]}`. Buffer content: + `{id, clear?, text:[{append?, flowbreak?, content?}]}`. A `content` array holds either + `{style, text}` objects or alternating `"style", "text"` strings. `input`: + `[{id, gen, type:"line"|"char", maxlen?, initial?}]`. +- Zork 1–3 are Z-machine v3; ZVM itself draws the status line into a grid window. + +--- + +### Task 1: Vendor drop, game files, licensing, toolchain config + +**Files:** +- Create: `src/Arcade/Zork/vendor/zvm.js` (verbatim from ifvms 1.1.6) +- Create: `src/Arcade/Zork/vendor/glkapi.js` (verbatim from GlkOte 2.3.7) +- Create: `src/Arcade/Zork/vendor/vendor.d.ts` +- Create: `src/Arcade/Zork/games/zork1.z3`, `zork2.z3`, `zork3.z3` (from `C:\git\Zork`) +- Create: `src/Arcade/Zork/LICENSE.md`, `src/Arcade/Zork/README.md` +- Modify: `webpack.config.js` (asset rule, line ~148) +- Modify: `src/@types/global.d.ts` (add `*.z3` module) +- Modify: `jest.config.js` (moduleNameMapper for `.z3`) +- Modify: `.prettierignore` and ESLint ignore config (create/extend) + +**Interfaces:** +- Produces: `require("src/Arcade/Zork/vendor/zvm.js")` → ZVM constructor; + `require("src/Arcade/Zork/vendor/glkapi.js")` → `{ Glk, GlkClass }`; + importable `*.z3` asset URLs. + +- [ ] **Step 1: Fetch vendor sources and copy game files** + +```bash +mkdir -p /tmp/zorkvendor && cd /tmp/zorkvendor +curl -sL https://registry.npmjs.org/ifvms/-/ifvms-1.1.6.tgz -o ifvms.tgz && tar -xzf ifvms.tgz +git clone --depth 1 https://github.com/erkyrath/glkote glkote +mkdir -p /c/git/bitburner-zork/src/Arcade/Zork/vendor /c/git/bitburner-zork/src/Arcade/Zork/games +cp package/dist/zvm.js /c/git/bitburner-zork/src/Arcade/Zork/vendor/zvm.js +cp glkote/glkapi.js /c/git/bitburner-zork/src/Arcade/Zork/vendor/glkapi.js +cp /c/git/Zork/zork1.z3 /c/git/Zork/zork2.z3 /c/git/Zork/zork3.z3 /c/git/bitburner-zork/src/Arcade/Zork/games/ +``` + +Verify: `head -10` of both vendor files shows their original MIT headers; `ls -l src/Arcade/Zork/games` shows three files ~87–93KB. + +- [ ] **Step 2: Fetch the exact Zork MIT license text** + +```bash +curl -sL https://raw.githubusercontent.com/historicalsource/zork1/master/LICENSE -o /tmp/zorkvendor/zork-license.txt +head -5 /tmp/zorkvendor/zork-license.txt +``` + +Expected: MIT license text with a Microsoft (or Infocom successor) copyright line. **Use this exact text in LICENSE.md below — do not paraphrase or invent the copyright line.** If the fetch fails, STOP and ask the user for the license text that shipped with their Zork copies. + +- [ ] **Step 3: Write `src/Arcade/Zork/LICENSE.md`** + +Structure (fill the license bodies with the exact texts gathered in steps 1–2; ifvms license is in `/tmp/zorkvendor/package/LICENSE`, GlkOte's in `/tmp/zorkvendor/glkote/LICENSE`): + +```markdown +# Licenses for src/Arcade/Zork/ + +Everything in this folder (and only this folder) is MIT-licensed third-party +material plus Bitburner glue code. The rest of Bitburner is licensed separately — +see `license.txt` in the repository root. + +## ZVM interpreter — `vendor/zvm.js` +From ifvms.js 1.1.6 (https://github.com/curiousdannii/ifvms.js), part of the +Parchment project. + +[exact MIT text, Copyright (c) 2011-2017 Dannii Willis and other contributors] + +## Glk API layer — `vendor/glkapi.js` +From GlkOte 2.3.7 (https://github.com/erkyrath/glkote). + +[exact MIT text, Copyright (c) 2008-2025, Andrew Plotkin] + +## Zork I, II, III — `games/*.z3` +Compiled Z-machine story files, open-sourced under MIT +(https://github.com/historicalsource/zork1 et al.). + +[exact MIT text from step 2] +``` + +- [ ] **Step 4: Write `src/Arcade/Zork/README.md`** + +```markdown +# Zork in the Arcade + +Zork I–III cabinets for the New Tokyo Arcade, powered by the Parchment +project's pure-JS Z-machine (ZVM). + +- `vendor/zvm.js` — ifvms.js 1.1.6 `dist/zvm.js`, verbatim. Do not edit/format. +- `vendor/glkapi.js` — GlkOte 2.3.7 `glkapi.js`, verbatim. Do not edit/format. +- `games/*.z3` — compiled Zork story files (MIT, Microsoft open-source release). +- Everything else — Bitburner glue: React terminal, GlkOte display adapter, + localStorage save Dialog. + +Licensing: see LICENSE.md (scoped to this folder only). +To update vendor files, re-fetch from upstream and update versions here. +Design spec: docs/superpowers/specs/2026-07-07-zork-arcade-design.md +``` + +- [ ] **Step 5: Toolchain config** + +`webpack.config.js` — extend the existing asset rule: + +```js +{ test: /\.(ttf|woff2|png|jpe?g|gif|jp2|webp|svg|z3)$/, type: "asset/resource" }, +``` + +`src/@types/global.d.ts` — append: + +```ts +declare module "*.z3" { + const url: string; + export default url; +} +``` + +`jest.config.js` — add to `moduleNameMapper`: + +```js +"\\.z3$": "/test/__mocks__/fileMock.js", +``` + +`.prettierignore` (create if missing) — add: + +``` +src/Arcade/Zork/vendor/ +src/Arcade/Zork/games/ +``` + +ESLint: check for `.eslintignore` or `ignorePatterns` in the ESLint config; add `src/Arcade/Zork/vendor/` the same way the repo already ignores things (if nothing is ignored yet, add `ignorePatterns: ["src/Arcade/Zork/vendor/"]` to the root ESLint config). + +- [ ] **Step 6: `src/Arcade/Zork/vendor/vendor.d.ts`** + +```ts +// Type declarations for the verbatim vendored MIT-licensed interpreter files. +declare module "*/vendor/zvm.js" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ZVM: new () => any; + export = ZVM; +} +declare module "*/vendor/glkapi.js" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const Glk: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const GlkClass: new () => any; +} +``` + +- [ ] **Step 7: Verify toolchain is green** + +Run: `npm run lint:report && npm run format:report && npm run test` +Expected: all pass; vendor files not flagged. If prettier flags LICENSE.md/README.md, run `npm run format`. + +- [ ] **Step 8: Commit** + +```bash +git add src/Arcade/Zork webpack.config.js src/@types/global.d.ts jest.config.js .prettierignore +git commit -m "Arcade: vendor ZVM + glkapi and Zork 1-3 story files (MIT, scoped LICENSE.md)" +``` + +--- + +### Task 2: ZorkDialog — localStorage-backed Glk file layer (TDD) + +**Files:** +- Create: `src/Arcade/Zork/ZorkDialog.ts` +- Test: `test/Arcade/Zork/ZorkDialog.test.ts` + +**Interfaces:** +- Produces: `class ZorkDialog { constructor(gameKey: string) }` implementing the + non-streaming Dialog contract listed in the Reference section. `FileRef` type: + `{ filename: string; usage: string; gameid?: string }`. Storage key format: + `zork...`. + +- [ ] **Step 1: Write the failing test** + +```ts +import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; + +describe("ZorkDialog", () => { + beforeEach(() => localStorage.clear()); + + it("round-trips binary save data through localStorage", () => { + const dialog = new ZorkDialog("zork1"); + const ref = dialog.file_construct_ref("slot1", "save", ""); + expect(dialog.file_ref_exists(ref)).toBe(false); + const bytes = [0, 1, 2, 127, 128, 255, 66]; + expect(dialog.file_write(ref, bytes, true)).toBe(true); + expect(dialog.file_ref_exists(ref)).toBe(true); + expect(dialog.file_read(ref, true)).toEqual(bytes); + expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); + }); + + it("keeps games separate and removes refs", () => { + const d1 = new ZorkDialog("zork1"); + const d2 = new ZorkDialog("zork2"); + const ref = d1.file_construct_ref("slot1", "save", ""); + d1.file_write(ref, [1], true); + expect(d2.file_ref_exists(d2.file_construct_ref("slot1", "save", ""))).toBe(false); + d1.file_remove_ref(ref); + expect(d1.file_ref_exists(ref)).toBe(false); + expect(d1.file_read(ref, true)).toBeNull(); + }); + + it("cleans fixed names and constructs temp refs", () => { + const d = new ZorkDialog("zork1"); + expect(d.file_clean_fixed_name('a/b\\c:d"e', "save")).toBe("abcde"); + const temp = d.file_construct_temp_ref("data"); + expect(temp.filename.startsWith("_temp")).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx jest test/Arcade/Zork/ZorkDialog.test.ts` +Expected: FAIL — cannot find module ZorkDialog. + +- [ ] **Step 3: Implement `src/Arcade/Zork/ZorkDialog.ts`** + +```ts +/** + * Minimal non-streaming Glk Dialog implementation backed by localStorage. + * Implements the subset of the GlkOte Dialog contract that glkapi.js uses, + * persisting Zork save files per game. See src/Arcade/Zork/README.md. + */ +export interface ZorkFileRef { + filename: string; + usage: string; + gameid?: string; +} + +export class ZorkDialog { + /** glkapi checks this to pick the non-streaming file API. */ + readonly streaming = false; + private readonly gameKey: string; + + constructor(gameKey: string) { + this.gameKey = gameKey; + } + + private key(ref: ZorkFileRef): string { + return `zork.${this.gameKey}.${ref.usage}.${ref.filename}`; + } + + file_construct_ref(filename = "", usage = "", gameid = ""): ZorkFileRef { + return { filename, usage, gameid }; + } + + file_construct_temp_ref(usage = ""): ZorkFileRef { + return { filename: `_temp_${usage}`, usage, gameid: "" }; + } + + file_clean_fixed_name(filename: string, _usage: string): string { + return filename.replace(/["/\\<>:|?*]/g, ""); + } + + file_ref_exists(ref: ZorkFileRef): boolean { + return localStorage.getItem(this.key(ref)) !== null; + } + + file_remove_ref(ref: ZorkFileRef): void { + localStorage.removeItem(this.key(ref)); + } + + /** content is an array of byte values (israw) or a string. Returns success. */ + file_write(ref: ZorkFileRef, content: number[] | string, _israw?: boolean): boolean { + try { + const payload = typeof content === "string" ? { text: content } : { bytes: btoa(String.fromCharCode(...content)) }; + localStorage.setItem(this.key(ref), JSON.stringify(payload)); + return true; + } catch (e) { + console.error("ZorkDialog.file_write failed", e); + return false; + } + } + + file_read(ref: ZorkFileRef, _israw?: boolean): number[] | string | null { + const raw = localStorage.getItem(this.key(ref)); + if (raw === null) return null; + try { + const payload = JSON.parse(raw) as { text?: string; bytes?: string }; + if (typeof payload.text === "string") return payload.text; + if (typeof payload.bytes === "string") { + return Array.from(atob(payload.bytes), (c) => c.charCodeAt(0)); + } + return null; + } catch { + return null; // corrupt entry: treat as missing (spec: error handling) + } + } + + /** ZVM autosave hooks; unused (we don't enable do_vm_autosave), kept safe. */ + autosave_read(_signature: string): null { + return null; + } + autosave_write(_signature: string, _snapshot: unknown): void { + // no-op + } +} +``` + +Note: `String.fromCharCode(...content)` is fine for save-sized arrays (a few KB); if implementation review worries about arg limits, chunk it — but do not prematurely complicate. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx jest test/Arcade/Zork/ZorkDialog.test.ts` +Expected: PASS (3 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/Arcade/Zork/ZorkDialog.ts test/Arcade/Zork/ZorkDialog.test.ts +git commit -m "Arcade: Zork save-file Dialog layer backed by localStorage" +``` + +--- + +### Task 3: GlkOteReact — display adapter / state parser (TDD) + +**Files:** +- Create: `src/Arcade/Zork/GlkOteReact.ts` +- Test: `test/Arcade/Zork/GlkOteReact.test.ts` + +**Interfaces:** +- Consumes: `ZorkDialog` from Task 2. +- Produces: + +```ts +export interface StyledRun { style: string; text: string } +export interface TerminalState { + gridLines: StyledRun[][]; // status-line rows + bufferLines: StyledRun[][]; // scrollback; one entry per display line + inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; + filePrompt: { filemode: string; filetype: string } | null; + disabled: boolean; // true after glk_exit / while no input + error: string | null; +} +export type TerminalListener = (state: TerminalState) => void; +export class GlkOteReact { + constructor(dialog: ZorkDialog, listener: TerminalListener); + // GlkOte contract (called by glkapi): init, update, log, warning, error, + // getlibrary, getinterface, save_allstate + // Outgoing (called by UI / session): + sendLine(text: string): void; + sendChar(key: string): void; + sendFileref(value: { filename: string; usage: string } | null): void; + detach(): void; +} +``` + +- [ ] **Step 1: Write the failing test** + +```ts +import { GlkOteReact, TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; +import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; + +function makeGlkOte(): { glkote: GlkOteReact; states: TerminalState[]; accepted: unknown[] } { + const states: TerminalState[] = []; + const accepted: unknown[] = []; + const glkote = new GlkOteReact(new ZorkDialog("test"), (s) => states.push(s)); + glkote.init({ accept: (ev: unknown) => accepted.push(ev) }); + return { glkote, states, accepted }; +} + +describe("GlkOteReact", () => { + it("sends an init event with metrics on init()", () => { + const { accepted } = makeGlkOte(); + expect(accepted).toHaveLength(1); + const ev = accepted[0] as { type: string; gen: number; metrics: { width: number } }; + expect(ev.type).toBe("init"); + expect(ev.metrics.width).toBeGreaterThan(0); + }); + + it("parses grid and buffer content updates (both run formats)", () => { + const { glkote, states } = makeGlkOte(); + glkote.update({ + type: "update", + gen: 1, + windows: [ + { id: 1, type: "grid", rock: 202, gridwidth: 80, gridheight: 1 }, + { id: 2, type: "buffer", rock: 201 }, + ], + content: [ + { id: 1, lines: [{ line: 0, content: ["normal", " West of House"] }] }, + { + id: 2, + text: [ + { content: [{ style: "normal", text: "You are standing in an open field" }] }, + { append: true, content: ["normal", " west of a white house."] }, + ], + }, + ], + input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], + }); + const s = states[states.length - 1]; + expect(s.gridLines[0].map((r) => r.text).join("")).toContain("West of House"); + expect(s.bufferLines).toHaveLength(1); + expect(s.bufferLines[0].map((r) => r.text).join("")).toBe( + "You are standing in an open field west of a white house.", + ); + expect(s.inputRequest).toMatchObject({ id: 2, type: "line" }); + }); + + it("clears buffer on clear, surfaces file prompts, and echoes line events back", () => { + const { glkote, states, accepted } = makeGlkOte(); + glkote.update({ + type: "update", + gen: 1, + windows: [{ id: 2, type: "buffer", rock: 201 }], + content: [{ id: 2, text: [{ content: ["normal", "old text"] }] }], + input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], + }); + glkote.sendLine("save"); + const lineEv = accepted[accepted.length - 1] as { type: string; window: number; value: string }; + expect(lineEv).toMatchObject({ type: "line", window: 2, value: "save" }); + + glkote.update({ type: "update", gen: 2, specialinput: { type: "fileref_prompt", filemode: "write", filetype: "save" } }); + expect(states[states.length - 1].filePrompt).toMatchObject({ filemode: "write" }); + + glkote.sendFileref({ filename: "slot1", usage: "save" }); + const frEv = accepted[accepted.length - 1] as { type: string; response: string }; + expect(frEv).toMatchObject({ type: "specialresponse", response: "fileref_prompt" }); + + glkote.update({ type: "update", gen: 3, content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }] }); + const s = states[states.length - 1]; + expect(s.bufferLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toBe("fresh"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx jest test/Arcade/Zork/GlkOteReact.test.ts` +Expected: FAIL — cannot find module GlkOteReact. + +- [ ] **Step 3: Implement `src/Arcade/Zork/GlkOteReact.ts`** + +```ts +/** + * A GlkOte-protocol display implementation that feeds React state instead of + * manipulating the DOM (replaces glkote.js, which requires jQuery). + * glkapi.js (vendor/) calls the GlkOte contract methods; the terminal UI calls + * the send* methods. Protocol: https://eblong.com/zarf/glk/glkote/docs.html + */ +import type { ZorkDialog } from "./ZorkDialog"; + +export interface StyledRun { + style: string; + text: string; +} + +export interface TerminalState { + gridLines: StyledRun[][]; + bufferLines: StyledRun[][]; + inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; + filePrompt: { filemode: string; filetype: string } | null; + disabled: boolean; + error: string | null; +} + +export type TerminalListener = (state: TerminalState) => void; + +type RunOrString = string | { style?: string; text?: string }; +interface WindowUpdate { + id: number; + type: "grid" | "buffer" | "graphics"; + rock: number; + gridwidth?: number; + gridheight?: number; +} +interface ContentUpdate { + id: number; + clear?: boolean; + lines?: { line: number; content?: RunOrString[] }[]; + text?: { append?: boolean; flowbreak?: boolean; content?: RunOrString[] }[]; +} +interface InputUpdate { + id: number; + gen: number; + type: "line" | "char"; + maxlen?: number; + initial?: string; +} +interface GlkOteUpdate { + type: "update" | "pass" | "retry" | "error"; + gen?: number; + windows?: WindowUpdate[]; + content?: ContentUpdate[]; + input?: InputUpdate[]; + specialinput?: { type: string; filemode: string; filetype: string }; + disable?: boolean; + message?: string; +} +interface GlkOteEvent { + type: string; + gen: number; + [key: string]: unknown; +} +interface AcceptInterface { + accept: (event: GlkOteEvent) => void; +} + +// Fixed 80x25 terminal metrics; glkapi derives grid rows/cols from these. +const METRICS = { + width: 720, + height: 450, + outspacingx: 0, + outspacingy: 0, + inspacingx: 0, + inspacingy: 0, + gridcharwidth: 9, + gridcharheight: 18, + gridmarginx: 0, + gridmarginy: 0, + buffercharwidth: 9, + buffercharheight: 18, + buffermarginx: 0, + buffermarginy: 0, +}; + +function parseRuns(content: RunOrString[] | undefined): StyledRun[] { + const runs: StyledRun[] = []; + if (!content) return runs; + let i = 0; + while (i < content.length) { + const item = content[i]; + if (typeof item === "string") { + // Alternating "style", "text" string pairs. + const text = content[i + 1]; + runs.push({ style: item, text: typeof text === "string" ? text : "" }); + i += 2; + } else { + runs.push({ style: item.style ?? "normal", text: item.text ?? "" }); + i += 1; + } + } + return runs; +} + +export class GlkOteReact { + private readonly dialog: ZorkDialog; + private listener: TerminalListener | null; + private iface: AcceptInterface | null = null; + private generation = 0; + private windows = new Map(); + private state: TerminalState = { + gridLines: [], + bufferLines: [], + inputRequest: null, + filePrompt: null, + disabled: false, + error: null, + }; + + constructor(dialog: ZorkDialog, listener: TerminalListener) { + this.dialog = dialog; + this.listener = listener; + } + + /* ---- GlkOte contract, called by vendor/glkapi.js ---- */ + + init(iface: AcceptInterface): void { + this.iface = iface; + iface.accept({ type: "init", gen: 0, metrics: METRICS, support: [] }); + } + + update(arg: GlkOteUpdate): void { + if (arg.type === "error") { + this.state.error = arg.message ?? "Unknown interpreter error"; + this.emit(); + return; + } + if (arg.type !== "update") return; + if (typeof arg.gen === "number") this.generation = arg.gen; + + if (arg.windows) { + this.windows.clear(); + for (const win of arg.windows) this.windows.set(win.id, win); + } + if (arg.content) { + for (const update of arg.content) this.applyContent(update); + } + // Input requests fully replace the previous set each update. + this.state.inputRequest = null; + if (arg.input) { + for (const req of arg.input) { + const win = this.windows.get(req.id); + // Zork only reads from the buffer window; prefer it if several ask. + if (!this.state.inputRequest || win?.type === "buffer") { + this.state.inputRequest = { + id: req.id, + gen: req.gen, + type: req.type, + maxlen: req.maxlen ?? 200, + initial: req.initial ?? "", + }; + } + } + } + this.state.filePrompt = + arg.specialinput?.type === "fileref_prompt" + ? { filemode: arg.specialinput.filemode, filetype: arg.specialinput.filetype } + : null; + this.state.disabled = !!arg.disable || (!this.state.inputRequest && !this.state.filePrompt); + this.emit(); + } + + log(_msg: string): void { + // Interpreter debug output: intentionally dropped. + } + warning(msg: string): void { + console.warn("Zork glkapi warning:", msg); + } + error(msg: string): void { + this.state.error = String(msg); + this.emit(); + } + getlibrary(name: string): unknown { + return name === "Dialog" ? this.dialog : null; + } + getinterface(): AcceptInterface | null { + return this.iface; + } + save_allstate(): undefined { + return undefined; // autosave not enabled + } + + /* ---- Outgoing events, called by the terminal UI / session ---- */ + + sendLine(text: string): void { + const req = this.state.inputRequest; + if (!this.iface || !req || req.type !== "line") return; + this.iface.accept({ type: "line", gen: this.generation, window: req.id, value: text }); + } + + sendChar(key: string): void { + const req = this.state.inputRequest; + if (!this.iface || !req || req.type !== "char") return; + this.iface.accept({ type: "char", gen: this.generation, window: req.id, value: key }); + } + + sendFileref(value: { filename: string; usage: string } | null): void { + if (!this.iface) return; + this.iface.accept({ type: "specialresponse", gen: this.generation, response: "fileref_prompt", value }); + } + + detach(): void { + this.listener = null; + this.iface = null; + } + + /* ---- Internals ---- */ + + private applyContent(update: ContentUpdate): void { + const win = this.windows.get(update.id); + if (!win) return; + if (win.type === "grid") { + const height = win.gridheight ?? 1; + while (this.state.gridLines.length < height) this.state.gridLines.push([]); + this.state.gridLines.length = height; + for (const line of update.lines ?? []) { + if (line.line >= 0 && line.line < height) this.state.gridLines[line.line] = parseRuns(line.content); + } + } else if (win.type === "buffer") { + if (update.clear) this.state.bufferLines = []; + for (const para of update.text ?? []) { + const runs = parseRuns(para.content); + if (para.append && this.state.bufferLines.length > 0) { + this.state.bufferLines[this.state.bufferLines.length - 1].push(...runs); + } else { + this.state.bufferLines.push(runs); + } + } + // Cap scrollback so multi-hour sessions don't grow unbounded. + const MAX_LINES = 2000; + if (this.state.bufferLines.length > MAX_LINES) { + this.state.bufferLines = this.state.bufferLines.slice(-MAX_LINES); + } + } + } + + private emit(): void { + if (!this.listener) return; + this.listener({ + ...this.state, + gridLines: this.state.gridLines.map((l) => [...l]), + bufferLines: this.state.bufferLines.map((l) => [...l]), + }); + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx jest test/Arcade/Zork/GlkOteReact.test.ts` +Expected: PASS (3 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/Arcade/Zork/GlkOteReact.ts test/Arcade/Zork/GlkOteReact.test.ts +git commit -m "Arcade: GlkOte display adapter feeding React state" +``` + +--- + +### Task 4: Engine session + headless Zork smoke test + +**Files:** +- Create: `src/Arcade/Zork/session.ts` +- Create: `src/Arcade/Zork/metadata.ts` +- Test: `test/Arcade/Zork/engine.test.ts` + +**Interfaces:** +- Consumes: `GlkOteReact`, `TerminalListener` (Task 3), `ZorkDialog` (Task 2), vendor modules (Task 1). +- Produces: + +```ts +// session.ts +export interface ZorkSession { glkote: GlkOteReact; dispose(): void } +export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession; +// metadata.ts +export interface ZorkGame { key: "zork1" | "zork2" | "zork3"; title: string; url: string } +export const ZorkGames: ZorkGame[]; +``` + +- [ ] **Step 1: Write the failing smoke test** + +This is the money test: real ZVM + real glkapi + our adapter + our dialog, playing actual Zork I with scripted commands, no browser. + +```ts +import * as fs from "fs"; +import * as path from "path"; +import { GlkOteReact, TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; +import { createZorkSession } from "../../../src/Arcade/Zork/session"; + +function textOf(state: TerminalState): string { + return state.bufferLines.map((line) => line.map((r) => r.text).join("")).join("\n"); +} + +describe("Zork I engine smoke test", () => { + beforeEach(() => localStorage.clear()); + + it("boots Zork I, walks the opening, and saves", () => { + const story = new Uint8Array(fs.readFileSync(path.join(__dirname, "../../../src/Arcade/Zork/games/zork1.z3"))); + let state: TerminalState | null = null; + const session = createZorkSession(story, "zork1", (s) => (state = s)); + + // ZVM runs synchronously until the first input request. + expect(state).not.toBeNull(); + let s = state as unknown as TerminalState; + expect(textOf(s)).toContain("West of House"); + expect(textOf(s)).toContain("white house"); + expect(s.inputRequest?.type).toBe("line"); + + session.glkote.sendLine("open mailbox"); + s = state as unknown as TerminalState; + expect(textOf(s)).toContain("leaflet"); + + // status line (grid window) shows the room + expect(s.gridLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toContain("West of House"); + + // save -> fileref prompt -> localStorage entry appears + session.glkote.sendLine("save"); + s = state as unknown as TerminalState; + expect(s.filePrompt?.filemode).toBe("write"); + session.glkote.sendFileref({ filename: "slot1", usage: "save" }); + s = state as unknown as TerminalState; + expect(textOf(s).toLowerCase()).toContain("ok"); + expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); + + session.dispose(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx jest test/Arcade/Zork/engine.test.ts` +Expected: FAIL — cannot find module session. + +- [ ] **Step 3: Implement `src/Arcade/Zork/session.ts`** + +```ts +/** + * Wires a fresh ZVM + glkapi instance to our display adapter and dialog. + * Wiring pattern verified against ifvms 1.1.6 bin/zvm.js: + * vm.prepare(story, options); Glk.init(options); // Glk.init calls vm.init() + */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +import ZVM = require("./vendor/zvm.js"); +import { GlkClass } from "./vendor/glkapi.js"; +import { GlkOteReact, TerminalListener } from "./GlkOteReact"; +import { ZorkDialog } from "./ZorkDialog"; + +export interface ZorkSession { + glkote: GlkOteReact; + dispose(): void; +} + +export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession { + const dialog = new ZorkDialog(gameKey); + const glkote = new GlkOteReact(dialog, listener); + const vm = new ZVM(); + const Glk = new GlkClass(); + const options = { vm, Glk, GlkOte: glkote, Dialog: dialog }; + vm.prepare(story, options); + Glk.init(options); // synchronously runs the VM to its first input request + return { + glkote, + dispose: () => glkote.detach(), + }; +} +``` + +If `import = require` syntax fights the repo's TS config, use `import ZVM from "./vendor/zvm.js";` and adjust `vendor.d.ts` (`export default ZVM`) — pick whichever compiles cleanly; do not add build config for it. + +- [ ] **Step 4: Implement `src/Arcade/Zork/metadata.ts`** + +```ts +import zork1Url from "./games/zork1.z3"; +import zork2Url from "./games/zork2.z3"; +import zork3Url from "./games/zork3.z3"; + +export interface ZorkGame { + key: "zork1" | "zork2" | "zork3"; + title: string; + url: string; +} + +export const ZorkGames: ZorkGame[] = [ + { key: "zork1", title: "Zork I: The Great Underground Empire", url: zork1Url }, + { key: "zork2", title: "Zork II: The Wizard of Frobozz", url: zork2Url }, + { key: "zork3", title: "Zork III: The Dungeon Master", url: zork3Url }, +]; +``` + +- [ ] **Step 5: Run the smoke test** + +Run: `npx jest test/Arcade/Zork/engine.test.ts` +Expected: PASS. Debugging notes if not: +- "This is not a Z-Code file" → story bytes mangled; check fs read. +- No update received → check `Glk.init` was passed the same options object given to `vm.prepare`. +- Fileref/save shape mismatch → read the `fileref_prompt` handling in `vendor/glkapi.js` (search `fileref_prompt`) and adjust `sendFileref`'s `value` shape to what `gli_fileref_create_by_prompt_callback` expects; update the Task 3 test to match reality. The state of the art is the vendor file, not this plan. + +- [ ] **Step 6: Full test suite** + +Run: `npm run test` +Expected: all suites pass (new + existing). + +- [ ] **Step 7: Commit** + +```bash +git add src/Arcade/Zork/session.ts src/Arcade/Zork/metadata.ts test/Arcade/Zork/engine.test.ts +git commit -m "Arcade: ZVM session wiring + headless Zork I smoke test" +``` + +--- + +### Task 5: ZorkTerminal — the React terminal UI + +**Files:** +- Create: `src/Arcade/Zork/ZorkTerminal.tsx` +- Test: `test/Arcade/Zork/ZorkTerminal.test.tsx` + +**Interfaces:** +- Consumes: `TerminalState`, `StyledRun` (Task 3). +- Produces: + +```tsx +export interface ZorkTerminalProps { + state: TerminalState; + onLine: (text: string) => void; + onChar: (key: string) => void; + onFileref: (value: { filename: string; usage: string } | null) => void; +} +export function ZorkTerminal(props: ZorkTerminalProps): React.ReactElement; +``` + +- [ ] **Step 1: Write the failing render test** + +Match the repo's existing React test style if one exists (check `test/` for `.tsx` tests first and copy the harness idiom). Baseline version: + +```tsx +import React from "react"; +import ReactDOM from "react-dom"; +import { act } from "react-dom/test-utils"; +import { ZorkTerminal } from "../../../src/Arcade/Zork/ZorkTerminal"; +import type { TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; + +const baseState: TerminalState = { + gridLines: [[{ style: "normal", text: " West of House Score: 0 Moves: 0" }]], + bufferLines: [ + [{ style: "normal", text: "You are standing in an open field west of a white house." }], + [{ style: "normal", text: "There is a small mailbox here." }], + ], + inputRequest: { id: 2, gen: 1, type: "line", maxlen: 200, initial: "" }, + filePrompt: null, + disabled: false, + error: null, +}; + +describe("ZorkTerminal", () => { + let container: HTMLDivElement; + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + afterEach(() => { + ReactDOM.unmountComponentAtNode(container); + container.remove(); + }); + + it("renders status line, story text, and a focused input", () => { + act(() => { + ReactDOM.render(, container); + }); + expect(container.textContent).toContain("West of House"); + expect(container.textContent).toContain("small mailbox"); + expect(container.querySelector("input")).not.toBeNull(); + }); + + it("submits a line on Enter and clears the field", () => { + const onLine = jest.fn(); + act(() => { + ReactDOM.render(, container); + }); + const input = container.querySelector("input") as HTMLInputElement; + act(() => { + input.value = "open mailbox"; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + }); + expect(onLine).toHaveBeenCalledWith("open mailbox"); + }); + + it("shows the save-slot prompt when filePrompt is set", () => { + const state = { ...baseState, inputRequest: null, filePrompt: { filemode: "write", filetype: "save" } }; + act(() => { + ReactDOM.render(, container); + }); + expect(container.textContent).toMatch(/slot|file|name/i); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx jest test/Arcade/Zork/ZorkTerminal.test.tsx` +Expected: FAIL — cannot find module ZorkTerminal. + +- [ ] **Step 3: Implement `src/Arcade/Zork/ZorkTerminal.tsx`** + +```tsx +/** + * Retro terminal renderer for the Zork arcade cabinets. + * Pure presentation: renders TerminalState, forwards input upward. + * Colors come from Settings.theme so player themes apply. + */ +import React, { useEffect, useRef, useState } from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { Settings } from "../../Settings/Settings"; +import type { StyledRun, TerminalState } from "./GlkOteReact"; + +export interface ZorkTerminalProps { + state: TerminalState; + onLine: (text: string) => void; + onChar: (key: string) => void; + onFileref: (value: { filename: string; usage: string } | null) => void; +} + +const MONO = { fontFamily: '"Lucida Console", "Consolas", monospace', fontSize: "14px", lineHeight: "18px" }; + +function runStyle(run: StyledRun): React.CSSProperties { + return { + fontStyle: run.style === "emphasized" ? "italic" : undefined, + fontWeight: run.style === "subheader" || run.style === "header" ? "bold" : undefined, + }; +} + +function Line({ runs }: { runs: StyledRun[] }): React.ReactElement { + return ( +
+ {runs.map((run, i) => ( + + {run.text} + + ))} +
+ ); +} + +export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalProps): React.ReactElement { + const [entry, setEntry] = useState(""); + const [slotName, setSlotName] = useState(""); + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight); + inputRef.current?.focus(); + }, [state]); + + function keyDown(event: React.KeyboardEvent): void { + // Keep keystrokes inside the cabinet: no Bitburner hotkeys mid-game. + event.stopPropagation(); + if (state.inputRequest?.type === "char") { + event.preventDefault(); + onChar(event.key.length === 1 ? event.key : event.key.toLowerCase()); + return; + } + if (event.key === "Enter") { + const text = entry; + setEntry(""); + setHistory((h) => (text.trim() ? [...h, text] : h)); + setHistoryIndex(-1); + onLine(text); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + const idx = historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1); + if (history[idx] !== undefined) { + setHistoryIndex(idx); + setEntry(history[idx]); + } + } else if (event.key === "ArrowDown") { + event.preventDefault(); + const idx = historyIndex + 1; + if (idx >= history.length) { + setHistoryIndex(-1); + setEntry(""); + } else { + setHistoryIndex(idx); + setEntry(history[idx]); + } + } + } + + const width = "740px"; + return ( + + {/* Status line: classic inverse-video Infocom bar */} + + {state.gridLines.length > 0 ? ( + state.gridLines.map((runs, i) => ) + ) : ( +
+ )} + + {/* Story buffer */} + + {state.bufferLines.map((runs, i) => ( + + ))} + {state.error && [ {state.error} ]} + {state.disabled && !state.error && !state.filePrompt && ( + [ The cabinet hums quietly. Press Back to leave. ] + )} + {/* Input line */} + {state.inputRequest && !state.filePrompt && ( + + > + setEntry(e.target.value)} + onKeyDown={keyDown} + variant="standard" + autoFocus + fullWidth + InputProps={{ disableUnderline: true, sx: { color: Settings.theme.primary, ...MONO } }} + inputProps={{ maxLength: state.inputRequest.maxlen, "aria-label": "Zork command input" }} + /> + + )} + {/* Save/restore slot prompt */} + {state.filePrompt && ( + + + {state.filePrompt.filemode === "read" ? "Restore from slot:" : "Save to slot:"} + + setSlotName(e.target.value)} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === "Enter" && slotName.trim()) { + onFileref({ filename: slotName.trim(), usage: state.filePrompt?.filetype ?? "save" }); + setSlotName(""); + } + }} + variant="standard" + autoFocus + placeholder="slot name" + InputProps={{ sx: { color: Settings.theme.primary, ...MONO } }} + /> + + + )} + + + ); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx jest test/Arcade/Zork/ZorkTerminal.test.tsx` +Expected: PASS (3 tests). If MUI TextField swallows the synthetic events in jsdom, dispatch on the inner `input` element (querySelector already does) and ensure the `onChange` fires via `Simulate.change` from `react-dom/test-utils` instead — adjust the test, not the component. + +- [ ] **Step 5: Commit** + +```bash +git add src/Arcade/Zork/ZorkTerminal.tsx test/Arcade/Zork/ZorkTerminal.test.tsx +git commit -m "Arcade: Zork terminal UI component" +``` + +--- + +### Task 6: ZorkRoot + arcade menu integration + +**Files:** +- Create: `src/Arcade/Zork/ZorkRoot.tsx` +- Modify: `src/Arcade/ui/ArcadeRoot.tsx` (all 43 lines shown in current form below) + +**Interfaces:** +- Consumes: `createZorkSession` (Task 4), `ZorkTerminal` (Task 5), `ZorkGames`/`ZorkGame` (Task 4). +- Produces: `` rendered by ArcadeRoot. + +- [ ] **Step 1: Implement `src/Arcade/Zork/ZorkRoot.tsx`** + +```tsx +/** + * A Zork arcade cabinet: loads the bundled story file, runs a ZVM session, + * and renders the terminal. ArcadeRoot supplies the Back button. + */ +import React, { useEffect, useRef, useState } from "react"; +import Typography from "@mui/material/Typography"; +import { AlertEvents } from "../../ui/React/AlertManager"; +import { Settings } from "../../Settings/Settings"; +import { GlkOteReact, TerminalState } from "./GlkOteReact"; +import { ZorkTerminal } from "./ZorkTerminal"; +import { createZorkSession, ZorkSession } from "./session"; +import type { ZorkGame } from "./metadata"; + +export function ZorkRoot({ game }: { game: ZorkGame }): React.ReactElement { + const [state, setState] = useState(null); + const sessionRef = useRef(null); + + useEffect(() => { + let cancelled = false; + fetch(game.url) + .then((response) => { + if (!response.ok) throw new Error(`Failed to load ${game.title}: ${response.status}`); + return response.arrayBuffer(); + }) + .then((buffer) => { + if (cancelled) return; + sessionRef.current = createZorkSession(new Uint8Array(buffer), game.key, setState); + }) + .catch((error) => { + console.error(error); + if (!cancelled) AlertEvents.emit("This machine is broken."); + }); + return () => { + cancelled = true; + sessionRef.current?.dispose(); + sessionRef.current = null; + }; + }, [game]); + + if (!state) { + return The cabinet flickers to life...; + } + const glkote = (): GlkOteReact | undefined => sessionRef.current?.glkote; + return ( + <> + + {game.title} + + glkote()?.sendLine(text)} + onChar={(key) => glkote()?.sendChar(key)} + onFileref={(value) => glkote()?.sendFileref(value)} + /> + + Type "save" / "restore" to keep your progress. It is pitch black. You are likely to be eaten + by a grue. + + + ); +} +``` + +- [ ] **Step 2: Modify `src/Arcade/ui/ArcadeRoot.tsx`** + +Full new content (current file is 43 lines; this replaces it): + +```tsx +import React, { useState } from "react"; +import { BBCabinetRoot } from "./BBCabinet"; + +import Button from "@mui/material/Button"; +import { Player } from "@player"; +import { AlertEvents } from "../../ui/React/AlertManager"; +import { ZorkRoot } from "../Zork/ZorkRoot"; +import { ZorkGames, ZorkGame } from "../Zork/metadata"; + +enum Page { + None, + Megabyteburner2000, + Zork, +} + +export function ArcadeRoot(): React.ReactElement { + const [page, setPage] = useState(Page.None); + const [zorkGame, setZorkGame] = useState(null); + + function mbBurner2000(): void { + if (Player.activeSourceFileLvl(1) === 0) { + AlertEvents.emit("This machine is broken."); + } else { + setPage(Page.Megabyteburner2000); + } + } + + function playZork(game: ZorkGame): void { + setZorkGame(game); + setPage(Page.Zork); + } + + if (page === Page.None) { + return ( + <> + +
+ {ZorkGames.map((game) => ( + + +
+
+ ))} + + ); + } + let currentGame = <>; + switch (page) { + case Page.Megabyteburner2000: + currentGame = ; + break; + case Page.Zork: + if (zorkGame) currentGame = ; + break; + } + return ( + <> + + {currentGame} + + ); +} +``` + +- [ ] **Step 3: Typecheck, lint, tests** + +Run: `npx tsc --noEmit -p . && npm run lint:report && npm run test` +Expected: clean. Likely friction point: the `*.z3` import in metadata.ts needs the Task 1 `global.d.ts` declaration — if tsc complains, the declaration file isn't included; check `tsconfig.json` `include` covers `src/@types`. + +- [ ] **Step 4: Commit** + +```bash +git add src/Arcade/Zork/ZorkRoot.tsx src/Arcade/ui/ArcadeRoot.tsx +git commit -m "Arcade: Zork 1-3 cabinets in the New Tokyo arcade menu" +``` + +--- + +### Task 7: Full verification pass (build + manual play) + +**Files:** +- None created; fixes only if verification finds problems. + +- [ ] **Step 1: Production-grade checks** + +Run: `npm run lint:report && npm run format:report && npm run test` +Expected: all green. + +- [ ] **Step 2: Dev build boots** + +Run: `npm run start:dev` (webpack dev server) in the background; open the served URL. +Expected: game loads with no console errors from the Zork additions. + +- [ ] **Step 3: Manual play verification (use the `verify` skill if available)** + +In the running game: create/load a save, travel to New Tokyo, open the Arcade location. +- All four buttons visible (Megabyte + 3 Zorks). +- Launch Zork I: opening text renders, status bar shows "West of House". +- `open mailbox`, `read leaflet` — sensible responses; command history via ↑. +- Typing letters matching Bitburner hotkeys does NOT trigger app navigation. +- `save` → slot prompt → save → `restore` round-trips (check localStorage key `zork.zork1.save.` in devtools). +- Reload the page (dev server), reopen Zork I, `restore` — save survived. +- Back button returns to arcade menu; re-entering starts a fresh session. +- Launch Zork II and Zork III: openings render. +- Switch to a non-default theme in options; cabinet colors follow. + +- [ ] **Step 4: Bundle impact sanity check** + +Run: `ls -la dist/assets/*.z3 2>/dev/null || echo "run a build first"` after `npm run build:dev` (or check webpack dev output). +Expected: three ~87–93KB .z3 assets; JS bundle growth ≈ zvm.js + glkapi.js (~330KB pre-minify, less after). + +- [ ] **Step 5: Final commit (fixes from verification, if any)** + +```bash +git add -A && git commit -m "Arcade: Zork verification fixes" +``` + +Only commit if there are changes. + +--- + +## Plan Self-Review Notes + +- Spec coverage: layout ✔ (T1), interpreter stack ✔ (T1/T4), Dialog/saves ✔ (T2), display adapter ✔ (T3), terminal UI + theme + keyboard capture ✔ (T5), arcade menu ✔ (T6), licensing ✔ (T1 steps 2–3), error handling ✔ (T2 corrupt-save guard, T3 error state, T6 fetch/alert), testing ✔ (T2–T5 + T7 manual). +- Known uncertainty flagged inline: exact `fileref_prompt` response shape (T4 step 5 gives the resolution procedure against the vendor source); MUI TextField event simulation in jsdom (T5 step 4). +- The mockups' standalone "Back" button inside the cabinet was dropped: ArcadeRoot already renders Back above every game (existing convention). From 57477b4a487f367c45ce5d9da90fc0095be3a4b8 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:19:47 -0700 Subject: [PATCH 03/12] Arcade: vendor ZVM + glkapi and Zork 1-3 story files (MIT, scoped LICENSE.md) - vendor/zvm.js: ifvms.js 1.1.6 dist build, verbatim - vendor/glkapi.js: GlkOte 2.3.7, verbatim - games/*.z3: Zork I-III story files (MIT, Microsoft 2025 release) - LICENSE.md scoped to src/Arcade/Zork only; README documents provenance - webpack asset rule + *.z3 TS declaration + jest mapper + lint/format ignores Co-Authored-By: Claude Opus 4.8 (1M context) --- .eslintignore | 1 + .prettierignore | 4 + .../plans/2026-07-07-zork-arcade.md | 80 +- jest.config.js | 1 + src/@types/global.d.ts | 4 + src/Arcade/Zork/LICENSE.md | 97 + src/Arcade/Zork/README.md | 41 + src/Arcade/Zork/games/zork1.z3 | Bin 0 -> 86838 bytes src/Arcade/Zork/games/zork2.z3 | Bin 0 -> 92524 bytes src/Arcade/Zork/games/zork3.z3 | Bin 0 -> 87984 bytes src/Arcade/Zork/vendor/glkapi.js | 6667 +++++++++++++++++ src/Arcade/Zork/vendor/vendor.d.ts | 15 + src/Arcade/Zork/vendor/zvm.js | 4312 +++++++++++ webpack.config.js | 2 +- 14 files changed, 11208 insertions(+), 16 deletions(-) create mode 100644 src/Arcade/Zork/LICENSE.md create mode 100644 src/Arcade/Zork/README.md create mode 100644 src/Arcade/Zork/games/zork1.z3 create mode 100644 src/Arcade/Zork/games/zork2.z3 create mode 100644 src/Arcade/Zork/games/zork3.z3 create mode 100644 src/Arcade/Zork/vendor/glkapi.js create mode 100644 src/Arcade/Zork/vendor/vendor.d.ts create mode 100644 src/Arcade/Zork/vendor/zvm.js diff --git a/.eslintignore b/.eslintignore index dbf2a1896f..bb2a67194c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,5 +13,6 @@ markdown electron tools src/ThirdParty +src/Arcade/Zork/vendor webpack.config.js diff --git a/.prettierignore b/.prettierignore index b94299a92c..bd19e222a6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,3 +23,7 @@ tsdoc-metadata.json src/Documentation/pages.ts # This file is generated by tools/bundle-doc/generate-math-notation-output.mjs src/Documentation/data/MathNotationOutput.json + +# Verbatim vendored MIT-licensed interpreter files (see src/Arcade/Zork/LICENSE.md) +src/Arcade/Zork/vendor +src/Arcade/Zork/games diff --git a/docs/superpowers/plans/2026-07-07-zork-arcade.md b/docs/superpowers/plans/2026-07-07-zork-arcade.md index af855d9b46..a6395e41e5 100644 --- a/docs/superpowers/plans/2026-07-07-zork-arcade.md +++ b/docs/superpowers/plans/2026-07-07-zork-arcade.md @@ -36,7 +36,7 @@ Facts verified by reading the packages (do not re-derive): `file_ref_exists(ref)`, `file_remove_ref(ref)`, `file_write(ref, content, israw)`, `file_read(ref, israw)`, `file_clean_fixed_name(filename, usage)`. Save/restore prompts arrive at the display as `update.specialinput = - {type:"fileref_prompt", filemode, filetype, gameid}`; the display answers +{type:"fileref_prompt", filemode, filetype, gameid}`; the display answers `accept({type:"specialresponse", gen, response:"fileref_prompt", value})` where `value` is `{filename, usage}` or `null` for cancel. - **GlkOte update payload:** `{type:"update", gen, windows?, content?, input?, specialinput?, disable?}`. @@ -52,6 +52,7 @@ Facts verified by reading the packages (do not re-derive): ### Task 1: Vendor drop, game files, licensing, toolchain config **Files:** + - Create: `src/Arcade/Zork/vendor/zvm.js` (verbatim from ifvms 1.1.6) - Create: `src/Arcade/Zork/vendor/glkapi.js` (verbatim from GlkOte 2.3.7) - Create: `src/Arcade/Zork/vendor/vendor.d.ts` @@ -63,6 +64,7 @@ Facts verified by reading the packages (do not re-derive): - Modify: `.prettierignore` and ESLint ignore config (create/extend) **Interfaces:** + - Produces: `require("src/Arcade/Zork/vendor/zvm.js")` → ZVM constructor; `require("src/Arcade/Zork/vendor/glkapi.js")` → `{ Glk, GlkClass }`; importable `*.z3` asset URLs. @@ -102,17 +104,20 @@ material plus Bitburner glue code. The rest of Bitburner is licensed separately see `license.txt` in the repository root. ## ZVM interpreter — `vendor/zvm.js` + From ifvms.js 1.1.6 (https://github.com/curiousdannii/ifvms.js), part of the Parchment project. [exact MIT text, Copyright (c) 2011-2017 Dannii Willis and other contributors] ## Glk API layer — `vendor/glkapi.js` + From GlkOte 2.3.7 (https://github.com/erkyrath/glkote). [exact MIT text, Copyright (c) 2008-2025, Andrew Plotkin] ## Zork I, II, III — `games/*.z3` + Compiled Z-machine story files, open-sourced under MIT (https://github.com/historicalsource/zork1 et al.). @@ -204,10 +209,12 @@ git commit -m "Arcade: vendor ZVM + glkapi and Zork 1-3 story files (MIT, scoped ### Task 2: ZorkDialog — localStorage-backed Glk file layer (TDD) **Files:** + - Create: `src/Arcade/Zork/ZorkDialog.ts` - Test: `test/Arcade/Zork/ZorkDialog.test.ts` **Interfaces:** + - Produces: `class ZorkDialog { constructor(gameKey: string) }` implementing the non-streaming Dialog contract listed in the Reference section. `FileRef` type: `{ filename: string; usage: string; gameid?: string }`. Storage key format: @@ -307,7 +314,8 @@ export class ZorkDialog { /** content is an array of byte values (israw) or a string. Returns success. */ file_write(ref: ZorkFileRef, content: number[] | string, _israw?: boolean): boolean { try { - const payload = typeof content === "string" ? { text: content } : { bytes: btoa(String.fromCharCode(...content)) }; + const payload = + typeof content === "string" ? { text: content } : { bytes: btoa(String.fromCharCode(...content)) }; localStorage.setItem(this.key(ref), JSON.stringify(payload)); return true; } catch (e) { @@ -360,21 +368,26 @@ git commit -m "Arcade: Zork save-file Dialog layer backed by localStorage" ### Task 3: GlkOteReact — display adapter / state parser (TDD) **Files:** + - Create: `src/Arcade/Zork/GlkOteReact.ts` - Test: `test/Arcade/Zork/GlkOteReact.test.ts` **Interfaces:** + - Consumes: `ZorkDialog` from Task 2. - Produces: ```ts -export interface StyledRun { style: string; text: string } +export interface StyledRun { + style: string; + text: string; +} export interface TerminalState { - gridLines: StyledRun[][]; // status-line rows - bufferLines: StyledRun[][]; // scrollback; one entry per display line + gridLines: StyledRun[][]; // status-line rows + bufferLines: StyledRun[][]; // scrollback; one entry per display line inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; filePrompt: { filemode: string; filetype: string } | null; - disabled: boolean; // true after glk_exit / while no input + disabled: boolean; // true after glk_exit / while no input error: string | null; } export type TerminalListener = (state: TerminalState) => void; @@ -456,14 +469,22 @@ describe("GlkOteReact", () => { const lineEv = accepted[accepted.length - 1] as { type: string; window: number; value: string }; expect(lineEv).toMatchObject({ type: "line", window: 2, value: "save" }); - glkote.update({ type: "update", gen: 2, specialinput: { type: "fileref_prompt", filemode: "write", filetype: "save" } }); + glkote.update({ + type: "update", + gen: 2, + specialinput: { type: "fileref_prompt", filemode: "write", filetype: "save" }, + }); expect(states[states.length - 1].filePrompt).toMatchObject({ filemode: "write" }); glkote.sendFileref({ filename: "slot1", usage: "save" }); const frEv = accepted[accepted.length - 1] as { type: string; response: string }; expect(frEv).toMatchObject({ type: "specialresponse", response: "fileref_prompt" }); - glkote.update({ type: "update", gen: 3, content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }] }); + glkote.update({ + type: "update", + gen: 3, + content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }], + }); const s = states[states.length - 1]; expect(s.bufferLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toBe("fresh"); }); @@ -749,20 +770,29 @@ git commit -m "Arcade: GlkOte display adapter feeding React state" ### Task 4: Engine session + headless Zork smoke test **Files:** + - Create: `src/Arcade/Zork/session.ts` - Create: `src/Arcade/Zork/metadata.ts` - Test: `test/Arcade/Zork/engine.test.ts` **Interfaces:** + - Consumes: `GlkOteReact`, `TerminalListener` (Task 3), `ZorkDialog` (Task 2), vendor modules (Task 1). - Produces: ```ts // session.ts -export interface ZorkSession { glkote: GlkOteReact; dispose(): void } +export interface ZorkSession { + glkote: GlkOteReact; + dispose(): void; +} export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession; // metadata.ts -export interface ZorkGame { key: "zork1" | "zork2" | "zork3"; title: string; url: string } +export interface ZorkGame { + key: "zork1" | "zork2" | "zork3"; + title: string; + url: string; +} export const ZorkGames: ZorkGame[]; ``` @@ -881,6 +911,7 @@ export const ZorkGames: ZorkGame[] = [ Run: `npx jest test/Arcade/Zork/engine.test.ts` Expected: PASS. Debugging notes if not: + - "This is not a Z-Code file" → story bytes mangled; check fs read. - No update received → check `Glk.init` was passed the same options object given to `vm.prepare`. - Fileref/save shape mismatch → read the `fileref_prompt` handling in `vendor/glkapi.js` (search `fileref_prompt`) and adjust `sendFileref`'s `value` shape to what `gli_fileref_create_by_prompt_callback` expects; update the Task 3 test to match reality. The state of the art is the vendor file, not this plan. @@ -902,10 +933,12 @@ git commit -m "Arcade: ZVM session wiring + headless Zork I smoke test" ### Task 5: ZorkTerminal — the React terminal UI **Files:** + - Create: `src/Arcade/Zork/ZorkTerminal.tsx` - Test: `test/Arcade/Zork/ZorkTerminal.test.tsx` **Interfaces:** + - Consumes: `TerminalState`, `StyledRun` (Task 3). - Produces: @@ -955,7 +988,10 @@ describe("ZorkTerminal", () => { it("renders status line, story text, and a focused input", () => { act(() => { - ReactDOM.render(, container); + ReactDOM.render( + , + container, + ); }); expect(container.textContent).toContain("West of House"); expect(container.textContent).toContain("small mailbox"); @@ -965,7 +1001,10 @@ describe("ZorkTerminal", () => { it("submits a line on Enter and clears the field", () => { const onLine = jest.fn(); act(() => { - ReactDOM.render(, container); + ReactDOM.render( + , + container, + ); }); const input = container.querySelector("input") as HTMLInputElement; act(() => { @@ -979,7 +1018,10 @@ describe("ZorkTerminal", () => { it("shows the save-slot prompt when filePrompt is set", () => { const state = { ...baseState, inputRequest: null, filePrompt: { filemode: "write", filetype: "save" } }; act(() => { - ReactDOM.render(, container); + ReactDOM.render( + , + container, + ); }); expect(container.textContent).toMatch(/slot|file|name/i); }); @@ -1084,7 +1126,9 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP const width = "740px"; return ( - + {/* Status line: classic inverse-video Infocom bar */} {state.gridLines.length > 0 ? ( @@ -1100,7 +1144,9 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP ))} {state.error && [ {state.error} ]} {state.disabled && !state.error && !state.filePrompt && ( - [ The cabinet hums quietly. Press Back to leave. ] + + [ The cabinet hums quietly. Press Back to leave. ] + )} {/* Input line */} {state.inputRequest && !state.filePrompt && ( @@ -1168,10 +1214,12 @@ git commit -m "Arcade: Zork terminal UI component" ### Task 6: ZorkRoot + arcade menu integration **Files:** + - Create: `src/Arcade/Zork/ZorkRoot.tsx` - Modify: `src/Arcade/ui/ArcadeRoot.tsx` (all 43 lines shown in current form below) **Interfaces:** + - Consumes: `createZorkSession` (Task 4), `ZorkTerminal` (Task 5), `ZorkGames`/`ZorkGame` (Task 4). - Produces: `` rendered by ArcadeRoot. @@ -1327,6 +1375,7 @@ git commit -m "Arcade: Zork 1-3 cabinets in the New Tokyo arcade menu" ### Task 7: Full verification pass (build + manual play) **Files:** + - None created; fixes only if verification finds problems. - [ ] **Step 1: Production-grade checks** @@ -1342,6 +1391,7 @@ Expected: game loads with no console errors from the Zork additions. - [ ] **Step 3: Manual play verification (use the `verify` skill if available)** In the running game: create/load a save, travel to New Tokyo, open the Arcade location. + - All four buttons visible (Megabyte + 3 Zorks). - Launch Zork I: opening text renders, status bar shows "West of House". - `open mailbox`, `read leaflet` — sensible responses; command history via ↑. diff --git a/jest.config.js b/jest.config.js index 4e6fe89b15..c718b3ce46 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,7 @@ module.exports = { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", "\\.(css|less)$": "/test/__mocks__/NullMock.js", + "\\.z3$": "/test/__mocks__/fileMock.js", ".*?raw$": "/test/__mocks__/fileMock.js", "@player": "/src/Player", "@enums": "/src/Enums", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index b68b0f01ce..036efae56a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,6 +15,10 @@ declare module "*.svg" { const value: string; export default value; } +declare module "*.z3" { + const value: string; + export default value; +} // Achievements communicated back to Electron shell for Steam. declare interface Document { diff --git a/src/Arcade/Zork/LICENSE.md b/src/Arcade/Zork/LICENSE.md new file mode 100644 index 0000000000..ec2ec0d731 --- /dev/null +++ b/src/Arcade/Zork/LICENSE.md @@ -0,0 +1,97 @@ +# Licenses for src/Arcade/Zork/ + +Everything in this folder (and only this folder) is MIT-licensed third-party +material plus Bitburner glue code. This license file applies solely to the +contents of `src/Arcade/Zork/`. The rest of Bitburner is licensed separately — +see `license.txt` in the repository root. + +--- + +## ZVM interpreter — `vendor/zvm.js` + +From ifvms.js 1.1.6 (), part of the +Parchment project. Distributed under the MIT license per the project's LICENSE +file and package metadata. (Note: a few internal module headers in the bundled +file carry an older "BSD licenced" notice from the same authors; the project as +a whole is distributed under MIT.) + +> The MIT License +> +> Copyright (c) 2011-2017 Dannii Willis and other contributors +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + +--- + +## Glk API layer — `vendor/glkapi.js` + +From GlkOte 2.3.7 (). + +> The MIT License +> +> Copyright (c) 2008-2025, Andrew Plotkin +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + +--- + +## Zork I, II, III — `games/*.z3` + +Compiled Z-machine story files for Zork I, Zork II, and Zork III, open-sourced +under the MIT license (, +, +). + +> MIT License +> +> Copyright (c) 2025 Microsoft +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. diff --git a/src/Arcade/Zork/README.md b/src/Arcade/Zork/README.md new file mode 100644 index 0000000000..e29001973a --- /dev/null +++ b/src/Arcade/Zork/README.md @@ -0,0 +1,41 @@ +# Zork in the Arcade + +Zork I–III cabinets for the New Tokyo Arcade, powered by the Parchment +project's pure-JS Z-machine interpreter (ZVM). + +## Contents + +- `vendor/zvm.js` — ifvms.js 1.1.6 `dist/zvm.js`, verbatim. Do not edit or format. +- `vendor/glkapi.js` — GlkOte 2.3.7 `glkapi.js`, verbatim. Do not edit or format. +- `games/*.z3` — compiled Zork I/II/III story files (MIT, Microsoft's 2025 + open-source release via github.com/historicalsource). +- Everything else — Bitburner glue code: React terminal UI (`ZorkTerminal.tsx`), + GlkOte display adapter (`GlkOteReact.ts`), localStorage save layer + (`ZorkDialog.ts`), session wiring (`session.ts`), cabinet component + (`ZorkRoot.tsx`). + +## How it fits together + +``` +games/zork1.z3 → ZVM (vendor/zvm.js) → Glk API (vendor/glkapi.js) + → GlkOteReact (display adapter) → ZorkTerminal (React UI) + → ZorkDialog (saves in localStorage, keys: zork...) +``` + +ZVM makes `glk_*` calls into glkapi.js; glkapi drives the display through the +GlkOte protocol, which `GlkOteReact` translates into React state instead of the +stock jQuery-based glkote.js display. + +## Licensing + +See `LICENSE.md` — MIT, scoped to this folder only. The rest of Bitburner is +under its own license (repository root `license.txt`). + +## Updating vendored files + +Re-fetch from upstream and update the versions here and in `LICENSE.md`: + +- `https://registry.npmjs.org/ifvms/-/ifvms-.tgz` → `package/dist/zvm.js` +- `https://github.com/erkyrath/glkote` → `glkapi.js` + +Design spec: `docs/superpowers/specs/2026-07-07-zork-arcade-design.md` diff --git a/src/Arcade/Zork/games/zork1.z3 b/src/Arcade/Zork/games/zork1.z3 new file mode 100644 index 0000000000000000000000000000000000000000..e447402828441d44375b2b067befcd0ef02b37b7 GIT binary patch literal 86838 zcmeFad03Oz);PM}Hv@>G0hBR>7zHsgkT9ssd4YtPFoPBqL{PDH;Mh8}Dz(+NiX$ir zigAb{v{s_6<2eQEfL5%C7PSsIKrOb`QmjK;y=(7~w`^WFQ`{XNgUUwp{g!`f@F zz4qE`t-ask1<*!`pIK{Ix&D!?y@dAzs!i)i@2{S?-f8Xc6i0*A{F z1RL`fR^1RB1J~)L#)Va+T;5%3l(sp!lp0&}x=J@{=3F}{aK*C1-S-6n-_{D|?AvZ| zP#y!H-ey5`dxNI%PQc$ffrFd3%6RG1Dk;0>4sb6_sagZZ!!-hstX3QOUA_yATy8LWo2upTO4 zBW!|dsDW*;9d^QQ*b5)Sr_cZg;4}Cf4#QD64*!BCI0>hr8O}iqv_czPfp)kKH=zUW z!}rh)58+4n33}j(Ss@eV6R237kOwgGmbuLGoq~zP_2yjSJAJUo`&W`0whtJ9gbV=h z>wPuJh#fp~q1S+feYC_%@o`^`>qr}t@_LbQ)LioF6}J5*;RiVA>F6n8CZZ(%`Wyt| zC>!~1fUOie;hCjMF@L0^B|YK+a(!1Z6Y0#9N0L(g@R9Zj%_QZ0SJB54W{|StbA^GF z1LK_!)(LBA+7`<%(-dsIjp=iVQh?;~n0nt_s;EfVl4OwxGgcq?YRm{+)&J8+@IO`3 zIu=01!GK4sMg$}5V{8EnV8m>&b)ktOC)-i9>d^dghM6wQ>3${~O4I*#( z%yu4WX1Wcws{u6dPzSc#X!D`sPc$L}WM8`q_z(5;{vvk)$51R0S zfdK%GsB$ncmX`F_4=fZCex|{|#R%E3Z7^^Rnuu-|h(^!>7PpBw(glErXgZ}62%Sf#?cL5)-}={Xm;ngIzzYhcI!2oq|Esjyqtd zr*vw-ZW2vw2fLZH_nC+6mQa2^2fOvO$^q>50HzX7f!*h%Csh*Iogs<-0Cv|PS_8*k zfZY$2pS56*a@0t-0NC6Aa$HmjU}Y~S?2QNj`%$D-$B$qiK^cQC`%Fr3EZ8p*VO7{^ zurH^`Ux0lrp@R+WzoaF$8TK7GN9;o^c>3xoejn?Elce32$Tfx)*4o$D81a1SP= z3jv0Z5yOZlFeH$OcNdnVQi5-H4#`6t>+26-$P(J>H5jsvQqF-PXhHI%d*RxUufb+1 zB&>%akAMd-?rU%~FNbyD=tz+#gQJ2D=LR@NKxrNt-xS9b+Ucj@IG^G$;8+1-DY2c7 zJ4mNrVB#>v$?A5zOvlynt>bsJWIQ&d(Yc5+W&r1UQieEyw$)`DZGIhG{t0|!9ZSyB z5)ugH; z0$g`cWEr@AP7`u)JwwL62$9kHp|(gCyFQ_b*ibh2N^yZ9K~%*rp= z>XuEbHiKIUZ8Zqo-Xp!^$88HUSjrB9U)?@MI{X({@)e6UBMyUGCzXcuEiraMa6v2~ zE5!{~h=VCXe-I~Ag2zETfgpta6U1mhu=zL;zef#`Cx|!Fk`@q~Xd)cMEmZ5Cf%p!c z+;{tlpF?9F{0!&aQKK5A7XjSWghN~j?s1gP?j!DbM7(I)-50SM>_Q3dt8f)yh%nE+ z0r=6d0;|r^Dz{*Fv#vUVdoQvBVef#XAK0^iIf)x(W6~QEpFSZ-lId_pg9H~p{YcPx zbU;VRhmvK$bV!F`tL?B_3JKsW`4Ta#!&D%-iu|z2SapXmX1WBD=b+JGcmf`_RCK=J zp(LVvjwO-U7ykezvT5%wXFV2?R@bp)EeNGB4ih_ItwsfK(BlYg{t^ zA#xkVSvXSKOZ(abLq(L}6&UJAhtmv0H56z54?|N(RmWu*T0n6^VCXzRZAq8{LzhvU zl`wP{QPD{NLyrJo3YrCpL+@f>0xAUzeTv8fuIGb{G9LyY^CVr&#Kag@gHu^=BZ~oI zNxlSG7Gy|K_8=>wjNJs;`zUY-`yOOQ;%XDdfb1ykRC_?y3j92bJwbM#SQ3>GhS|_o znmNPF!N9}Uhxro^yK5p0i>IxYz%Z(-s;Mw+KC8jrt-l)fE>Y}bU>vrC^{K3P=?Om* zw<>zErk^?`4|opp_=Z}ehrQ%y@w-m;jz15!SFF_tQ043fyGreoTeKdt-Fc28#rg%n z7hQ{Po8nnkDE`7JT(M?1!E(riMu#zbwPR|>I6hJ@{Jd}mGnNAuw=JC|@oTkS>Xgrc zpK&3V8M(V0$F)cZ}bq?;*&?f5*`<~MD@kLe8ENuf>9t)Y;gsiT|k_stk7Nimi<3!Bh(Z7 zPYD}VItJLH%I!u|T#V*v+!A$eg=$l!FAVlE=#(&U6i$x7eyFpY&-c{rGJ1Fo_ELP* zD_9F`O!{_XPxw~d$jG43=c*rnP^=maJbSV`De(AI(&D~UeBVW@SaTWp_FkUyTz=OE zC!S-hVqJ@8P6O~_uMOREX{{-;I&yF14t2?NkqIr8VoRX+NA4)(fC;vx1!8tgZIUCa zo=_)f=;PB~_lH(8T0AZ5wL2KjuLb5sRZrxhD86oA=o@OeD{|T^lfb%>qiT&#j*jt) z-7`HW-^IBR>QBU0AIX&ralaY5Vv%d9;}Es?B*pH1+R1lAP>L@LyrNwuoUL16imM2} z5Vs`d;@3YB39Pt|OLi0nHGJ1;LFrTMtBfVlLn4=_Y&&!tx%JsQz`JrIQum5C*a?5s zN)EfXgzorIvF54Z76|Ah3X|0Jf|8KrXi)#-bx{M(h0ltID5ZggwsnOynZKYeyC*mu zd+WxyO$`+^t4=?n1RL!y3RD)quB2<;;wrUY{8F_(RP+*fbPH%KiC7u2Uj14)2poh- z;qZZX^FgE7+pSt*tn?J#Me*4M$#D{nOIA7#Rjgkw_zvfN?u4BC#)u75BzxQ+iA%?9 zU8Y_nR;*fBIAbiwV6~0T>xR)|N!W%S@zYB5Q2^?!H5_YR(mkgw63zEICwpcSH8fnP$B zFMvC&z{{#zCbz9)Y)PVEGl8~U-_tcZ`iM~NKUQr+wM31hzqxJYG zykfa~wz$>pBbV0sy>A1&Nxsy{OSD`P1N=Gr3N-}JPm`tAA4&QBw-+GDZSz{seh?OW z-`Jyh9NVsO()c4umkGL9{u}2W8zpatRnJrGdR-u#66?F<4hXW-hXqfFgWhUSQ}8>{ z^Py0*lpbNUc>mbF7AYis!D+#(BAE-_{t9sZy@=ZTrv$~#V zmqcIF99G>v?}31t zqYu)9E@?d$ISo8=L8@DZZcVsS+G^^No%DcQ4SjloSp>GCHRVP0O)~cY)xcJSf8{%B+=B2r=KC7N~ z8s*Q+j8nK&dHO}`)F19p{k~Za60r7L=b6sfrHV=_HP)_0w@;OX#Go5^Cnja(i4{Iu zZYuGlg6y*B%33awxy!}dUH@ELe1|dKSBtw#3mlWxvyY+0P^^AV9G#@A0Nhj2F9i$K z=`O0???GVO6i9`8Kwyer8g>a~cj&faOS5;yZCp*=fo;S2W(|;(ZgnG;QQYeKk#p+? znpP**Yq?+0QjvLWHcBRit??eS#@lg{LzH6IO3T#tnK=)G#b0hN8ou55(HQM4Y+_S`QJ7tZos8Sa$v<#3x z*+x6gCZ+zAA2(FA7uO?LX?2BlxmGTvg4ooy4x=e(XXprI5<3vfWEGYx_neb1dr=@CZqu-=B&-UVe|vJu z>Q%da_F}+H0v9=kAgrBjwY~ULx3`^_IxQW&vA<$_ujj3XDIt58U@%aL zb@~kO^PLnQ5%1Tdn?XO^0*4XbZ5H@Hf3GT^5k`=43fFQm~?n%R`YZ5+E8?=NMwWzq9=AmFoQlrIJP_Rn+fI3ONIJwO#oEts=TL$AAEw61RGmGC zzJIw+e*2Wi4fm&tReylctbnfdZZ^>we;bPccn{P&6dT)6TVHpoa3oiR2X};!Vq-e?z$O1zNthGBc=Jm z8)tfS+K@p(k1^uC|X)pxOl1O)^&@f;g@)*`Xhm2)k);n&b$5S z>mE{xqkHC`OMF*=d5N>4*8A-q>|6T_R>Z2`dZ1YQK3Li53Or|TQMmM}XR&0E`x3`u z>`JlbIGQRpuMH)xlc^W30yh8PkEtfzk7V9(^;j5&r*6*BHSK2-=G?`o`upFMz>LBT z4K<3A8u#T+sgBgs^4;5qAI@{Ackzn~6s`8D93&@B<&CSdsyur)`GnIaamw~boKqM4 zAgb`Cq4nHJN|4(fd}!nGNRsKU2{qKYWIAgUJD(I@Ny5;K@8?wj=vL$geqnKILou0$9+q}#aaV~m#OtAVbld5+=F)n{L@bDuI1evYgjCZf+ zxejAb@O!~4v}>a0sprIqK1EGko#c~LYG%eGK(e1000Hp<(QSp<=P|^26D{Wt>Nzh( z-xKS&dAMSW*I(qtO@us8A}<)&vZ*9EI${x6(*^V2c%|8QiCfz0LdB1dSUmmWs?|A8 zqJJt3<1l12n>3GcUFClU7}w3yBd7PAxp?<4JVNBBsr{ey?yqWNjhF7+_OU2SLm92^mC7v@l5Kf3=4o36Y zYxeg)KIm1fs0RUeZD?Jhi!&}Fzu0x6C&VgK@woeoYh2K*%wdn9H#PVp?Lp zRgZL7d*+wTAKgG)uV$mie90{Lx5YagXQ}f(6bNx~t{ZNhI^$@eDbyuoJ{pEEQE|pl ztr?`Ko+5`~z?-r#`b?dtiOUNwjQ&nlvlSyY@A8M{$>QOz{sy`($LM*gn|FOtB7Gx~ zNZfamaRmC;=BXF{Ls4ap>f_pPRhqQ#MxGD5rk=q2&v{zpg7fswZp&m1Gfx&qx-Z&J z`Gq*Dy|tnTB!u5Gff}XPbb6nuje=Y^42OEi*#q}WL@S;@_>y2xbgi@5(M_@HGon}a z$w?ZrZELY{uNQj^saNdL1MAn@J-sJlk4_c_;UB6;P6s}5h8EwF>8ENw^OrNhN1{d# z{yW5-s0*KX3LB;nKLY$QiQfIjUb-~BB=jLlues5@;1pZ%Y>0-N(vZ z^Lu3AD-L?^I1K!v-pqe7Lfk`y%u#1@qRa9Tc-o~;+nVjCX#pO6GK^c9%E;^C&g!DO zstYY65P|R4Ezk3asge7ISgZ5@Na^jok7mQGu&r3LrQHJzeupkojyR`O7A zH+WMD$3pDGqq~HmJ8olP+DChyl=Bf6Ttqar>4p@O-wP z?TaH<>hatlU^+TQiHrLnryc6)0>Vp$rr=F6lHe8U+z32bX%58WpPtC%=&@nb zxm)TtU(t!Nbh`R}N@o`XQ2=AI35^}%~%dm>aZ4dEf-N7QMKs-GVNpD5kG zZSo$V!Y`j7A1)4_-nPsy{-!#8spuTo_NX-0wQLO!v8H6fexq`UX#(MhF!5!_=j@Q9Y$I{~t7X&CcecXH zx^@WsLW|Vt`$av#?7GwwYK#~h;TGIh3UX)~0THrokJYTUE<4j4gx7CcG zONDiOAZFX zxU_YQt-=L^jgb8zha&dH`Kb#sMD6GR@i1!&A1_lFai>T8H^W zO%2_xZN?Q8QW3f|rXPxF_6pUdR;r71GgbYY9NnkcpZ%%NsV%A}z2w*-tr$LVecHCn zO{2~bD|TK$^|5tXfX8tY+#imv#O+_ql#o>)-T+>Kk9J(wGQU^{b@pS?E+mfw(~)zK zp}X&DJ%&maj$ZYnVpl@n9w*!8`v7vn$+sKhvViO*7hB^wuz1{D0_brtViLOSlryKa z=wapHjk@r^pZ_-{fQNqnKl=RdX#c-E+4#uw|IFwASjYcohWo$0{;vf72MOSPfer8P zeS7;C7;wPm#~ba}UoafSXdU(ns|_pY1Nhp1kh<;lJNIngw_!C&S8T6+dxz)xZwTJT zuU3ZatjD8VfKTv7=4`wdB*JR|^>~XCYw=0lAlQ*8B^)FMC*qv}ghlv=M|XH9awFWs zGcLSyh&PdpB$WbihTzaUggnI$ieE>8QmP!O^izf?qm>hs8%W-xn$em00De@==XZ zX;i7I@v0fBcT~$%8&zMcuBz^+x>diZepd_B1JzDyZ#AcmSEs9U)o-d7t3Ob0Qtwrp z)W_5()mPMAeolVget7iYm+qJASLip>Z=T;;zs-I-{CWNZ{N4S-{bT%d{HOab^ncI4 z%zuM_jsG71!~RYF=lt9JZ}@lmKk|PSAPN{7;1>`Q5FL;dkQFdBV0OTQfVzMK0mlPc z0=@~j9q=IFd7youA}};CGjL+y)WG?H?*^_9tPZ>p*cteUK`BW<1@Q>!=PibXmmr46 zcw@=@S;S+0$CL`jGk6z-LQ;Wwyt?)iz;Y<26?0)a^9MM<3&3lW_(XOoOk?m$4&Imz zV970#zhJ6t@l=+!-0xOwU6s}@^r7(te1>niNqe&Z;ylWwXXYdBr z5SYn4qxnoEVa}ULK?!LhaaWyC3K@D;2VxMFd}8|eied0t8qN3dPjgt0eIRCIpKKao z3Y87!DVr9E*dJLVrXAjx&BF^oeee$oQB=-|@D7T^4MjE_rSFVTgIP6I^%;V7D$l?@%!ZH*2lUt`+J{u z{y`C3c&Io;fgSr4BY`20;_#{`(ONq^%to2C* zkKXB`5Esaaz6EjuhEUPr5+b}yPN?2x;TH-mB+D&>$RY^|vN3`|KFpm^Bo^MZAoDF+ z>iB2EGVz{t2mnF6zR^VV$@?FDjsIckXu5?OM@op;RJ=ajUeecQNguH#q#H~(X7?pv zGD-lo{V{w8C#}ae>F~vlixwahQiB69 zjbIX0q{Vz-*n;Ftks=(KRvCfjvF@3_D zYboi+LvuuCB@EZ38JekvS;QG_L0vPzB6;=~I?7DiPcMWrXj}1kyqCRh^dsUnJ1&r(Q_9@xm9Mc3i#^0)`D>6xL0msm@q0#wPKK}Zj+;wuW#L70d7 z9H$wNi*XvwwBY3+=f|kBq<8#?EVM?F`65Gi#45skoTVnmf@?)eh{5zS592W`Hn-_< zMD_i5D{NUJygmcDn%ES4A!RishR`Ajypl;j!91#IMB|8$C6JW!QRDjhSChnGT-l>Z zTYKaKZ#TcBkfCjtLJ|&w>Z`@1zTMXrB-VeraEt%?%WshoVA!7_fmy+2IG5(oz-*F- zlHT2rqeKhI<(46638VPY20aRT9?W01B3Kl>D7Y&4c<`C!(ZOE_e;>jNc`IZ?$oi1o zA$ue3LcY`aXfs2ehH67|xQE=Ap$(ziLq88a8Tus5KP){gC@efIDz-T6%W#`;akzi@ z!hz2*w%%lUKpy1m?d zZV~r!?@n&)TKl#=s{T(Z`kmN$||Gn(r(@}MQ&qw(GdwWu+S!~AO;&r?Jj{D!IwdObr zE2HoVZX6nW@`>pn-~TUs?8n}QM?o*)r?@-tI7kmahu1?-&=mM3>TT#9+XYXezJ_0e zK7gm;-S8}`5}t=ezzfX=_$}TAevfK~KjM4fWt=;_;`xk{S+O4vZYnS4c7UU{!pQ#_ z919UV2FC}N(zp(Ad|EY~yN+P*U~UySzPQlMb%B#7qWlETMg+JHdLMdPKLQu0HfQT6 z;No(uh+7XXA$1GTfFgKK!1AMo{8!-8USTA;_R0+iT-Dh836P7CBuBda4P2)kNtfNm zJN=k@2Cnme&gM3P>w=@#W!u2@vooi-Yv76x(X4+4*XNX&o2bIbT>>{`$@&*?3)*bU zT_!-poddU-)d5J_ZF!TH+l1hm(fTR4A=jj^jsh{>T(j9i49d z6TR0(P*U5LSz{QHUVDvz_FaS*_x3#k*=lgVR%_(m1&P-Vf$SavqZaEVUiUn?&p{%; zH<084b_lp$1g+t+?+`pTa<@SejC@`}Uvty5k@*&ZTy~D+#*zm3^ooFwj^t4AjOOsE z6v>s>3kbjme-xY`TuUmxq=k5Ug5(>>aLX`vPayjqB;8e%rS57a zse81yklO;1-llZfT?9Ravg;suamUCV1M(ygQ9KeWjE&49j||F-2i|nx*5R%GmN?l_ z0@`E^;4u!T{}%*Z<=8-azLqd3ZL4}o!EQoD+V;a$0`QV11-DOeKVa_1IN2VM;w2+X4)0g>R)edme^ zICzd|!O){P0j&r+38qY5Rfq#a&?!3wx46y);w}S^5171f{&ISGERZ zRml89kX6@pl0tlBMgZQ@C7}17fGY#p%X?n3pI}%foyK9=$i!|KhSKH!1;d(8lVvy@ z*Msv5$KjVK%J55@SD=Cozl5{08-}+x8fCu#xx_9z0-gycuW>iPGrQ__SvPp*?0Cez zhoGw$8+e{J&E={PoFg;liOY%m3_ROfvPrJ}J{hg&)w&q&BmvX7r3fxxZq7 zzzau2&a&9w_2jV1$cUHOW;I-vEVbJT*1p!wHUXL!*x<@~1aZb-2_ z>cHzImBfg&ZOgH6M9z1oWM6?dj?X~?_>wUNxNcF9BW*7lxoQ|$Mh88zrrk%@0rG&B z<+5ua4>@KlGk`q)Mk`X07oAO&-2(ZvgKgY52yW-dR)PHW8{h6|W#n^jW$b8V#9U^r%aePhS<8`)8wh2LJo$Oce@j>yd1|PhXh$uemPH#cY@~PO;PI48yh&+8N zx^@##iDtA1!Pc{~j}UCv(n9R040H7^24ZbNLqD`njqC}B=!lYiDLc$!QJatmq43C3@oF51jNV*fl>G?g-Y-%a zlZeyb3}ezwFJ%{COg5@UD}wu^-B^@7_Z0+{ACser4URe7jSDC^v)Wnq6oLzJ+5a{q zcv{s0G=Ra=jusP?87Hqz{_Kz7S>4^(Jb3;NqHe+S>z*S^!Sj#ENrMGnr;)-17isgi z>lR=S!EYZMDEk>g1US>rA;gmoEM)HOQwF9o6h|cc7lh^3c^a591ajGKk|R?bfs(-0 z&rPHaIO2=f0!P&QEO1vljNB{Wu5GWA;ftzx=L#D{&is+gX5?J7!nYurjPDa5cZIMp z`mB-Ig6NBM;-W86AxGmJIXK{rx=vcx-k_B|hv@4qw%ptJO9vm5dC=o(zp-viy2x=!wE0=i|7ASUK!Gb$!|TL`Jd zq#qCvkloZK`w(KAKO?#ud;W+(b`WAOP~U-|mlWdIxfS@D2W2U?%_Nt7N?-aQbyv)V z?KN`QW{ACd=Udrnh(&ERFeU_h2nx!Yf;uwqaa!6SE)@rM9OA}RdCIzg+)2Q?_;vLz z+;fO8zehwDU-h*Y1)YT5_-bV0J|v{o$hi%WkZsJy*-W@vr@*>IoG}LvNHlYuc;O5g zSRzJNvK~mhiuU@Skc8bjxIt15bsEXbP^EqZau*8QA*jPT1jij*L67DFo9ht;bEyH; zX{3hVv*A95)Ix0c6jEoPJ?lk)(%lTHt*9?2AZ;WrphJ*`DAvy*tr#(TAq^Fp}v#d&t&T%!yKw) zT=?1(>k#-*Ksn23qiULQg*vH>_U$H8ca<8X46@Jfn^46AXGASxE_u| zW?a1s$z@*2mi+*k*>{Pf%|w>4T_#yMHzD&Jwbz;FZrY%K&upa&A+wE|*vu<5m*urx zi;FqS>z>b!`%IQsXPbffB+KiWk-H07!TU+?S-}lPZVhCu#c8a8tWA5*$}U3IW-9Bf zsvAb`5oC2?gR2OhlU0?~jj_fN$a+Kvoz=TN9&K`VDsufBkc;Du%oo||JA)_hdz_tp zFCJY_cFsW~HpnK-zknPgf?we^9TnDV$*69>LM|>RB%Nzkl-vuah|cF;I8Q{LdjaEu zZpbyS@Z2`K8go&-(In*JFi|llp!wx`U_u;D&I6c`a4HttO-QZ1iY{wH+Gav*LRwof z8r2Dzo7-{F6Ee||?Lu(o6mB^1y{~p|8-lJ%?jxAc+%!&B3ls1r9nJ%}RfNqaez28{ zed32-o|3JDiR*B*wLo6}!NN%~$jf1vlt^VZDIIf9VG;>PjzJ!d)g}2*waF_@ytC*jT z5;Q?RnWNQ^zZ+4uAZXHZ=Mmg8%I-jZU0tVaE##9BXE)>@eCSD-z{}vI_oG;d?am)G zq7le%#Raq+!H?Mn=2Cv!F&EhfkbezHzXOwPPK`4#dnXTUCNnm9;AP_DCcB>YL@JZL z_7k~GCi;06Ci|f7{tJOo+sG`(U#0Ps^}=Kw?ky0+pZ4Uo5)dxC3X^AkQb=;^j(Ezx zgvrF<{y_jSYvhhTP5^>Blna96_^St#_hIu#2yof&L~!#O!8}xbimQjo*Y{517DIs- z#uF)cPNn88D%#R;J}_DXcv$MKBvgG zr68b1D?5YWmR9x*3dUeyy#qn*B~mx$s15f46a?c?UO+)+T_<;lfbR&%{^k+sA^TR9 z>=hK`Txph7L%}kfX=f-{emVf%e8Fmz=a&dtmT?9Ion+@m?!X}+x19(e-r@(C(u(um z0#ndw$ZB8;8YJ#rm~tKW7HffANJGp*vY0PHVcIwHMrL6lZk_Q(_rkhnT8E~R|0|G7 z(*$$h-uW^k6dr0zw0=pzG7Luw4?Qr_+(Q@Sr4YkjRKCJXbO9A1*VeC~X#EyKtf=Co zaYs8-RB?*1TU2pIKtU(1!}o1TZc_&daf+(YR31if!zkMbMO{ZdNnO`vqwG8Z$7${g z+5Q)GSGywDf zk$nr(aQQm_?o@2{GQ~yTd7(uqo?h39yLkk^5HO?Kg%rMVX)OVBT1DJ1P(1IUi|jZQ zV@SpQ6N(oc(ULkdu^Bd}a%6{L#=w?kD5ck1=@#+zOIO+r%)>XbYc_HC6fpbFW$ppI z(R{Q@whP|4x}!=~0dHKpnIYQ`vyqCw`}Wx+a{LlzU%-X;Bg`gqy9s8KxqXU&MhUa= z#Y-F*d2yNmVqccQ937hU(||w3M5xT!jm_VKIlI3T5KxB-{RHOh>(t7AhdHPr+&%=m zjNEq!jBPSRY1>VXMdq}fKTqne9yD?X0e^#vOn6BJ2IMz^TvSAql1DV?o_G7zDReLM zk!$`BFux5M{0$Zm1*<@CdW-BESdfEtkKi3d!J#b14aem2i;F8yBqNn2Jq;oR_{?4d zA3-LxA{62;CrRNZJc3e8@$p9n-0)p=H+%=#g7a6s0UTEp?A*Q5bnM zJBBX9L&IaklNAeUOUv!c-O9bn{mK(oOoJ(vC7`QZ2!kq@;lbYE^6_9(o(F>RV$9iq zC-$`u|CD$`fc&IOATY?6CzdHOk3Tm9voMFdIM|JOZ!D1@d>dc#{{sJ_AZx>(jqla6 z*vkA*h$F#YDL`tLf0h~sYw+qk{AIP zcpDmkDW7A;+c6V(NyC#=Nn`u+KIVRQ;O&ZyRoJE$%Q;{Sn}DAbo0O82n}k0(KziSK z0QpTrj0nUe*vAl~BxzyN(xl~mwD1-=!8BB3pGSbH!WT6t-(|q7lG3~&sUm4h(ypYB zlMb5eHl+07`~+-LbJE4c>q*W@nxtt-E0T^UUBxyJ@b6cw`yuHuFx5CKNiPtJfWe>o zV7BL?1n_rCl5LU)qI_Or4Zg0D?3^sd)F`Z4*7E+=12zLDI4(oy1_36yM2J>IK1gLyx^hhvK^A0_{qj3cf& zihrl@^?`wU>~%*5FnV$FJIO1OtCCI0=acX2g?dLlO24K9m|7ER!rPes3HbWa`e1#y zJ`zg_FRze7iXK~Uk^{4ikAKd<(@)e-))%21OEK?*8=_9>17`zT+7>*QaE zb5ws?e*y77$G>mz58p}B-_qaJcVYS$)GE9KuJ6(Rs@Lgr^u_w6`Yn2s{u_O_{sr1E zgX*jAmP~;H2AUeai@+D-XbP7rKFkOB?hBw#mJbX6{yYI|SUtn=HnJHE%Vgo>HGJAD ze3Ru7eEuvUU!WH4;sKwe$B?c ze{}U#6MvYuXMf{GB zhYp_nkN0-r&$ZpD1Yk(+{Xv+ciFf}jwvzwx4}~{$5a`$p_p&bv`5Bv7WN01keKrw3 z*RhZ>mXzRQ!;G<{WHApi6r^Mh;*6jP9@vVI@#MF=AYR04?uZEC!7{SsOMC1wi&N(rTt2vQ*z(JXA!&3&oo+q4;?&JbvH!j1fSS3?j zhObXDA+{{o27&FnkiULR77l)djkoaY#tvWaUKW2t53x<}d)BQtflgNLn=^8#>PY{| z!~Pw`aoK$%8e;qy*0vZXpWG8;P&e*T?>8`Q`b9!kzsL$YMh0vq7>D(Xtg)cqUj?*X z1I~@7<^Vrubd^?`cZ;`5p_F&&Uww3#%sCXbYQ- zIh$>;cEW-F1+V*N9&$s+rXE+=hCc>#Z)(NFy!3G!wFm6y7Vb+b4Y-n-p2u{UpsBJb za7Z#&^UwGWw^5Y=Ik2A-RmXNj;On4=xguX2$FNbJ16o|ahjh>L>NXW=gX}^50sg2- z@!;v~96Yl?o;!@bt2Sgpj(nOolPewGt{D>Dtiaa<7~LeL;Qf%XOwr8B5IN(ckn(qr zDAos0$j%$qFL^2+M!ESo|LiTysw=$a%{a;9>+tQFm&~=i{@(2%*Pb6UR8^ib0DGFF zD9*JSna;Dp7m*tIc0sGAFeAO@595EOC>1&v{}xg_eBe}2>Lv}2(}dp0V|?X0Dc{9a zzER5+ML;T*CQ~_GW5o=Jw>vdiQ;_Y=26(zjqchWr+2Dx-lS+km7_W&A3TL+1^}G3_ z`oEK&>Qz`E)sAB)vOek~Oa;6il-K_qc{lI!gM(a=UUR^Dy{k>0LyB{H`f%3U(>2uX z>VGBl%tW-eXwJO8xpwfDl?IqZlV)kW?^vcw_$gL}uir%yiE-mA!m{t5miw z`bxgG*~k?|;?xYLQ$kK3Rc`O;e0C{CmU)m#MijT6C1F@2@W4{a19#hsp0R) zmAcNKlTGNpo6qG8)*NtHHJ_D^h{%~Dc2%)HoF=(4ww0|*YpDHl>WPgEhUePSgWkmk(unGlES=b&`+ECA21l+DFKymW0& z9%DBV_2kNgB>DVcoGl<4smVhHPn+Sw8!~dBZ=-X+;O=2QHk`78PGyzWTpwz4D9L5TwyL23GP%2Sf*Z}$Llfqz(m*cGx$P& z*{gQGrLDpHw5>NZ80IxU_#&+g1P{KzcEBJ>HlJa28|>@(#E}@_KJgd^-px!Nx`t|o zfhZnxQIGdYI?%@$;FK+&$zX&`*{gGijuv!0CSSk+OF#b!ZTVm;09$LOBSL9E&oFQ4 zuvsBeW>&G3HB^^|clZYVvl^*Bu|*@oXWC|k@PUKKh$naIL-^IUHs}+xk%A|my*R7* z&#Gdq;)8*WRr1&}@Q-4d9Hp?>z{FxtWgygtFalcR62&lSw$`8&hUkk)8G{42C*JK|I(*jEL`VcP75SZG?pD7a3DmT?YKRex^!;GN-??W&loR zhnz^c#)`9I!(ZUng`Mjm6)!w?Yy%-FOhEHRW+({tYNQAZ^BS8r6tT>lCIkblfq;HO z0RdM1ZJJQ`ysYZ2to~i&l!79qFh^-s(=;QJ#}--?EZ0PRusGlvj1aW{ZIY^4j*ZkR z*=ml5fLE<7Vw5?oA*Y3_i9CziaY|X8qpaaI%{Y-$lh6_2eS~k|!#pLMqvX}3u1DT6 zlERY0zlOq)eCDjZjHNElz`Kyj`x7s9+cII~5}VGnq*SwiT;dQdtXj z(yUqwofFC;fig#^tg))+18xTXeca|8`8izNCk^QLjbUxVtVIJbxEqCEdoWe@s#_^s zue2I(D+Al{U^^I#i>3G#LcUz2V0bqZ|w z&L!dZKb@74lEEYyl2T3mr;aS;Z*cuSt?;t(Si>xgQ=8VXQ6Do_n1WdusG;w{*ga$S z#gYhiV^UNSbH&tWm_Lk3%WqRSlbBN^6R)Sshr1|_kfzvVn#p1L#FQKr7NASH;*4L|ch6 z49x&Ca(w{9V0P?^St!I&cPayTU=oP&K}9L{%=mO?c@5QKc-_$S9vj4lzdB{R3d3eZ zLirGZkTUo+1uqC1KyMOttAj3Cz-sbC&PZBm~uzj*yvGE&35*G%Xa zrTu2jaACVMbyI|Fa)H@)pcL@Ah-Gk1rDl=ey7*@anhF1`42I7j_jriH!rf|2An&IB z)65Y}@pSpKDPDqS@&$5qj>j6XX&*9UMkXzzaJNFlkP!#UZPXZVBT`h3x9XEdpNv}c z175wt2i&O0OT%Xo4X&pe{4;GFN<=w!)(Uh_D96$KB$8l>VDVBPkUb88)|u9LW`%Q+ ziI>q4!d#pKM8vHDc50nu>jt()uzer*_z;WATE<)^>StgRJABpiNMh*(LsBeS>@u+F z6NC*aXPkR7$G;TcqG!jpmVpXOVs8~kB|(vaUtoAQuc^F0N{>{>UMvHKl8Iyt zcXnn#>@7riS4{i8LwnH>hyTpLe^bfFsACb3FGhE#S_iCIso4RuGX~%^>QJKxP^pp( z4;jBg30MOXm_)SIDXONuV+3-J_5_k>s+26v^{8R5A$w7CGYz~n6a&E=d$rkCfNHST zz`JWmI){vsA!XYRW+II&EJQ9?!U}QnZ*WF-0ezZu_Gd|!VJeg?So4Z;-ee${@xv}KWcdj{@umj_QeEQWGsVc0JtHpXZetY zfnWyy@YQTa2zTpQVP667uYwFd%l8!UA};JFyI=eeYMH?BW-~m}1Ya@dK}7HV|M0K+ z0G`kwY*zAxW#Fg}z#c+{RvCDVFgp~Nm=*Bn@=4r?%WN)RU^SN&!0cJbW&H{(*dWLk zFcIB%>qB{b5?x{fnEe9UIXPxT4)}tp0#*~n<2S_#c!`3Cme0G2O6BOzcj1lWB&>M*K1suq6(x|x`b#+9#L1>kuHQRlg63ka&&nwH{2 z@Ud5eJoU4%Bm;=}Fo8B93n|(B4c`Wk23iQzbhCxP#Jgd{LRkOJLRdGw#|!dDe`Sjy zGQ1)=RP?l_HP%sc;qk^KGgseXIZ^d*h=TE~acb}D7onoUO@r0=4N3o{=S*b#FvX>d zX(F1NR7x_TuLIaXRFfyAA%(%Zh2UBfTzJPIoZj@Fb%XU{I z)HAk197v*Je4=4(oS7Xne^R9LCheUpg&79n6Pm#Q4H9WegQ+{2=pFttF&Rm0651XN zf$4}}?x%=kq!Hc!z5@P3zbq2RU%;ithwz6ds65o?p4qc_e2yQSpcD#dq%Zz*jbMbR zF_IswiQ@ZevII6(nS2$|LOjYUdvyV=9iwSMB;JEYgYa6U&^j(dH~gSM?vk3U$Ar_zw1U&ONd zQMip`;go^Z7=-uDRIr{L3mNdgO0W|&d8iK2z9n%Xh1Z+L3kF)@KI@y2yQ&VDxWV% zGYG#z%Vr{)R7fxl!nr9{e24)l8d@u|7N#iCUhw&RgCOF(QozRnWb(-=9v#9WE8Mhx z*0jbd3WM!FI__oyr4e7{TaEs+)T$%GuNCwQP}1n9AY}oGNzgx~EI{i;d=qld=%?JwgjYq>tT_uXGS+;HzhtpkJ&dM}oJ2-ci_&2$O7njVz98 zKhADC2=R-t!Zv?LR|NDf8ucI8^t=wOV$lmoL<5YlHQ;z99CEZ1JDYnS`-{ zukd}RI$+Slup7uX**=V`$NQq4&OkW z+5H)eH6B*HYY=p&2zWLr0zv;2^cEfRz;QPC^TP&y8`%*tcdCmSK4~F&!)?*(ioj^QwQ^_qN0T+kOA;GtF% z9%u=g)(FrWo8B)hgMkKqGoBk^w2i`RdQU+2m3W+02Dyk8c__YWu&ov5k};=gN$C1w z70pJVO*2@<8t|y*eSu-E)-e9e9z06%Bv_AAEC&o{$a#kyYF)n+jGxHjM5fRrq^@a6ZhtNc7W_a9&6thgLRM zpU(U5EB70C7ts_${%DBBethvT3v(oeFXf@pEqir>azZF$c*tgA%y_4M5->RTDEazH zQg*+^hUoXuJuJ_Yo{$E!u;MV0voa4CvsF=j(Vz3gG0xG|Bb2W(N)PtV zo;Rh?F=F(lH;H2`)!V)Xv#U*$Tz)upI2Q&wf1{zg`3m=)c5 z^#6HJQpDZ)@AKn%v`Nl6-*djV_wso^?|ZN%A~5bpaDMj5qr%KzG!|O97!?99C`cmm z?=SNz=1Z{>Q_<%<-kbzm8tVoQ0glLV1Ys2%;aD#nxraaxHV!OOCUR%-80UI>MSfi5 z-o#JLCu27HZQ;UWY{o_judRDHU8r|hds8sE6;nB-cPcb!j7&ES2&qKCt&@(=sx=I_nq3ve!4a?${+LT{8XJ|3_mq0A@jRNkjxevK(fRf7 z>EX-~k=wt0pLTzEW@ZN0ET4}_Qj~8I#znJFto|Lfv1^bP;#+3(E>$)9tPFvS>~#_S zt6q&+_*7ik4k6^1^T|0;2EM=7TRG`NFlxrpBwa?6CC)E@yKX)Zil1;|1niySUz6-j z5tm-5E{>Vfm*~}MytyLuv3Ssm{j6hH5<>YFZt!&Z7XCg__`)V|pZwu9f%ZBi9t5F3 z>5V%MFw}nq{?3jt|D4gkjh?*n9H)asdU~nAy)6ptiSd8HZ3llM{qdOR%gvEOsBuWl zqqxiV@79Zg-7G}jR5>q+{O6*1ddgG`n+@uf>#b1Dg$XQI&5bm7Rh#cHLyZ(;+@MCb zvow+a-T&5oRO`gpy@W?uB1Q}@68YY7B3wkC7I}x^`8Cg5qy$=_6Rq%pzu*BKCX~uRXWaR?B?!~ae^d6Y&>6Ylk>pY zbNxKtCbykME#-MTv;(Y)#glHsm$5XvxpOoPwl+T$jFqU2*)@Rf2wFxW~(R{&YDXX#-+0 zPHkXQ!BJeYdPX_7+(iFnh+?)_TdGW7S=P!52^KXA`==Jvu8>fkaR6-*39p?ndjzMT zLO2PX$gmCN0;a+&XmAS`*bp#|pu|GKCOD$7zW#s*=)iexHsSRezpzA?AM+^*3+ZD> zdF26sl`r<6OdPDHGXeyySYLjz}tvcv_+AeW0 zvt-M#B!1vSydFwONgp&pWyJ3d!hNBpC?ipWH94XP=C_n?&aj)-5i1 zr@NSQ()VI~r@tKj-t!Y04Q9vXX0;3^z)^UP;eSppA}E?a!cvtccnrN4xuAo)t8D*W4lb+gDEBNT^t;cQDw$Iy zvcPP|{qOc#*EOZ?UASnE9ltXARsB_I6=(7#Szfj51Oq~q$RTFl*pzxL+pG7aUu(*9 z4d7(<%n$=yS>+u*x|$Xx!Biq&gY@9k;;m)uq(6RFZ#K)aUoaw`&|zrP2{BqH zyIqLL-cj3EvpIIUqp-!AoSf2|iQoA#-(|~Ap}$#Kql>zclU(A$vf5f{Z+I^0jh^?7 zLi4tGp>yl(PdYPZe`4GlpSf%}&K5h&_QaQ!{5ot&Xk_jjs)hQ;VznFw$?FI2NF0+0^{4Wl*9wFQh)}8{`8c zOs`pE`EyF%I{QQaGB_H(Ea;z2C*;*U3g`KiK&xZR9cI2# z#d1`4g2Y;h5B|6#E!KI}mH`&*zFHUA@;>+?sK%|)l(>l0dQxH@9SfaiTJ2THinRln zNS(a4&eVLi>-DM~pN3DOX3zZt`|jIN!&pIa{?NNv;!gE*!uA$NrvN|h`+$3;1gVuX>Ymlx@A3#_*#EhxzG($*!hs zUH4VJOZx??s!!xS?y|+~sF7DZH+$OdxMH1!u|Y>7zGGKkV}5pm?KNG~>Zp+I;HaES zI-if@KtZ;Dcm3ossOk9LmbW%eCU;c&IhbNc;2+(mn}R;RmI#^t^;`;+P?_SlQqQxYW(XZ+<6f&Rbv zyT2V{ftVLu1@6JV9cfnY@4WKPCtXj^v4e=^^rW)#dQSvf!dNSDw>*BPZ>uydVjpRh zHQw|5jwB_^cId4sTW#0?+~IXZ56i0kd8sl};)qBW2e?TD>yz!y#))Ho z^l)~6>JtN!NxA+V(4;uEM3JFLDNvPTboWpEai=H>21jv-nT5yXZ);a0P6>KV=EXhs?vbGn{DOn1 zYurkc_l~DQ_~CHwNMMg0e4I~jZG(PCZ_RtyzCm5Z&_|iQF1%hSwnmoR~Lj1giz*}DH=Ah37k3ht;_@6x~m02M@XY*hrs-x zU_P@sJt;rx_Nadw8e#tK4qa4wg-+$mt)#NWSY-Plc2lg-ZYAZ4D|j&O(?`xk=H=7PPPgG+UIc#mJW-}Jfm_?}T*Kyn19S^C#=AL)*GK7Azb zWZ#gd-Xk#$cnmq4(&P?nJ-)J4&u07zLTYQT8rT!-WWwz4| zK#bdv@-#?Wuhb7~t&pjjqRBafNpGa@D>$f1Sp8Jer_v}?mfoJEktqha;-^yd7e<*l z0*c$6AF|`l5skw+5+C%(@8}Yxp`J6^Fb0lscxbFnkCyqmJwvCvhddjn7FZ1)78)f^ z(WS~^=1a!hSzJuk2E4l%XH-Dn!*uo2%pMyhde!qU09*S0vcS#DxMY3$&JGvp28>AN zH?3Y8EGz`f?d(@fV;!c}eN$twH1BfH!2-v~;8T+H3DYGF`rOc<>*Flt{7$(~T6DNx zj>0D4eoR9nxM+`*2N-Hfm!=U`iKS_Ji?(``CR_`G$ zU?=Nq53m-n*JjSZ!)!!*M8U(;q%IGg#vk4UqCiS1k%VxvVA~``8V`&?8t!Td#BXz? z#kgI)AU)(QH$Yej4-Z=<;e=C{6n5x@Aqx;)%s;xCtZbLT~$#w9}47ve+7B$T(MA`vg-i zZGou0wu|-$-+*lLw>#=o7O_1q%`=EC9bi_=eZ9`+Gj?lx-nu3)SN5fQ5CuE@xXCAe zLEokA$*UR6&P5@6Q&_YWUNS6V=S&}@fd1zXz4Rr0XIf)tMh0+FW3TXIXaaGd zce;GB{a=h>58G8>ERAeg5s)ZX@yn8olS*4|EA+Y8ro zDJK1UYvkB=;bVux$9xydPeEE>!!(5WF*W!@!Y!Y%WBBM}o_GAS{=KlR)d~nV zTZv29b+6a#>{R5xbKVbmriLVJGG1Ib%@#=PTUma4RJ*>r6N{fj;4KVXSdvqV7ne!$ z98}80Y_0wDM`Og!@p+h4x;3^q&Z+(rmU1uB(g-=V68in4ZrKnR>Naaml-ZRH^{G1|3vhjH8t6V*^auVK8f)He zlNX-Z%&~C$oK@tla29o&POdXrMj zrm3zM+cX36f!nnOtgiDSUa*ltZ=Wwe^*6s<8dT|jVM+{JN*|WPn?kUY!Xt75#`|$1 zTo_(1{!V^G-Jiq=PA#6hdEZzRyZv)w?Wy-WMphonZl2$>F(%Er1Yk(cN z{dU!UX6BdvBft?!Eb-$$e(5+wfW@>xryx!;Sgs@$K$i zHOeOFO!AvX*iyhP0BwJJNa``0(v~&?V8QdeMT&coMlau4$s~)cUSx;4+-hR%kg>#Wkrjlt5WE{cR=uIcbl$ud01VRyPyG?1cZ@zFauVK zap^lyA7y~A&XNzL1bq`}R^i{%U}-8sQWW=aj-+0~Ce*`DBzwcpawI7n%8VEZ@Y4%C zIc!#uE3d2mh97p;wTCXXfbqse&)_mBqUNwdynY{xtTCQrIl`5e;p`QbwGiD(g>0Kc zx1|4GZ|>PnZV_neKGnkOjjx#cGLZYsUX45AC`{7HyTgkTSsAIKw1mkf%&Sq<8M
a$77t)rg9~Bc=?e!IYbzRI!m!&!ZOUVrtGp&cHm_;HLE6n-^q{^ zJ>}E2@2Y9yV>?)KV(FvUbJ!dp&6A~n+^ce7oPl1#lziOEpA8xCIAi`pPEY(>l6tRS zv&UZTi5Hh5?m!z->M*>H+5`NP|3vn~|1cYX`B+-L^6Ta<>2C8kpsgyYEwaI@Jn^?Q z0lR>j2sI5KbjiOn>|kxXKWyxBxiADF{X7A|H(%wh&rD7UwxbtVa-o}G!tuN*EAfND z(7URTyYpL_0=9&_o4&oe=g~03e))9IBH1>}A4zfq;q2;k!v(AXJ(Z4TFvH|tcnTEHkMOnP}<^pb95Un7p{e|F_E82NfVd?N^UBg z18F5aW9a{qbR^Lg>JAUp#P`b*dPDY9merC0LnDSYb1y8w_L;$P9jS(FS)1KeUk0LbQZk!2dN?dpjjNL^@n`vP}ETpSxmoRMJQtggD#B;{3KvbeQDP2Z*g;*x|Xdo zoKdq?qLy_|&B}>bwG+{6XkC?-^9#m}ajxUSI^!jBQL5j?tKF95UM^}%O6E+Of&skf z+BYezvBMW(mif>z>9I9~h6B|c>r>iTquj<;VNI)gxR^8i)O@Sq3~iRJWcCyRyVG2B z!M1`NU#d9KncO&`QL$oSdkQl1tr^|OVv+VB6gOKFa#996u-L*P~zA?TctPxJjvesD4F2&0uUZvLJ(McULh^2hZ+3;+W(KZ`+t7p#8U9ZY)k3tC2y z8Gl%I>Cjk!tYhuNNHBZ!!f42bmL_I1O-$U0F~!fv6JLqqYS(xrl9!9 ze#d$WD3&_Bl zBw7l*GU)0N2;p2^UZLwsj3K8RYS=X8w|O3~nwkUC<#BjV)S0o~DXBA9Z<{Np=GUxj z!tW%_mtP%F)&hoz$y|PQV-sG07>qL1fhKzh5?mblH`~a%cm1EAX+7)Eh%(7|u{@~!%(pYK6KBkL z2qV##oFj84oFlV;;~YtO3lA(g(LkGEaZ4j1L1GDwZb?MfZbGkoBgirfy9w|e-Wx?` zh&!}gm_G6f%of6{rWVvBk0AVAlk8#rGRD`B%wqQg;He)`vJQ^b%s6E-kzfMJLUB1g zDr{r~VZs7p=719(&PK?@@jj$-c+hAy{GUKvVT~QVmXuJ@Im=r9B@J?sV&0qI=l0P4 zz9nbTIiiFCY;T+~fa90NxSo({Te{P%N*m-OJ#fgcmeS@RbCzP7@QY;U$_L&y*_XW* zLikEed;JUvMWn#j%O8M^Q!GSjp*X~mpmQW9=*P%J)qB8cibHl-QOJ5CD?|Nn-ycc> zViGbgX3TWh@9-oWC3`mz`8YItis@Ujm!dMXhAxtW{>aUVhg%8;;w;MYV93eAGR6C` zraMdkE@nVU$@Sc27eoWgS`haeNE>Fi|n#|2IXEMir(pThaSq zhvN-D#v!$V3@ko3%ZI$k^8^X(8I|U6*1KAzOj!dn8-7FX@Dj=R55)Cf7^AD|1BxZd zIg0pYN^@_9o{lefv-j&{A%~cg{FtHcP8NK=y=J>3sEuKYS_18e-X2#mQjMAf9@SBCBcz zobnqIJ4D?_l*Zp-?qU3_p0RZ!_g2=I)4ddu<_@pStVG4fK#I_pB3hXkk+5mT73MJI zH^lc7E*DMWv9SP?Eo*xXt&3zHlc_^ak^$qn5#xCrPc*e)bMg>o`>hbj1AGoRH&?jo zYlx)l%lE=4(E^BIYGQ06a6X9F-&VzvV_VNMR+s$+~eNZx>&t6neUHuZPZMVl`M{Y4k_eNjj)%b z$H0}o5%zvaR!WagP96y#IwcJ21ZG{McI366&}>8Q5$_5Q)d1rO!1!FUNWz9FcA?Ep z>jSHSEf!1PG!J=@vvpJ*T95qUHA*jE9dr(vHikfUq{gp@J+nZ#LCNPWO^{wXN`HXU zLtDwgLMSnz_aSF$KWKgU2kXEEVeCVG9e)lV3W3FoC{J+<&@6JWM&kaj)K9-qGb=6b zh(@Q`Y8mJZ&*&t}0`%F~$=}tRqCX3zF|;z|KlIi0m-q4?;H9d;^qt+qUGJSAqy4Zo zydT2o)?gDdom>4eubRFp?bB)QxW0aDFztwklHQ=^_oM)08u=XF>uT!zh@Z2hC$mXu zoVX389`-FWq+j(N@p8F3ygm3tWuH7@BR$QnOH z`NoGwSXPV8^Ty>sR469I>mZVv;UJj$S3gDO*{mQv3LHs77a>(|v0~sRB2+=Y*#;`R z-ptHsADopoP}#e&v~tg*s0ZPX&#SaHO!_=hoDN)~w@!HBsp_nGDxzc21P=S!?&vf*^MTMvM^<>^aY8Ka?n|!@mGR>JG1TR zxUk`-6ZrNmt%TB*&%7W*BUpYjK)%$Z+=@#;<%quZwm-N4vn)XWksk!#v38Td_VeOW z=Av2~8OuOa24RDhIn~18VzKE@aN#UZWX9J|<}e8}Lmt22ORfO1Mx=xgpECjE&5niZ zJajZJU&MmM^Z@k#Xvp*W*N_}%7ijNo(}rX&3?~RYO6JFW&IXvTEw}-65V3+CHM(93 zL4{*L6p$*fux1Dc7OG@B9J=%&qoz$liPWc)FSUMEu)W}gqEWJU)t6bOuW8*`$YaFE zSK!ftVxkovU%G{71RkZC(r)BtK3QD49>&I;7rRvKON|p|4zMH z3h5CZY!mt4)T^Vx?>LL40394Y2I7iZ7)JxPdLexBG0A#a`q4AsZ_oo1(?)LQlBbMS zUNr{-47L?urMZ{A{6b~8&P1+yA`^~e`!p&P_Ci+PvewN@I8jzx&!IxcFY!A)ifje^ zr*rV15DcrV=c4iF%nM*RHjA<@tf5^&SVe;+V8Yb<*{_#hUq@yqsy_^WicsZtcE>&2 zW!Mz{?TCQloQD5)4roD6j@5T_Lp3-uueJtBqvu}>EjU^8vqRWE|K^mZxNy+DB5YlN z@tlqIT}|*k3qHdE_BT~|IbOzYR*b>kgPp4OuBT`NwF9^gb zGyyMf{OAzJRv|qcr{Koy0WoSqjJWVj1gqXHqsNTEV@fx$9utMvKJ*C@6`;#*XAYk* zG-z`Q5rcO+1=)>yAr?6ZUpj@@voA!R1ofKwOSG=vJu4u`Itrf}o)Orxsq88*cue?u z3LQ8o2(KO8{@wHA&j}H^gRRus3Q<{$2H!-3IEgvY;^(4#aw@0l3sAd3e_J902T8_u z%W!@8^OVW?u7KwR4HWlinz{8mp&rnYJuKmoxw6NEU|-#*aVW^ILLbAz@=D!CYRYC$Hk5RLmby?6F1pZ zo4A$uJ)NR$@gVat z+R|N?oSl@TKl`fG!+K7|=Ill32n zT+ko&l<k^fHv|hMuhla$dt%AQ zB%dDx07nyjhq3Zu==t8GsF^gkWLwaEUc$jkp*~!Y?5-KDL4Xzjn2rn?+wF>m32MCXhv! zui3Fox><;_0!8xzcf>=)Bbkch;Q z56>b!j*}w?1^a;*xuD=dV4D?7%@JydVn@B8kc+Y!)Zv)QsjD3&IUTK5kg^@Y2MC&8X&VX}Lp#4F=@p)zNSd z1$gkKtV=3!gi=JZxP@sxT_nujFvNvFM`%3#Psz!uc(RBc z^Qj{oHnm|~hLOZA>b(ffFlW+ck?J#-|INqE8{C{vfMY0%4iAxh2#k6jo6(KTZ_NC$?d&Q&GSq6 z_~5;2o=BNCCSQDQnG7R>XZiGK$S0`cl0V+hMf2dhG4p=zC@POJ8z-}3Y*DOXHFj6= ztcFH6*JsLR4RVLo2A$xsC3} zYJ|gV@rDV*0ambORQZ?a@&IZ+`vdJz+vP#c)sOt)@k7Nl4`gc%Z6X_1DahL|@Ks#& zNt9JGSaHPpf~=a0ru2jG8N`A^HKK)HD@p_{yOhP`liAvq`hn;a4I#3qGc?a?^Ze0j z8+p7I)Gn!<(%w_#sB8+vnd%H>s*O8Ly-Z(p{*Fcbbox-GTVE-(8+RnlW=*I+C6zo` zKY@;YwMHc~o2(Qb^~VHCmE1WaVqfyZ*wh3IVtmy6I+QAE3<&AV`h?@Iyv&&92{=^1 zQY2x4CsZcS{7M`f*Uae+|4*mSUypt^E?yak+=WUTd)03Y>_xn8SlicSjo<3flaHKH zO76IHTZkDdPR2>cRpW>>;A-z;)NCqot(|u%lS^odSz^@3+-|%y|J2!06wAVc+w@1Y zQ$n(ILOj8lwr*=3K(@@vyr<#aV(fozDft!t zKn;LkJVO3FXSMQyVw1BxxWQ3wzCmTV^7Ieb)Qw)OV9AGs!|NCBw5&#^=`+`1h@)iQ zUts@6`!Gfn?_Qe&K%^QAz}7t<>l4^Lt6cOm3bjeP0d9&;O-ys><(ic|j3XPkCm69+ z7*XpJ7f<6}sE~J)iwLg?n+&v0>K;mhp|Tj}lklE=`9Nbu>BBd@8{BX)xy_qkflC{c zuhw->=U}#^s%S!y1p+i#SLAX$z7(aXE8yc+HB~jylVLWyGt$$nfOfH#xVh8zH`iG~ zX66(Py?|7_T zv$-~p;xzF7Dv;fzAEI%J$e%)0DI2e-SMev%OrlpJhhR8lXU!upmQlBd4}~?r;HPDr z=w*KvW$c>BUH5S#=fsh9sB`p3C?GJ!6#NN&0l6KiKkpurLKNUZ=3&H$ZqnyLp5(|E zwvlar@(XF*CT%iXE4R@O_Oo#Gz#Hpy>Q7T(UAhJ+fnnPab11h(zwZ~a+@*Hw&8gnT zjt^~PAH<&62GJuO3AhIr)R%ENTe}d23m{4nXF1M_geNU)O*i(~9iDByFo(Lukq)?} z(i58GTN;}R-}iD-92wn?0moY+>6M}B+e!LT|h0(JuQqmTdBamqWv-3+flm?N$-CA_)zeHX|K+`E;P zC6`SEMTgqL`r8amgIG-2WRV93!(45Fm(#4&jx}G-VHQ_*UW%#w6hQ;ht&h{x8S<8? z7IRbT+7JKoJ6pBD8G+8xD7J9?DIN z?NXV#NOB?b%I6nK8ZV*v;k0SWvCpuG8A^A$sDPjZ=N~P#b>Gzzs^=GJns)9Si0x6@ zy|IS6VD=g^tstQ0j0Grppk%+{d} zWnlv<<@pXT(9@P)&YHw0q|HZ4^W?GEwO_}+^;f#^Ex;C`3tw_?L)FF{A;MkivdV2T zm1ZLuTu&g5G9IQi{SVrN&GQy`({Dk@gxbLPv2~`^VCX~^f=7qNYz5f6_ASUZBj^Bn zUq$b9c`$df%T3S7T~b!iX*9|;Q}f-i3ZjM_EyXmN=Xk9)_sq4{2$bwir*i9r@qtRpKB7@y{}wJ}%fykvYUhYnqP|0peA7C>48rp$ zYvV*jh~y5!bs(R+!|lYp?bgXRx+})8$IU2oh@=u~KIWY|QyolTTX$z)NLXQKbtSk; zm~I|KO0qr_+@vN^-qrg61CE6MWB=SkmXFi~wfunb>==!B9@rf_kB`$c+^;x9Bo-pvRaS^D5Rn2>c4F4C=|&wN#ik zB4q00UojnSz6|%h%Jl@y$RCl}Dfc1K`ZV}-OqpI%>2p?d_cgA+l$jgzkJ>LdmBpPC zq_Zf-pPVD`4fv@T9+Pd{PeS;M=7{Y?sW7@CWkIvod<>t8&D$&io)OtYh zvDN8?J`XpMGuC{jJV+cBP;qnBxMCzM)=o;Iy#%>rw=VRbyWgZoKDdR@0+eU_IVXjAy}&-qukVC%H-mI z+G$AN*L>cX3-8U*Vft;Q8Y5TEA_vd;L)G6JE_FgOg>rAYuQ|34pns?b{M53T6;i-WpJ6wOs;G?^B0-5sVH62j1wd+amT z6DQ*mLVG7}A($a5F5f5|<_cYZO9?`VIzHBmJ%diad`tJBz%SD$M-nL}X>Z~C**ez| zS{=;~y0T@v>P^+OPrFn18@V#XN3nvg)@xE4Fm`lQp4EJfC6acZmEIj>;}qDuUTnccIB%z{4$mprg^gc(Ao!LmfxNLeWRn&# z_L5{v1HXA4j(im_dj?1!oyao+ZazolM%byR$f*yVQ+8BV&&No}PUzI-%FwA-w}1CB z=5CF1Dk*n3?+bERV6W8M)KxW8y4nvtyPn_mYZAEbwfJcjdelZb-I|h+#9 zDh|v;dLgm_`%lSV_uJHIP?6n_4TKii_ZC4|&XT??DJIZ_88Y8(h%^vV3nWG**d`8+uvqJTqT%( z_1-&a1<4ZDpt`dQsO>U+So=Fj4dCffWX4%hk)SQn6N|&YmPMoT>{Wz+d!kO)C$HglrUbiqlVd zCmKEZd(Je)yXf`xpPM+B(fP zEAtD*s|ST=HAhk&W;yg`F>%K!38rf|7Iy-Q*R$BJ8FA{w7;1kcvu;w`Tqp9Qr1T7-FDRa-5}O5htrKA+oJ$ejmc& zQ;d4d7ZGIReZf>axT8C;S+F#yD@Q_JYmwugAlWOrOD%66g0bdiE^!B|#n22Q>JGU^ zk1r(Qg2SQhYS`7Bt44^}a2=Vb39Y@&c!aiU!6$9yv^hJ|teUNg&s?jF7a>SDl{S;L zl4(Vo71ZX^0A1_5EcjCUyUrSAt1q=w_ZnI*51;S;Xe=I zgS79vc{Pa%D_$oraWVLSY%WUA%R1rMLD8u&&cFN_^P*k#`^)(OR>AG7OFsv7ig zN$YoS@DiIwNcRzXf*cEGN{)1GGTlhYb9LJ%Kk>sZV4@;(PtcI5X3MAM8>6b2$OfyU zGh<^i=f!63`PjWDT3lh+qqdRAf4VF~yVf+F$!ShhUM#tsofbQtzTb3%E!p5$zEi%X z$f_96&7bAC-zs0*0w}E*t_Ci(QX|j&EZl&8Qv2uZG*!X|WaqS*TCX=}G~B6dSLT_m ziYwf{9!v8@Sz*mSS|i=p9Rx6Nd;^}aDlbzVlNlSGIZvhMj_Lu~wZ!b_V}L06MCoBZWKq-$6|kz&fs z(9x|V#>94(eam^u>+71zgUhe4PE!qQV%;7y#o7x z+}JBIx@ehW%rymi$Mv!dVH~K3$Io>;N}%?m9oYGCM0m#&}jD9lNY0u3p0a1F}O zQ~GKKh1qTmmxuKF+Um;?*M<(0H{&)__JfW^oTGPTEz0rz?!5>wIIs~ z_iAHn23OX0S(9f=Wt*jTXcKwwUTN#T+ll0TqGFyzcceoQia`J)ZYzHHUfSR67t<%l zG-Dp+TT+>a_?7evVyp|E^UDr3|+R_8>TC{SUQ zB1IS^ap&)(Q-Uhb_eg#w&%&Ica4dp%V6<;}4DKOI4Y)eI?lvOP5tUnrs|SY!i^Y(~Iz`i9BcS)n z&lc}#lga??>6l8c{cs-J6G0o|ejNK;+}Z1od3k0NkeHzCWulV_<1_57lUVF|zfea{ z#vhBpf3RMS`tV~)Hv2qly=N9%$`a}Wkw4`KE|6w?kGqYcPGMd@6 z;{bN0Us%d2C7^6uC-RJBjSx|q-up~C;YukiMOL)oNVTkMTMys@zV`v03PXSLq)i=P zwtw8F9x2X#N;k6*5~0*ln6>6dIHpP@$6oVRV=gYfG@a0QuEm*r z6%qnsWtmP409(IBB{OW2ta$? z?6hQ)AlN^|^FCobC*Lf8%B_*2qZw6_OUFOvQ`THA^t2$vmJ=&Q|WCG~o zdRQB-&kDJOZ>WMh$*0a1+ei!`ue7c(j2rtb4Kx4_I~~@51R0No>o#6056*9@=Hd>f zgi=9Tx+ZR~^@&1s*}Kn&5g~X3o6-YQwF-SF0aM9{$g3KoGWdC?r;gRVsnd=1PLG|(9}8&Q{w!?Y$D&R$ztX@qrU@J9`XJ9s7wm@)=M#q&HV(*Rx>mJ9WCQiQDq< zvWcs8vlo`apgl-&0NDMAINMcR-!p3dbtz!W|K=bsyxzxiW*#-^5P0~_K^eZIB9Dx| z6UqobhJ;$M&6R3mx9gn^;NHSc!)tYbJaB73VWwpH?uBPuV>5SfsK~}aGu4d>ezxHA1O%K>)zc=Q_Aw#ft zR0785Y&KiP+ka2~hg4O=u+vDH4PxfI0A???II>Q?H>$dwQ{_Vr*{ol=l+_$x3%fEm zdY$VC%Tg$0%5`qpo7x*mcbErj77m?t?JctreG2P5cqPM@q~V?+XTEUM{ULa;EEzR`R#A zn8GJrJ_vu>rXtM)*Ocp9XQfnmx%$FNTTDUT za1|3GvoB)QP1=q^%e;E`=KS5QKL=HCEe5BXe?umqqaY3S^Nv`gtG zSb&9<*dHh90pkwW>S<_*t*-u+3~P3KQ~blI#(?PR&bL?v8g!TDy|>2 zOVWwp55TQXmSuL9DP)?~o%xBq1xTrR3y8s7WVaTlpmE{jig42!3A;R5J2YFRo(*MF zZk0sR7Ko(KoUzO3v1MNmk)pZ!tzFG2<1sEpTH=H+sbo6yxr8aUb?!u_D+l?2ckDJQ zLXaRw;AY93N9R5#ywg6>140IN_O zh0}}<0)!)|%U8eOSjpV8oIe^)n>_0mA7O|>-^2dX=5ezx zRdooope3Xp)^2*9k&EAX@ zR_blIq+RsmN@1XTx6GV<3DG3j6T+n9KzXnSa?*?}mk0L|4AXVQGL@n-A^$c9wrI}F zEgs+N@ayATYm=UI{mETw@^&?~4$BNSk4&XK0>O=54Nw|pCcO`hhMl#NgPyBXbqyH0 zjtp>?eVwo2ygaQsMjKB#bob?ia_FQ;!BP~rMnWAI3L7MZz8n*O5wvwTLYuu|#Q^%_ z9BCpnA+{dnK_M-zX^L9X?1vy^qQHG>3-bT#o~8=5G)4*^ubrKsvm^E1IzC{c($dhZSO5hkF* zD%J%tN>-&d-YWjWs`>>|2Xo5a&0%?Y>B{E0mhAGNaC$jxe)($cDj?S;?bn47CO{Gd zLUw9eAho@CgxRLsS&gH05&I@R&L|xx96uZ#geT4bTK4v8zXnkRk47q$o@41PV|_2m z`_fng4HJKXH%_PxY#IDV(*U9zOYTy(xLgl2ed=|a8_uXMv~;voSK8@2Y;#ld>6WTg zMyni{L5B;4a%!7Zr6$N7T22raCslRqXdSX6e?j4&rG$oHP@ZS8ZUb(HT||MPJLEoF zg3FW%p$ubcMt0w11i1u_O9aOHq+?g6AbEpA=DX~bL|;t7`pHlVK=;0u$xcO2F%{xc zd~I=hqqS4|$C6yL%OacKO*L^Lfh495R*Dr6 zYC>A)eH0O4I^09=H~?6^q`WrGnvV!E1%!|o*_)XgkAOM<4G=~wIGkrN9H>LY1NE%7 zvvcGGdBkb4}9VJj3^tsPk3b^mfp(R^f-k6w=?iAZr&8}NHks2BHuF8 z6`{SbHmW4sErn{R%2&VTO8$Wq6PlD9iC9}GK#P(zXSAzby~vujt%`B z-ZIXcp|q6+6+=0OvItixz`uQUrp(+pDdCHSf;dli;);krwK%I0WbZo5zM1>w^P}Rb zi+)xh{FCDVc9OEiePj{L7T9ym6lnYt`-Qk_@Fy{wWS9Vls)EJ54G{NcUtq~uh4CJZ z_G>R^PH>nqbTfUT1&z>Rdt78_5ldg5Mp+eA1@_a;2A~YM>5ewu4V?>uv(`#9xscx!I%ZS)wQI zEsE@wJB9Yv9c8)k78t670*#ipU zW`_L@KZ|Y?V%iij&&V(|icj0@(Vs@BQV}O^dniN}T!)8Wc9};|ncG%ZFd)k}3A0!0 zgmKVIqNRWuU!lWW6#uLK5*UbZ0iJsXj-L9N)q$Afi1}q8YmB?olvlW6B7ZKHL)`Puv z=SJaKYfk!g$u>s+6)_kBssAz5cMg3A6j=V{bXOcIHOzwXFBTGp~|d(`OKJE?$N=LSl1bZaeiWYjaG^5|3==@zAmW zk_^EZjsvV+7@wEWo6R|V7Y8EYNXsg6VDHE3u~jgajUbbmb56BSbHmxoSWd_g?t#j> z!hR(7v9_3k2x&jT=+3JdY)3f|2q7s@YAgl9`c0b#v30(o52DVJbb9&W7I*g|p_?~p zS!`jNwiWnnbFlG!BdJbRA2)?}s&;He`a(#6V+E8u**87u6$pmuuR5J@**)*A@e2i| zPxau9+l6rDK11$ed~LNlKHxjinPBch9SMBKJSTt};Tf)r@yk@J7)orF!uPg$7F(qs zxbP3?AzmPgc!>vo>}iKCR9XNIV`857ev;~+!%`0&om@r^E2l|ly^eVN0_glm@-$>m znY_Z+?utT9k&B#!Jjnmh1+S;>HsNt3*{`0ug-Hw?n?rpcOhb%qPX8D3raqL`Jt!p+ z?e1IB7yH|(_}c!deXmLHknPwCHO7rg7A5KsJBBXUz)PF*57_9ZedpjqF6~3vTzWfc zHFB=8g*)D&E!<9Q%ka0U1)UFp0D%`c^?mmpi3(~ppay_fX|24ZiG#oWOV<}A4|Y}D z(T`wphHzzxqxw7+JPav9q^uk`#P349VZ53gXg3rP^gNEv8XWq2PqdEtthc+ZQaPjK zxK&@3DhNTu*@8i?>DkR}?xo1(ZO3ETT2C!ulT-oS$!{eUTyvK(E!or1- zZVwN$qvRsL>&G5urTXcV^F?t>G(lcFtsn|g5x0we0;`IkY6G|y@sxIo+E%XaR2a9$ zI(nr7H}(bYLxT~qCZN7#*N3ro; z{+n6|Ur2r8DZo@D++{mwM(YE&v@Vz8)E-bXTn3vhrW@WcXGWzo`DP`Bg);6qfS1H( zNFvGyG8V}hy!;lgQ@h%Dj=$Pv%YQ7pzs2g)hMc zH3_~ub+JC9j{1tAz$8fQAC#(LJC5IZKb}X{14cp=LU{BfgywE%1>fJ@qO>u)Z>+^C zcd$Z?XGZQNCiOi9&!Sv~vI!`?5yCa2x1zN$rvo!F)=W|NZYd;txa(IbK?Gy3kw60j z`y%E`heB-76lsUsZ83&qe*K-13Qke?I%H+Cdqt6h_}P+;r(i2W5`3^;tY9@{g8e`r zM25PAVJ;ufPs?a(ag*RFPax+8DhIx10WhZyDtaf2bdZRKmt43;egQ5jt)u?}aDck3 zhdZ^fSizzTbu-FVMuqoa#jMuB8-#&a@+yYGgCascyf$mny@H5NRDdl`tAqjNpeT++ z?cqHJvLEyJHIdL(u*Ed)=}$bgUk^p(1o0%vw70CMtoB-i{^8Yk5DIbw5#nIT=_|p^xXHP(wz-K@ z(UdlY9F|{;72p1_S&#}t3p_!eJh#+@2er+53L(qVSX#{BDq_*l2%n0iUJC^IlFb~#235t?sjww8Rea345%lJ=T&4VF=o_C*7t#>pEMe`oQ9w!!mwz zI&>NvJE(YCO949xl4{SZ3U(MQ;F18{N5_~2peYyvVb7p@^IDr{+p17J0#&oa9M_`D z@XWfKWhDmR;T}^P7CTJh#R$u+#W|sq{V~tAT(qCAL+)}8Qf>8~?(3Tv@eAyB>FhXL z_x&{xpOuCa9^h%nZ}vAe;PrJFkgNs#2u$-q`QKbha2B=ifhekF(P#K29IGf|+@Yg~%!jz6hj1ew zYUcK?Z3{-GC55sva4FbZke!&48>F+Ba7Sz-XSh+DCzd^f1_pP~FvK#tqhb?F%5=Ke zkkgjG(y&|H2)5!#=SEx1MmjI5r1#GYm6_a~S@Khz(wU$5Pof8GD*C=Im_moh zC|b4ircngu_Ia!pHV%3*`xTy+{VX(TdE3;{+oDlshaMZq1=wdl?=}gmiYk^e`8CAy zK`<12nqpXsoD9C6{HJp$Mj1!eago)&!Yj)U4t!M2E$Y>NIS}DE;N?>HLl*(vsE!7w z7`Ys5((Y`KZ$c$a{W+G^w1O4O|JZ^`J=%xY{|@hZpef*vYH42IZ@5JKLgAS-J~_@(ON1n>4CRew$vusw zx)dhw1p_RzaBIB&0=}H~qY*X~U#3_BL?(S!Uzq(g({FJtpU?*b0bG+Yr+Q zpy!26oi02K0*czQ8YM@1vM2Imn$OH5gS&f+50Z5glHj@2^ui`?mKi0WkW8M~(U}uf zioA22f>)pmOG*KKM0&c1G%Eg^iZ3OSKNGb!alH-omwt0=)um%g4zbE2c1X%Rd+iWr zZ~3i<$?hBah#kjS<$Dg%`vDJ$TI&z5X~g=u=Pt>l3}+2-qke^L)rMKl{*6i-$(|5k z!0IFRVeg3At71=wH144tnVQ!T-3tzQC&%@J&5Z3}`hj@rR8HjmaDj)>_(XneOTRz=z35F%6^p0UH ziC6QOw`j&g3)D5w{@eTm*Sl8etP%Y{`_N9jygRPIHB~|Mf zL~MZQH5oZ?LPll5C9lWJ>`^u|bd7+P_`A~%=08Q{YrNqclw$X7-wM6!+{_(f0PeA4 z+{xk()ySdx82dL%Z~?`V7&g%U>;z^o4E={t@`8xh^`@>0kVS77LQp6;i|nr0@#;o-`!WNJ ztDVN=&UYw0Kn&hPesNueSaG+QdMUutB5aAymN!}P!V(x*D{4Flen>x>G`%$w5Wj`1 zb@FG|xb-Vce}?m)v_=*3h~=9jU}ZCh`~g^y(-cdsCLfw=7jv-=R0)-J<(d03>N^q1 zU^$_BC_o3< zqT8XPusb1u1lla$hbk+WH;RBNGGKfk%s;TujI#C9m2Sg6Z-~K_dhkDKRxD(bC1zPr*RT(ICL}xXVSTxpms)MFAIhKD9e2?nb45 z#2>u8tH50d8bDD z`V7AQ1HW*V^!7G=2SW*PZuV|VAC-*(z21p~ckS#_t>a?OMfnAAhdc5z7`fzWhikUU zwDSvqwxJ@yNmL44{2#crMzUq43`W?Dd?Lk-9GuA{>Lwlpc}gNVGP@%)!R#e+YzIU@ zn_0Oz1tdp^mE4%)(xO8(Y_9}=iWPqrQ|}3|^r*Lum)H*g7GZ-0={E8vX?Z~@3BC|K zBUY4)sc)P-Hgl1Vpappqfo-f561wnLL}&&#TrA_WP;rLeSssjC5@pgL@`52$J%-FCZg8ja5YJ#j z{B-#tED^TFAGK@wSdF_gL)uHYp?)%bx(mKTR2rM^F*PS@zh+Z5fefj?}_wMmcm3jX7bCQ#!O_R3ig(hi%^pZ49+t6a6G<6_>CcV&W($XSrN$5R5 zZ2`v?8MY{(GRO=CEHeJsqJR#pytLqommPyUYDETx>H_NcEyKu+on3Zx7;oSC>hJw| zP6|4^ul;@h{LStR&B@6*&w0*s`CQ(g_q!N{PKcOIArvi0BYkDXoH`lm-K~q~c`~r0 zwb>e3%J>ox$Rm5d+dIrGz^pxQ#Zvup)M1t8Fi1SMS;(9_k2Y2Cez^;z7s8M$+((0B zA~=RQ+_xa*obXMX$-T5aJ*vD|v)YiAns+aF{|vw)Z6FHAx!KM0~Jg0rQ z9ghm)j93smO=fOuAErkKoq8eqWK=fvC|=9(nC5g=`M@ht)m8SjZ$W7|m%oY_aOrKY zvH^uYPn4T-;`V?C{bMD%lclQdQ$To@}k#GR|Yx)_c#Hm?788m4>Uva!|&XO^=}1ieg9TQ`%V zVHh($-@32m4J^l|FifD3XUqBBcCs`iA?BUUNzbL^jI14RyvG#{-^zUXj;0RS3DmhL zt$X&Q=18Y4#$_O*=tYWs+LrX7WqlK7XjTABR#}k#y^)-w>!n38DmA+k@!7RjUDhD* zVI#po0z;vkIsBggKi<;+uX=#LljmT;!2eist11dXFl^o$k0P)bohDwTs`e0%xbXAQ zdBa}eHJnu`s18A&=}V|{+^!=ZJ}d(=z2vw^)F7bD|DyE+!k;f;%nMKf)OcL7o-*xB zA*u(}kPzN|KU^0eq18k5s|uNp`730qp%uJOt@tVEoe&Y;kn4vnVJC#9I6#*As4{RmR(gt2wY$g0>XMHX33dq*>tzZZ&GQ zS}-s*W^Cm*tG2{MyM673jg*hXW^}x0RFKO6sAILd!%?V6-^WT35~CqT5X27evJvBr z(vYLucr^720TIQQD&KTF6`c@e-A&;P9p@v4ko;Y3o)S&-I55&2x8Z6!!u|~Fm;DKg z#?W=xz|>UI2pGb0QL*+dGVSd*@?S*ONRZu3nQBh&IE5m?O!HfS?rLLl9WGc+QSOS# zBagMB!W64;>0@7@ycCN<*JN_v#>262tyWo0X?r>-{INLVdLBFs;7kC4{3y&?5#4$i za+T3v9Hwc3YDjOf9_vwR#5$weA79IDbPfp@Wbs8o$=HiY%PH4KIfCphGgUMNP-JX_ zsEn(5amu;yny%0CvWa}mBOq0_hwc2R!!ij5%PxSs`8Ten!$ae)gNNC_>JD=`6;@7v zpvHk+TY*2wINQ1_xaQrC_HpHDW6apYAN%<0|p-oK&oV3yc^Ab^kWO7H zep)NcJ2Cz|oa#G^*<&=PFudIFH#81J6kIYGuopaKeQQ8H*u>`8)7#ND@ol6T37M*F zy@7N&g~M`V?y9bCvp6xHj9=Dm$H4Do*gx31h$irAt;Klq>pL!F7u5pa8*t9D zn4RKLM@2@Qtuqw{CLpN~JEqd@-ay{2q)0fVTDGRhAGOIgMP;9o)07qi_nQcF^N`&a zuR0q6H9{;A)cesjg>{Zp@~!|2ufzxo?K#XWUoa;|OwMC)*5xkk1e246Ue?t43w_fc=SZB&LX- zq9!pwX=-MR@EUfRBC`;jMh{vdmP=$gEgmGqKe>sVB$Mwwa!me&9LRQ1{>U-cegn3s zQ_uuhS}12P6Gz3+&hLIQD!(|UJCtzK2Xr{VY6@)0-y4refC-AG(Fa57!zyXvPXO-w zFvoqvfoS>usjv;C$E4xA4?h&0=)nMsWj_1@wlMl`vSmea(4P+NuKt0jtaAv-@iMP0 zQ?T1Rkow+g;tuPqN{xBM=AY*bRQP3T3Miy=1H1_dvXBythCghgz-J3zvBA@Z0m_=f zi`Tti;zZ%EZ{q><87(Ri-hAWfER%XUPn2UHmiUh6AS@Y^HfOubIS=7Sq=;^tV;Fwr zsPMa)`8oxp5*u6xTY-AKAFBe9oEB0Bh-V5`hIzgz4a>`$mquEIy>+I}8w4Qb69YaS zE-bzzRZUQ=DJu|@074BRlk3!w zPrKgJUpaJCp1RJV%@B;OBszl^HwQQuAsh z-f#fJB4{j%fkbeZPA+-~F2J;>CJ^)PE*N4Q7?69#ADI=T3fY(Khhs5L zU|dfw>ic^7-cQ8R4*b_c0_!JxYDb%+rAgM>W@~wGwGLUOGx9>~<=_-C(x{qxvTXeW z$;RHm5sVN}qMbQ2B7A=M^`+A8{TT$#X?)lLo=wkKc)MzLHlb+;zG?MGeoB-ni2NWm z1Ud|x6Sxvcx=@`g9jtT zWDEWS)eYJam&ycXtvTXm1AN%}b5=zGP~BS#zD&MzAMc+tU>F z|J*zY;;4~i2jR*P-#maa4JT5;omG}nA?A_VI7-;G6}X+Ea;3cuUM3Z6Hk?hx8kdqs zk>|m3irLatw`@X=*4V)MS9t=*GGvIia%yN%hg_1}y|>EgTaF<_qjdcYzLRnU%`me+ zN#VA*PW7<9;iTrN;9s)LeQQcC!ifetoYMTaejq#t9BezzLqvz)DdG<}+eFv>-dPN1 zQ9T^a7&?NJt4>B|PM@Y8j)%E)3IsUfi*$_S4ERVva{D?4&lag#lue|bYQa7OYZME^ z>^Qd71re@)Skl5u9&N$@et1&%`YNa4Uc*~(4X-Z#rV>DfGL)_m)-?9(h?OT1bu(M8 z&!Iem315uP-Qkz#PqbNT;B-gn3{kpxO31EP$t06vEDmdomCi(|U;8DPM_s|cptDAo zxU@?INe2OszEQ0+{CG^9n`|u*W*k7oSfev&aH+6jlXpxVR%=4)7D^c$AF>|&R%Gee ztv$$CgpM?{2*MJ9jgOt*hT_2MuL@%_&nH@JsBfMmk`lmKsQ1*mcp#MdjiwRG(#M-# zMhR9@G2qmv2C64vG74QW z1HyzHEN5K5cvwc+@=(1@tT+xMp=|me(U7y@11PCb@ImJ8&Pk{|1&Bts}$nF z(FAJ0<+vF0bPBOppZ#1%CqqT0Fv>XZ6b&&{#koMlOkyK7xYjNHuvG$F2LbY~Ol_2( z=L1Ya_X(-SgCRaHghaJb-+wapLb4E(>Obw5wceFF3SS!U(llWK(om;fX>s&}mxnY0 zNWG#1Wex`i@g5t;lFk&+8z;GBDq4s$>PRGxOevH9V>#mO3QX>HAPnTYsDOhQ-45#3 z;LUVnnf=NR$X)r#h|j3{C(0N<=0=}OlPlcFX8|W?wKxl*YOc4$DdGz|E?OxC+^TqO z7M`7IjSvjrWC#YrqbDUmo6#3=@>4!Eo@)7Dyc~&=4MyTyXhOu*C5-#A!(+f%VjxV9_=c0Lwgn*_)G#pRRlmeRL5ZjVyZW=DN65j( zR=OM<5e=MdcijQ^9S}Y&Uj;ZM-I11w>8_mJScM$HNm@+E3ng$PAsfjHnu`GxLUZ^Q zI}PzfOHf<@ULjlmLZAD)o3pa8iCBZS1=K6s9bqa^r3brEwB0;6c=557j~Oa;wMEG^;DPi@D4 zy?l&TqEyYO(Yaxu0${t`h-Y~N&+-v{(C`nM_;=`ojz6UiKu#Ho-ib!rU{berUv3~o zJf{YLmq0^ONxsJzL&mo|zoi11ippdGK_ldI7c~KAfQ^3Lh#UQj#AOBO;RKuUYF-Jl zsisvG9uT$SYIj6lUA^5OQF}qy?nn4NCy!joc0)gES9QFYSm-yxn@nQ{f(2f~a8*ODO;X+upu3PZW% zSA@?8bLOPx(DaGoTH_jOmvjknVBOwE;<`{$fiu{a_;`@R9OHTJO%7$cyTJPu1p<)} zZje&#^jctZO}7pa=Ec8b2s#msiu8LY59OC!%6DlK+$L4?{QXwF*Cw>Yq$WtH&hN3e zMIK5Bm5tAvAVz@}ucJ4ht}!7RtMlb%Gt0NOtEl9N?qWIU?SxRwMri8rI%c0oV39tvtUhuM_NCLAx7z#UzuOwL|f_zq|`?Jq~YLvkC^*O)QCk93HP!F=iRQ+hY5l5pcd5rx9F^wgj0RO95{V3X=P!Z(aA9qrfBXs2=ElW zkIrVmgVwQ8TouLJa}8R=5H=Gb*PQL>>Ymy#TnW1veI?N|y5F`c27B*lgeq^{z^jO5 zMkarU$YW%2V#&wMmbTkFl>uv~i-~WNHW(*j%!z#)kp56^Gu4K|R5N8iU2KCvr%bs* z`|ZohJKw3K)%8vm9eU^4;dcZ`;5aQrjQ`i@UGF?Q29h=sqk%e~-?9z8Oy}-m;F2pj zCl713IR?7FYMm{nD%GvIpS{kXyPtl(@T z>3tU{ppf+1n)^hQgMmpX1!guSu+WRf^~DC;C&)45s@=*1KWRradEUixFlNY550JIj zZBo90WTg+Wag=3L-uyGd@tcI>o2b(2?0>=nDKt!g5)|P>r*!8EMCFo#9qWFY^w>Q2 zx@wOZZ$P_=Fr;Uz@Gq@0zxXBgIn6Ks?!``We^_0HDrHm;n*!>_LRh`i^BU^{@CAU) z*(eud5=u1X5fyCcOcB(mx)w*J5RGrk_ z>vgIWB-A>(=|AvNc)I8(zz4?ug#S4QPk2juNI3wo7N>7Ia*|3J@jhf_MIau7ZUz;x zVmIA1QXFiK!WIWU0Tg=D%cP-5_q>Ha9DpDeJ})$avTK8&CA2>hFwTk*bwjYb3&A&O zk6#rADAMWE07W(i?N1ZRrg8}U3;b7u0a{#mGkZ#y{X;(DOSq4g$nJ9;{2fg71WqQo zZy1l%^e$#EH=2M-E)-R@SHKnNFZrNKOL}w=4{MkFpK#e@e=XRt! zUK1>xA(trCoB-ol#D79n&4!j0gj}HhEp?vMJO>}~9=8X{-6WJIFM$0o?=yonNHAEc zcA$V^mn61fw6yHN){>p{NKux}-}=xJAur8bRs>9)ePr&CxW8Y*QUTj( zK9T!Ae20IY$3X!DzmOcHXE{${9S}TU!4d*)7nOMZ6p&-$r_)$0%5_p596z-d0215( z!OtaQ`dRMv9hmmi&+HRqN_hzOg|H1wO9QYB;%Cha0fJ1VOS4n(*D{Z*DJ66g{0$~i zr=Kd3RTBJ|#2pMP!4XfvCs$`ByGLp=sw`vwdF3*gX@Hq+1q0m5$}xgxU%0fB#p7ZX zV{L^`Uj&utSj&gUJ~;`YcEOp6M}Mrthxi4k6x(Gknb7#(Hr*wYA@=X$GH&vA*Z^SH zgT}(l4`9(%;9nq8-#w4Fk~@R_k|bIW)5`cRvzNq9CU0whHR=tGuT{?|4!G=r02e(ocXlDPF`Q(#M&2NCHRm4ESkfCg0`hkMpw6mR2+cb4tojHU{cQKk^E;BujN2 zZAL+^^0M_K@7IN|W35o1l79qwjF>tMT4Bb*X@wfRVEhvI=F7;3%>FLFH!Mll!GFYq zEjHy*Fi_^G8d~-^?AzX|{$v|H{xqWZ;K>=I@kDDY%mg9tv@HnV;KgC*=+V%ag!>PR z;%uzm%-Y2$l)2hORq!A^^ig*FD1)^z2Mga;$ePHP{ide+yYDQUW8G1kRotv4F1c!X zr+Tvr?g*$9tx9=>@p_6pqEOTm5*kUP>iae`&1TiJce|^(gE}4clQZxYxE8c0noXqN z$tzd1>$45+a(2;9AvHcud`kRyU_7D$ILjok&jIy1$Lx=p7zLZo#s58sf1J|9{& zl!(<-U^&y z&5|>&rU#i*+4CR*Lw95w!i*Ojv!bzx>inTqCy;qxqo{6A4>S^24{CkR-Eud3SoHcE z&$^m=5LLA057A}^V|%rDdD)Gs{fO}eV$?js7>NKN{JiL%8%q0?cplMA-#Z__TA#AuvrXJ6vJ>~ml!5qQMS#U~_KKiQXH zUkesvrl6KQCLa7nyB!bkz`9lSoOXHnyLy zUML$W+Eq!?%S)ShLaZbMJ{!Sq2%HTffQXg8)KMhfP_l8<{*49eXWu*cz_zJnlJlR$)#?m@$qrg-B!oEdg8D;8^ciD6Xmu1x~Frb$~yB*TM&3;P@xn zH7x{Mpq)^t;v|fMvhvbbi1lLEZ#4b(3^utNiZftd2zPD`MQ=9!O)_1cx~nuOKLh^;%X-?H&|Wh-V#T$ zXm!7xBP4*<&o-*=c3+%{;0>*_rBtQT0TVI6BR-LKr=sm~ zxVT%{GS|T(=ji$gBp^}MZ;tr72vB{R^l(DF$56d~nynM{F#S-eo6XCZI($@&kKxr% z$%~h@>xC#s^d1L{y*E5U<{7(#!s2NN;xsujD@WJA6<%>&o!eMjs^cJQCE2Uex+b2l z?ih>YscoR|&dM9G3?M-Bkx^a4C|@=?=Yj8_?-e$14J#|>!imvms;(3vohEOf&5ldb z@t=lBVh8KwL#s#EU&8Zh9MR!tLITv+aTW~8DDZ29(2|fdaUZ1MxMkMmij8U;UGI-} z#PPG~fq4mE7;PS|xJQ{FRq9K9#4o|i>GA4ZO^&+J_2J#35TB`ZHt23QmaxY#oh|E*@&I2mqF>yI?Ku)44mFtRZGu*WDVehP3k&3k*yeN}% zHrJEP8P<;qEVtAh5c#9+!71w9FyZy)xyWU_>=IrAjX62tr>lij9%7hQ<8pQ03ygRZ z0()GgPEjQR!zaUDcIVC(S8N0Z>`WRWgH+642xwtXv7JD9Xg;FdA^f9?(1bAdlnVN= zBX`Cd;5TVO7`a3b3N$=2Vrn1i3xRnIYkJ_U`4;=X;h!Ai+;{NL*4fPjknej4+{X~G zZYwL0caH;IW}eyV?O_?F_w_g2oP5O9`S8kv=ivYaj2SJX-jR&yC zbat?75FBOiXogkSMuv4vYiHU%rhN1t+@MGsBma1MJtC7CtC0=>p2A=QXr7<3g7sER z?dHS-;n1oH<&r@gKwGxbIy(?4yDF6w&cW>|@OnqKo~rRdIPxbVpd~q08C8-f{n0r> zWe6oldO+ww>|0Nv(Y=`hC*0uBA(k2WwqXLbZFo>x5lJ3EPq4=Yupp-;Aov(=#C(X{ z*ZeVs>D2#W1+Eu{pBeAo0)Qyfm^?-oK8FznHr|AF2gKYW$SV#aZ17KU>=%0q$WkZZFVQ5M9~>{NDn zYsKxsWJGaC-7W7pUA$zyP&@_y_;>c;+D5r?W$#+t0@qrBW*9n~h1_o{4rwgy3zERB z$Uv)$$`8IAyAFO8UXuB$xkk~17cFBZlwsil(+e`)xx$H91H$!q6ut-~(pJZLGnU=t z2hJSW>K5wO&?>Goj z(0xGKOtFl5FxRA>Xk>^{rIP`KC{%q59W|pPT%F0pJeJT2Y`X^K3tjNmd9^St8R6vu z6)#fEo5!@7Qp#0H|Era;eX@W|@Mz`2vv;@36g3J4{V>_0KZW(bD2uh|JoBh>RJrK2XI*mHqF4X$4{}x@Fy+qgPW|BOP^c5l3h}ph3ks!6$x4p_ z?e>{VDU=?Q?fmYK&*1!Pq?&`3v9UqH?nN1SfsDzZl36#bVjB=bJdHeYeOIE|m;N{Mn&R7-KgR`<(3S1t*>jNR1HzM zDS8+I?HTxA+t8bASixLT!q5g+RK(ETt|-}1&=nb*SpvSvrZy}chM-VtuOgOrfM!(l{#f)?P=Nye zs-(n@hHEz1?!T&vip1#G{(aBi{#?__wEI*6mG+&xTU7$y2wrv5qW^>=Q86s6!|H@} zJ0HR>zu@+3V44Nuw=E@)%wrp2TJ`wZL6UoF;@r}hw_7e;V{8K$9p_cxn zzzZ4+cD7a=J_5giFqsR@OKm;EiKS|H#|DvDApzFoLtzB-qJD`2LqQ>LP~090c(qFu z7zzrJ9}N!$C5D1Rsd(*Y3O*DR(oo>fAp`XPzYYcYe;Eq3qvCJ4wQRr(3y*{+elQZ6 z4)T$(oJK-DMgj_jl**l;tD7DVN{okMjE725$<{+cZYRsdFsN-%%9QAZ06&tAyq0QzGwgIYLX3lD6b zC50F1oe`S$w|q)*v?LZE=?4_qc={Fg9M+9+RuXbaD%mKO0q|d8ABRGl5?+xvSrS`j zUs|t#SY$ESZB*c(Iiv}tQ87xBqqoi0tm0gpL$>>D07L@Q7)X8u^)^v;L+-aJrNX&8 zoilpqWeL@}>Me+{Mm3w?xjQ z4@>c3H2lR}`a&j1Ul{O#pRUFqcx)_{gsec|nfCztS-V(Z<@*HcKmmU?LAT9Qk4gH2 zPJA|Z*#qP_J+N7FoSyyRzJOUKLs}85{XTg>E*tvU-K}!5Mg%NW5zCq@6e=jgDh@9l zwTxrw-TB=|bqeketgaE|*cUES1KX6W_Uecz$J+A1cM6>JLcP+!uKof?c7AufLID9v z1@9>mfqeA&l2<>Z{L8)9+yacm(d?uLfr`dATD`45Z5_*cJJ z7b_|bE0kyUDFson=nu6DnjBjwumIOT zhQv;`(1kOlQ~uk$I!;L*l{baYm?05vXsGxEKfku(nnwQY)e*es@3WqVDrB=ffQP$i z!}|O=G;I1mdkzbL`r&y=V*hPaeij7+^BBIEoV~WhiwWz+@fOHt zUI8ue`;&84&m}yl3)dLLNjupcmcv3NU6Bd~7L3m?0Q+TaHhP2;MFF5Bp&O*igFkgBTNK zXy!P2_)h{muDG@9|5r5qP(8uvM;Su~(;7Joe+@0Jl&OHN9~B4jIQK}1(yP(mRCw~S zUe^PYE%tDF6C^~1E4C)#n6Lq*UZ`Sl6Ihxo0|Me7K$ib7NA?2N3vll(Ww!4j^TW5U zCWL-@g)DNYgkbuMT(U3To<^>JehXypk~!S)Wzi;tSS1u9QHnbiSNR?y@2t8!4+{ss z`Jo?-)F%YRYom&i1II!~ zkSVDh)05Ye0*UsMwBXBR-Z8zN0)_IuP<8HrZQ_#2y<<>sxL$&@G8O0W^Y|q)dCeGw zSpDH+qu|G9`BczkHnx;AWT=OZG{^)te-;yOHBI39lhkj>gSsUFO7mmIUL zt#R##R@7llH}5bSoP?FwCw-$w!QlmZxJm5W_LoP0jEK97s`OkJ+~q?@Tt5~n#=PB6 z<43Bou3d-R2voz06)=g@My$f#BiCQmK~J{xF{4T0&OzoCw!(s>t2+y?zZ&O?6DsM^ zVDFQ0F#V9qO2D5%7}7l96rMi9;|TF_Ws!Y9Mw`@I2Z_3GA&yMespMP~8^il= zsViE6S}<4aH!ZdrAulzz0ugLTt7J0(7H)a|>>g1=7y;`ewtR7u#>Lt7n8c!(Vln%W zNAh)ktGNFTb&c9D6cOw76ygm9BvnOG(%>4amx}p>3G?NSiS$&>XI6@U5xC^47VduV zUMq#`U)J^sTCo#wuFc=FC&C&(2c4w5EC(6(R86KP z3xsJwHlcmGU?LUN28#JTSFBORqvAL9VUQXsbTSpGt`)X#!^MB$-~+TD5Mh{H^x&8H zpNDb~Yq+&9Kpw&!J-ie!5qI>-&#pCI(?D86{#s2u6S1B`8HDPn!gW_&qk>1;d6f0o z>)z$|(_B`DEo0|*6WEtto!t4|Z~ySB$nK733=~N_zxx+#_OKTG(`N{bQq%AC)vct`W7riJidLW#ZK`ua{=q3;2A)Q5xc!V_b0Y2 z?+Q`nyhA<2gS2jnP0V*n5o*Xk0`a5Gik&eeE|Ga$PWug8k6*S3v5H7v?aj;()l`G` z>q!HzlxGuzr}}bq2RuiR(nP!O@*~q_EjZK6mBctce%b03Cz0Yf+Q`JO2$X6WQe4e3 zrCfF`ms-WoUkP=cY^GO{LK^4h(ign_dx?cu<|J(e%3VS`B6dQyJ3|K(AAIw9>9gy| z9GI8j7=ShsQu_&?3_Rr7Z;gmv0!A93^pieGc`E+bwFg*d;awuHAYN zQ$&4FbjL(qR8bvJKl@^TUB4r4GF~h=pli?Bt^0N5P9jf@1D%es2XwwJ>u^t&+oc&s z?qq-RWloH_6o|YPoqL_KA0-gI^WEE_cTuozA-mRfXHk|LzXYicX`9G;ZAL)ElTEs< z-8+4 z;nW&TkynwN`xo|NysZ+lzU`68mm#jH^q8W$eRUa_pTdTfsN_{_N@n^<>sv`Xs!R(X zWS2oL@=)gpZ_5g$Quczk8Ue+q)eeYmqDN#?sD@G%4aMJVjVr?ZraR^x&^hC6siTT{ zt;5B%P^yK_8cUnql$32O1q&XCX5Xs<$J1&PIJ||Ko>ss3aMnncQ`sK=dAMLbdruXnaAcI@FOp! zsPh>RR$aiVBXbcD-I5G*biSTEd8kK|+{dT!;3MAYOB-s+yy=wG> zAp(8lsJvn|6l~mhV5Qm}$m@tcdk+Y5a!l1DWExGkM6dG=nLb2u#oeksC|j!+&n6qt z&Z^`fW*B-e`E0Y0wS#oW#fZAqN?>Q?U>W+aPv;22eCV$pk^}}t*OEJHBj)c)ZAuzV zOwHSE{m{x1i6sktVxFjKMAF$MksQYxyq_mM=F?}HAoa<>WWmJ8i77Cwjhv=TG3I5V zT3fD54uY~j%D4s_L}*$u`O$4`gLcc%kyv2J(90z8hk3~7lo{8clzbo|#5r&wSof%# zju?v|7YnB6S%{>d93`E&A?-)=ory;lJSUye{-)b0KG{>}U4t@ERqNxGdPA%@P+o^k zOUOFao@jfXlriaA#GQrwp}1^+)?40=Hs;$SB;98)qB6?t`#pyQbnO*CHlKv!NrT z#zTT%S*yKY14IsO01<@CzM-5;f&`m~DF8_Cx6p1q3ha{t%cRA;!iN!vJjJ&7f?BZI zFds$183la1p~CkqWNa@0pl6AG0Gvnb$%VrTG*SsVsQg_UQGlZl@=kK=ub zKa?wD*#(!Xn*3n-&VO-0#=&zr%4YAM@oRnXdGFU`CPM;Z0+`z4iIe3Q+^;R=^BC~s zy%oL79{#Qkw!FT62`u6)$A#i-U5&vPiQn zF`?oVlIXU?jys>8(P4Tzk)@Xw5zbJ^N~lW8w&CfHVQc^-BDZ)58%la#S2dPIbIEZ^ zLju93Y!^VCnuEF-wUA`Jl68xol=bVAy$-Ac?u@a-$(d+xxpC$=ewC`(NL+8$UnV;F zQ-EzAcT5Kvq?Sr6K(64TBT3dPVp*+|6Q=ORcLZTn*up$G$&-fUy z%GI-`NNNK`yf*VVB!B{WB`wQ(E@heYDc(O*2%nzM`A;;9{OO4udHRwMt&uGs+P)Gy zGL8wp{E4ha9Cvm#c7hET0wR#&s=tI$yaQPEMC&DaVU`QL(IZYEOv@$=I{^4n3JDdi zWSh|mn8ki<<8Hi>Z3U{}R5IiNa4mPvKP66*Hn$O&yQDYTb?O(_4Jn%E4-24xk}qs< zZ%R)zTTdps3r@(6HZ;n$J*(Z+h9fw(ZNYQ0Z&1>xZ@&nW9M{TwW=2QGbBX?za=E3! zx$I_Y)2b1$A5RdYKisBjk6n#QMzpet`YL`0t;E%j)7WHd0FD4yqGH4vU1Mu$qW#Z$ zg^KE?`!SjIgEb|{|q*;%SATnI9DuF=AExGFjMP8PSV2J1y;SIt@=l|(eLt?gJtRla*e<@6+wp= zDCJYJ6YRIp&AaJxmVvuRADPuaP9M(LowW_?Jbp#QXg}wFMQ{1}5zRAF=2fn&y0b{j z%qpx3Tg_r7oCcsKB*_-NzJOMa?SWAx+9p`MNJ~xv5$}zUm@NDD>UW{AO!~SQOJ9&Rv^|BO%-s=9ICE}1D{1;a6ffN}_FcOrH zwN$WNyl?Djv#wVGVFHeU1w?(u*$7SbkU_^?P02+?a6&-(qTVowv>d3K`aWiNLiCN_ zRduyHR||7ZeMs+ytmlU28)oc&^LJZ?^*eC?2R(Z;4J0XT0DK~llnnAx_A8E$h%?w| z6-_>tG+A~DtQ;JG49oCxnOq5NIizgQQi3#u zb)r5>*^f%5f@|lTHqil41ra^Fief)+0M5VgE1Aa^-cbQoEOX1Db%C zM9wcIuzMiEF>O1cEVPNC55ar2*|XOPk3l$wN-G`G+kYj?h(;~a(r{i4I}}?xxw<8w z&OY=Qa55xVSYHK|p4}NKqjj-nBVA8+Fx`@$_Grm)L;GN0yBug zpb}hl1=6!8gQb8|mKR4DKg}|wob#tEtz4r6RO=9eIi#@%fGR^@x2rh-MzO^Njn)l= zRCFWhSv*aJ_Y+lh5a^vC?N{oVgiy!C00w*R zSb{b@6V$1a_$-~Dwi|UpmWt7|k(Rf-lYOg55dbsY+*cnKb~FV4;+L@yI5S@{ekwak zmS?yJ1^th{|0|+cOS*psz0v065HA^sN89%caNOrKb{zYN_gFu{s-(WZKh~r8U4flu zG3;7d8Rn~zQ$*tW8;#vXzIM4O9dC+&_rz#Tj9449pKON&Zvq%eOYRJY+oC#_Y)HD_ zE8Y1F$(XVqflCeKlhiM?dZ^EWOUXt0p49f#OX+pS$+m(jr^;1uxAMBjWWEH+jXnOH zc=g&wM9Y@X^Rm1kEV5u{N8JGU;@M|)v1GZx&VI!~bG{7kp+EZsW?lgA0T#CyFm|@y zT+iSyV11Pr^a0GkB`~)rNRP!_f2m_UXlVt^vtpFiYih^5a|O~W_&kk8G2q_Ns>kYr zV-mkAouhJj02YytLfjX1TYNj~5RlcCf0cQ>7XRoGU%0hZTSb1v?8L$-Ux} z-4m&2YfO%Tv>T1EDl`FoB7hW=s7Hj@bpM8yy(n7M8P{|}TQ?Rw;fzZ$z)D5`Vm#)E zX|=r~6=uL@E4C@NC{VG5l~w9bhj^&rrFOF7}O5Md{^BjOl(`&K&~e)~-n6}eK( z{YTqL2tBNTwg8X43BVBG^oi*e$B9Zf1R_c4tNtv#b}D7)k;XD`ORP~6$!hL!dj zm=}rD_Q^6{xdKLiH?=(paRlsed0^cTWFE($trL=D4t2Y|pw6UAs44WCbe1wj^Kp3C z&l{&e(w-i9wVULCl8gfej1Vp|eMaL$eh;fF=R|VaK5&fL1{vU0R?hXLJ=1cpcMz8I z)r8R8o54{<>FOfy0H|9=AZOyRrVrM69v8ddvj9Z`xQ$iumUpBN79yU5^NmSk*gui+ zN-n~{2o6F`IpD;+FiWDVR1rfXht?+LRavz;AJ$Vo=ENfCwwuCou~0l99@GfPxx!jk z0rZ>`Ux0bCMUUR%8c26UUNhz3nW^-l9PmA^-azv07#ner4v1>Ln7y|Z7#sgvWLBskzr~- zX-x*vt#B`NrGG75>uPsC3MjDyx9$OviY;w>i3W9-nCY;#zZP_kQh5VWdt=sr#Vpb< zON4&TQcP_Dlu6jsO=C`s@csq(<)gA2Y3eRX2gqVZNdv7En6o(%!vwg=B(f7O5l3?k zd!v$QBO5ub09}s@iULgpp8UO)6u<|7E`iubhp`(8_mV2I&X9#lMc7SPAI>nh;$Iiq zxSS!>r05F7#jNMzxz=LRcff9GM@iRm{@LbCJ})qBiMSXU&igAm?I97Pf6 z8`U2RF3tqG9aNq9T;*yA;WMPQDFGDusFlJ_DeAI|0PT8FH)8DVaK>-0KMXO%+*vl9({z9p!fdZ?Og{Ae)SzO5=x^z&G!+^%99&{3BTv^ncb< zGq%;9g4c{$4yjdu!eNt3M3#dN^uCpN9W(Nv;38gW8Fw`fq|d!Ro5I_lgSvXVUuy-W zKD0o!-qWBRg90Sh6K`3L4&DL4O=86`$;>O(8P)8x%?) zHrY^%TQboB;z0o-`fPG#Lob&8Z#Yp=F_#hkL3%}F> zAC`8T%v~K%VzoXsVrGh38Z$qQ`+e<$aa%Y2BO8wWb>1!=i~W7=_IQ&{tR0R!tlty= zIzJ1Ml6iHWEt>s?3*Jv^@yS8a61#VR`Btlwj-(((6kj0h0Guy0>)`<*o5&vrl0!)u zRIrz1E}Q;dn1cvH_?wB07iA-i+b7V(xmuFhhscTDQ+3x`_zNgb8-s8AxbaKl-~jh> zm8Dcec!#HXqN5^1@2N9aMZbIFDx1+`tb zx6TyjNK?FfL`=^x&)VK%1DrEcQE012sg0`kQYMWTd{cSeT`t$Ircse)baLj~3_|Z~ zUN+hhSEd3G`YzovRa({#cqqzU23R&>4p|7Za`}MFZIf&tY4<`bGaZP{HGb%XGmBIn zEujxZ8^~RHtM)@kdY}ZntvN3$o9f3YickZIWg^3zW13~D1~uKSGk3H&9o!xU%lQMbP6kM=m)3ZeH2pOskhHw1@D9EGy42T&Bqpq)u3 zD~?qyZ$>0_nz~*3uH!@v^DP&_hHFHt9SzBPl*+{UeBdXhH_XdXm5A+UnW7KPvKe6w zU}R(ty6L-RmcS^_#rX7;o=umOo<&|UDxxHnM8MZ|(3zmEgGxiuPOO3<^4#&3vy&lM zD$trg!m`N#u1@~03CdenU{lFA{MM=kUtFfSu1E>n9bN_ThT^HC(Ze_S zsJnfp-ZkM@lCFaIQ}2}VbNGx+?dS55rAcE!uo&vKh6~J=d<<5{&-rS%d}T<{iLqxA zlZ*|>77mIhD!5JrcBITzcC&NZ+ZCZn8g&G*-%F6+AW87*D)W2%(@fvOgII+)Q2Nm$ z0z3%us}&w9v1_K5VkK$?r-!&|k(42etc5T*Y}#|y^vKeI=&GbFJLr?D15{(2E>dmv zI?jf=KrZQFI?@D<1(8H1?#YTJ(i4!kP2qkiMabBcH>RSF8VTZ^P=6Qkgx7L*fGdLD zOu7xT4A0O1m1Y~o;;5)j4strp@gQ&(7+XiH;HT_2fkM>=i`CMo_NT9^?>Gj_8)Ol< z4vsT({&K|V5XC}_wLA%j3J7_!Bcp$9kCGGw_;ldqK~YcAQO|P!sqU%*jd`1sHS>vA zL8nIXn^cVt{yNDMAcjv7o;)Gj2mQT#8*&WR54v8*`zG=$K^|+fwR>S>e$%#)$QluU z=f@FqQ^9i|C0iU|_tXCPY|7Luwj+tEk27eq5l5LjM7}a=rg*@Ka?5m@)-JC)3 zwOqRu!1|eZM&ut}3Y@4K+Nwy~^^Ss>Q+PZm!dR716ERI}sX0`a;4mo~sVav`)v$kq z0sJF?G1^R|u*Nb`LNp0XH*8E)XruQvPxyH}aatc5UdZLz$Xe;`67SS}L_)Jrm-PwW zHhngmmpXbe(;{(fvKl(N(os^hlc6M-nMpqo`LZ+SUc}C>6W^RT*EkwDwVt8g3~P zpl<}tofM;>=;aJgs~TJ&kJXCgyrZr*viS z0Bnc2WW*~Ys(N+^y=};M>9lA|R|_$Ob0ClG#%%uVbgnR>|B7XT&3&ZUAP%gk$euzh z{LDq==L(0Aca{!&4H?jb|TQ&#Ue^e-m=OVs_n=^Lq+}?tec+B z8_1bp-YB+2D#3>W!uy{e;e{SNo7UFU;4_!Akd(guKIIv#hcv|**$c(J?n-cp?LpM4 zWYgZb-#f(x|H4vrBj83lfauF=owwnVH5~w^>?Gud3=?>tm+|+oVWRy!tO8>d@(|GS zd7CU?@ghH3&I%0?-cPCQZ9dktl7t2@hoI2-db%C4E1&8c`VFWd|1oZB^Uo>TzwGAP zx2(jMR9}2l}g^iM;Bn{7^GTABM^N91X??e1nlXScmz^tOuw&&LU-0#skrzVg%4=nJh_G2u zrFS!!WdqZYH7;wpU)e55-YeP7=t&e3Rq1*#q9Sirp-(jb1j0+q0NRzyF9({2O#BFT zZtR@2zsQqyzHR#AAyd+}zE={?42brdv;J|DNcYzrsAjr^ila55gfo4w;QOZ%LKzHC z_DUSr`O+OYuZV}Y4GK!^&r}{hcL=c-b=n?8#UP=sdCE&tMvxM84Z0sVIW{1IrAqBLI66|Dxf+XQ?PD08Md^--AZNt}Zm+$@r~u9)6u=6Xg@ zP_a#FT)e2uWH<&517wdBS8d)|iZoVvi45f8Us^-pBTvFE^Vh8`$u^riA|dnp4J!lzNiwc7AGRHG=S zuEj1gl!4!Sg3y0s*~G3Qe}vXAMHgf%6PooM&1|uR)(|teTuFNM(5h1_7nDtiJ;jMU z^R+lkZOhh)#W73bg3c+U(^k>4 z1JS9_s#EQ9{UUWBB^y2e37%*H^e6@CEvr&zkh+esa>{reZA5QVT)Gh+F5-yLYNIh| zoZ3Ie>-OV)mRTqV^c)YG=_>G^9nBZO$yeEOWk8Tvj{1Zo`0jwAB~%bDsC=4$q+o(j zRjt(-=b?Pruxz9Q)SS?Y52=ccryK|5p50VKf~&= z1I4g+gmbR^Tq=@790R@q`4EWW;VjvM0V4_Lti4Qo63vmog|&@{W7$((+6nB#SI{=! z-0N%0#lR%caMNn@<~J(#WGFTsYYC?34@9g+CR+M7`=5I*7u$sEW@qlX;0yW~FeH}=Zv!;I)0(Ih3-k-}K@u>y*QoiP_y&RJ<)3e$>r;bVJf{u3S z4k7z)b2(pg8n~3}DrI^}NcF#s#a5*htEIbPuldFo(zpi#`V=GR43qE{={PJ0BDKDy zU?8sbW|2pxe9*8Z`Ug@HeHThVn9a;&B`J;qGnGG}mk6fg5tl_>ZK)TUGBx&2Vrlz( z4PdktsvAjJ>PDadvf-^#zj?Dzl-kD@h+V-P+%q>BJ9n1pN6j`whv<8%l4bd%utQ>6 zaz|upc7ZRq{&GjS4;{NR)RO;CegBBvUilcg6Ajlayzs<&uA>g3*^zI!DixMFsFS}@f>mpkIlac zw)#-ob@=}OW1FWlsy}EuL~#=u(`-g!^(Q2N!uD9wiG74(RESKd$T}nIgk@MSQID(&Cg%?%Awq^D^E}wI=q!!|NdumY zc$>v|WE!)@i23`%*Z9HN_tIz!?@ZAioPQmWgha$-g_ZVlH`dVawM6{a&R(cYWf(sP z!ZH)8VElVT68;@lxFy1Kk8_m;yBd%oh|cnM^aH~H8#_IZdm-_Njhcb`!+WHt1x0r) zRoG#vu2DRbVy_ss4U+sq8?rWd66wwW3X3XuEIf-`hI=E=Q`USLh>I_4--55qJsEoL zA6Ek}hUgA2d(Kn@;^MQK=w7@h)5X8ynXng2IH-tHC_NK>y;$vs;unoY z>_xfy6xf{5{5){tP~}WM`)~Si2VU$k;ke3KoHniATJ6SJ9k3d3>xV`ZhSihcv z^rcfgO)=_dof~h{1_rS(qFLR@onsc_W9)8Y`aO{xE#5hRg3IUaO2_u%@;D6_;Bmv z#HuVLk;O^ts1$|Xs(|;jcHKy(phZLsZ_Si@wnZ%%Tc?n{vx#BMtmrj%LW2zU7R!Lg z;?t{&Q#BP(rW8`_rb{hnGvsBVFuSHD0(?fYPQ)5Xv6dqWHUHc*?xcML9?Dk42Gy0_ zm%tx=IvLl*dJT`R<3#?ET4C!Eer95@y8t^?*LFW7p4qK+{m)~%V_B#!N2?yUzVAi7 z0xRy^k3P-h0%D*5N@LnHlIk444rTXD4W!4i8>qHRW$EIZkOP%S-C+$a1~QdL_g7g> z5re>hv54BLx3Myv89EZzo9bM9wlCdtpe+Q9nNx9h5ZpYvV;=0z^j*J)qN3K*Yn?z_ zXepCk0!thFZejiTb7Q(4t$Q5x6#dclnjwbEa*Qt-_2jLR#^(w*Yu^pRoey$ScNUD}m{m#z`Yf^)%3cNx=ca z_NcemNd@kZ-W2syZ$6MtU?hwHx&eyJhyq)Qw8ItiKm^%c3~av)zCUHv-Q!1eZ?jCt zKr!ki%ElC1m+j3--*_M?$%-0?N4LdJz~gDmrhN8W*4K&0hnd?7dtx!Wm92!m!@A0* z@_}ER1A!0X{Gs?H?YbE`XEcI$|%+ML{DN|MzAyJFFh`I}kyjzQM?yF|^f;YN{p{%DG}RVeAOWx*!xKBpqEd zY!;y4L+ymOKT2aT>HZ*(c*ADwR6S8$-v;DSmSJ*CBh8cxrJRD#k0H*Zxg<$ zI4vVt8f5^H6n1$iLw2HCKV*vJG5wN>OhHo-b7wLa_!IiEX1dsC2CN^5*LbSt0$1|_ zFv!o$Fk6wKtQ#E!7Dm|1+WjAs?WPDCwAlAwq){YSItrR!L>Bg8)Mw=SoDet%!e?%8 zSGiTl&K=MzuhpC8KJ0%!;rD|c@jly)QV6B{oPndI(B96O>e^xRckPA#V$eU9@cY0~ zk4aQQtV~n4Kv1@sI(`KOmJli&9&voOY;Os!(FPAy94riFpT)?@XXTB{5$^+5#rRjb zp(qx4>>X=;XkS38ED(SY!*@m0E)qRs6B$p%<6Bv(-X>y9On(=-0MC0xW}$p6r)4zE zKS1BfrW$67ua4=d7lkz58?9zf&YTrhaWB30k11 zcEL8@?|HoRj3%SN($!ca#P~WcFw@GWt`HY7da?&u3uOSS9wPK~<%lzLqN|3ISL4Z6h<@=?pm&MoDRoRagOHDW}BfO zXwN6gOu$zmj!tYG#vZH+g&c_n3@SxmQZBWEQx+Klj&flQ5}$ZsCG77#R(VFgc&fpN z`MfYt!9bW~{4-WTgSV8fRs=bxJb^9avpm#)qQg1=K)?kw(#cwR21Rs%BO$L4XuvmX zFPY21q``5fO(E42>vzEinkO1sFc)uKNm?)dFRny)C$;gP@cajm{VBG58D%?oCM9ai zk6JpYR)u)kKjIlAQb3nANNSea(|Iehpb<*KIah@*ttq6!bn^T%xW%e(MC z!FZUG9I`xK!x^K{CiFS4d_e>OqF}vUA*@x32^o*ukG2L)a5Rx9_+RdLs1TED7&~FC#Ir64E)%T;? z3`al--DE4yR=~1F%k+q>52UM3ciyNMtQ2A8nMoce7BCu~SV!o~W&C=m*n`i(pN7Zl zY+@3quye%J_Eiy=B?iS7PwKJO1O6P%6YC3;gQa-y=%YiluK-^@0}<_oaz|?~B79VT z844V21%OUBGcnhhDP~I!5No)@ z8|=s8^Tw|iU`LtVh*Il9zeU0+OpiNaB-cwdSHFtt!g}x*l+$CyL>4kLa+5I+)+qlr z>8b|t2*aMD-c(6uY zJjh#2TW^U_4mas2w!B58J~pd<+2cg(UdUD>qm1j{8f_Sn{mPoxDhT7UO1vB-i3$C@ ziEO64T8Z2{6w8Tldy&S&=<>_<_^#5p!1;}RfSgixPo+f<}XDTR3iU89a?2QN;y+X5If= z*7e0Sb%pVB;oiHp7PJjaxFuUlO07leWtLF7#M~+^ZBwZ9R)?`IlmZ(l%XnF+Sr(g^ zJ+N#Ku1nbF18$KAH9i=V?O_k_q$IvbHL&bK$rh7s2?^0emnF-7-?;@J_EKKTy*>Ay z?>p!G{(K)C81=U0$S01KnI*A4$g6Ru9zh{YReOcxxU^LlmH)!Y{McR{c!tOg^7*^c zxY}P5f6&0Dcj0s;UFLZHR+H0Nollc)Qo3wJ>YJIIhFOc~mG4K^+wMgo{Fx)l>3te* zGaH^*EDe=MhNF%sXlFeEJ+EE)#}_!yu!}@&(sjkh%HI^H`+f)B|CDE)u=*L-dzh3@ zp@k-g2lPGcYR@lT9zDVfQp^I0fWE`{-`$Y#de6DO2s z>T33LppACNiaB^#7eAl|DBwWpJ(g7$923XcsC^rl@f^-J7vw1cEGte>%=xSDxv@^1 z@|eC4@;dj^>v8m>;qDq_#z{KC>hXv0*dU(3lVl^&LzKJ0dIP8III~C@Q$j(|HnaW} zRxOOu5>5i0+RNg2j@&( zqi85SL#)$qKSGj$Pl^A57g7v|{6~FmB}2u?x8eDvd~=tzPyNd}i(keF|1nNUe(NT6 z2UXkKuXs(-Mc-C~O}AvRVmTI|_G=;DAg4+F|5I(3B~R>)!v~%90aa8&f(#@p{B ztpX71nG{1SKA<|ebW3kd7XYN}eRP$8{@JW(Q&*Kgm>ac&S95{1rerV!h^Le%u)}KG zq+I!~lE<6jIsrt;n8y;0Ityz&_m&*3oeXC<4;*2Otb{0r7nhn2gHLYg1D;C`y{k{?sv#&N2Bwbsnuustop0#-f-^4RQU$B$7?oW1Kf~y&DwuTk zVvsPL@I}f&&ivQ(3rm&y)5Tuzx)4GUK&YuzuD!xu6+txKy0z zG5b(PM!yl!w|U!bFBq{ zC+<%Sd{|g2R^a+hLWLx!{O5*9IU~CFaw2CZnVTcMz~@;Bv#2$cz}@4+N4zpHv6G}r zly}G%V=9jvm3EMCcl1jiZ8EArSeHWg^w;}LX^pf-cARTi@2=y3JD-rfi7y#gac<1U z4cG6uf}Jp-^znDLhp=wp&M&EOb8>U?;j24z4PiMlW~?`&ecH)=Lan2^w(IKub8Z;F z*l49vp_WIs>g(}~H7`bH!$ppOZn_V$)^+1syNV#<#fForW$+*xQp4Ysi-Caw&Oabn zA7?gbFIA9d_3k+Nd|kIA$mg}1%w-9DMsw3Fxx#LuYYRGrgytNpR!{o5l1PEi$a^v! z{ZD56@S}gz^WMSAk)KaT^DdRQy#u>x>0T7ak7J+Se4DxjwdAO82QOG%H*Iq?Lk)oI zpUt87{P6s~?1K=U!Z5nTchvOL?(fpfH^h8%B7vww&l9A%Pj&4mYFjLVL{FJ0`GwiL2gxtzZWRrNS!8@>m*;ecXbO~`+O_Q zb%kH0#C2F|MqMVEbuQ%Tx&t-`jd%XR*j%5|w&KjT4yQ4g$a zHAT+H#P1Z?4BV5R&qGdhvoZ$-_UANJCk3W&B@MjjUe$U~>W8&7Ro{YSpags8x@>A; zkF+-IwVHNm>A9m&Sbr_dIkhmeTqo_Eu)bVJ{!3=ds!s|gtoQG16v!`e-0C(+XRnfW zp4BPl=BC|)sjGJiq@Bp~=k@(+O&xgqo$ofk(BEBp?yx|8cBer0yukIaKr+Vt*q}h- zn~Hrv+2|%z_iEKAzmlqBn!@BM=ksg^?uR*OW_T-;%XJR1Cj}V;_D7Gln$0cTeX4u| zOs(nFzE&kqE!V|&g0%!V>}9o~SqU_LB_ci=sE9~Q$0uoOOmPhdH$fR(Ts zs-PNbU<1^`X4nGTp$>MzXRrtMLJNEa2jFY?C$z#LI1ERi1CGNfI18O{5xU?qT!m}! z1N1;I+<|-WBRqgd|A`_B{*$udA|7BjV8nh90m#GljyM6*Fzg^2x@f)MQ{4~&Z;3?RS(jLWf>DC9LTuI7-N zel@NKewb(`B7e&@`enZHRS=0m-rsE852P9vJp-{6TeC5da5#^_WGrdDLklJa9P&;u zc^|^^P@)wkwSWR^1~6#>*Dx@{#5ttF3)v1PePBW8#F+d_s%KI#(wHOImpIaqatVU~ zfRQ0w&V?Hzvng2^+{c_ZN%d!#SWQX8AZR9x+>PA=c_WNGicG+US7GFLq^KQno?{7K z=SMklbRH}n^$NL9QNk$S@^I2DjEVv1mVShZEDBjo!Nfv9wezZhQJ)dykPll%9p|ds z!>9*bB50A=oK#LxfOs^Ks@G)@vs_WfSaB*T@;VCQ>6|p+2k`=wnD{M25U=ENz5?+c zE|EDxd<5kq{Qy9G83$MDg9+>;cbVD~Id26p9mgU2IGau$E?U6x>;a~g^M4nY7wsOQNB{`D!z-%>1c&Wf_3s>tVm^Hy_Dde6gFgrzxf_?+DK9IIbu}@|{lN@^h zb2AS4Q!sZ0$b)0RHxK7>7A!U|;KmDaUITtPciI9z8Vb0-%p0m)8atc`Zk)mDoU=hO=G4U1?xklr_;&rZxJz((( z>EI{;i=9Ny2M567pB&|@V9^OH^Pu?)uoxZ-O95B?Bv{&UJg0)?XwrlifaOFkF&!+Y z)5KB&z;YfX#_H3+@&gfyIw)bJ#__i zIi(GW0xLTZhQSg5D-Yn6!#i(+RT#&{M_@IP^x_>XD&;I!vuyF-*HgP!n!Rj-v z@BjSPs)H1TtOu(euIM&c{mdl}&9xpW!q#`?fVC$vPYHmvzKPAz)>9~Szo9>Z^_!#@ z-lbr@2J$)}#SN@?5}v<=)z&A8=m1`~zD|Vciq)TzeubO@8*8o!lOr~A&YG@+jUS+@ zW2sFP$4?d5Od++bFlQErd<<+VxWs(0spb0h25Y0&XSJ2h8B)tj2sS^7vDOD;z?RRA z#VcSdqi;nL>62awsHb*jvOH2urlo8AN@&k~p zWT3((YISvqp?SiD6YhpA={s1wE8{u6$bls@50CoW!!G&NK%O#S) zZYpvLULS)U`f(KRKfvx25SHWQ2X`tu0{x2A6mU_MZaIta#b$IGj;a?b`{5UiZNM z3CRz68ytjO(OVlFY)A)DL=H+U;SFMfB{+CFA8|PQ zE;_E|5~sg&JV1)PE`eh^5es@y$6FlbTj2Ny*PT`1WXd650VmYeFzIOkr&tseoWaDa zLqc$>AdPyZfm0)wCvM2k3h2Q@)4>^~fv)MUlk?|X^_2$B|3IpQvh(*`PB}RL4A>Z6 zGJO-vL8hPI*MSUY;#TQFEJCwFYdQE%?YU}O09DR;YdWwBdhcd3pZQI>xQ!l|pqNedJT zsr2Mdolo!|s{I?7IyKN~r?V;fj#Fcs)DPGr>^I`qL10#tJ5Wkv_^W=WF$-b_UaJrK$@E;jD1`I+gdw{&IE-^5Hzy7kRJm5Zajsg+Fan?MU$mrrs(?0oSk{z0T$ z%tpa+mAj>5YshD&rdNSNUEIvp&<<6ZknXFy~Ww zQ_QlUb?RSKXZnGr!9_O9J#zmi#-U_2dYnL^TS z(z9Jxrv4>nS<;#QR^@bSrgk!?oAyWN{e4_iI4p~Lnc5=YI|jP*zb@u#sm>h0N{XFP zTvO(A)xrtorWV)KOzjaclAd)Rk-M%i2E0=G5|;54uPS~JJQ`1r9}&JRVwXy{Lzz?L zjqyJ1=qdxk$50QF&Ky61L`fGm;z|i!ilrR&s9y4@Ji_rUhX+jUTA(wkA}KdT&0v{k zd+ZHyy>iNIMpuh#h5p`7pTN85v}GtL+P7bk=UCOMYYNT|85vxVwDq$s$}9>gD|WbI z5aPCD9LD&58TC0a)G9k0s?1JI&DJPfC|~c%6SnJo!s5lBE2mU*!aV2!0=Mngz)*Bi z@-(Vdwn5SFLM2!@E!!+WYfQR1ctiQ_Z2+HyLJWSGUA4v`Gr~TIov7GV>jB`ZSSkm~ z1>hCJmFR=lsv}I!>o(hy(R9eQ53S--%5pbmgNLyvv0c8ocOu%&=Ae%e#cJUi4l0Y( zIOOpke_jkcwWbvz%KOFwG$QYWP4JOsU4z^y z(;}1Eyd1--E>y4a-s(_qpqT9eZrj6PL`HQ{pV1bV74)t2kR~+$yVYP`JQX3pb^D!x(6Ut5uv(fOL`h59^ z;gRS}`Q~O4pYjN1>t6jFzzaYH^a+m>KWB84K#<+(x|;Xl_~=`@M-f!)AK}K~?jV|~ z$**^@&0RC5ReChA1YOhk;NV}AzCC=F+4ur@qdJa8m5px;>QW7Uuc*m&t0^ZsG+8qR z?aAq`a>WVk4A7%2b2^=~x^+#Op7>uPvQ_tbl=JohKkZT;I`y)+ER9X+o1eZhSE*PE z20kYx^T&Z}1!bXe{T^Gp6*ncR_xd&EYey6{VeYt!;N=|MROu7-h3bRF=$C-!_&~SJ zCwObp)4?ao75D$e@tWHmmps zY*R|$!<=JHYNtW_&jQw)5huOwJL04Tx$6s4;a%tY9(mdLxp8xo2JhZc&P&6=R8&9p zKrg~KYe~UL!eN2M0%nWBUwsqt5$cJBncX_{P2tPNk3iq_8Tux46SSMI68OgF6|)ZX zK8jov^+W8q(9=o1-9ISb;RD~`!_;x@Sw6uf;^$-%HPm}Qr&jaXp34);%@n$2Ozm@^ z%{0!t6T-0RgZ2T;W()YcKG0L5=Gjc^L@SB;G3IC0nbXShIuEpe-l(tjruz61y72l$ zSKq#9qHDQK?a25YXRm`m+&2MCQ;Yge899Dx_p@Xjvqb@XnO~uH<4K>ORpMr4Y8UW{ zt7%PgWE}3=Pi8jw-HZIJ{8deO`Kx~)L5cax(G%uR1dka;F7}dQ6i9qpd_q)6^bysu z4$?*5l&C?RrLA$eYMHvxiaG_SD>y&PA4Oe?8Wa3e)H!8-Ayel90{M~HN|(R%?CP1J zp7AqWox?gVmqli2UKf9;oPmDGm`tb!@~CWAp)lU)&8F0m7!XB7G21QCL1_x4dWn8i zXrIk&P1akL;h&QWPDiGm81)lSco9eRhv!e3F ze<)YtP?!pv)XMxR%w|+u=iTwGg_`fym&IPuw5paaQ@TC&z}bn<<_4@n1B!46AFq5h z5zQa?F%u=y_G3N~jp9wpSC0anp2ErczM{!n_ln zA(SFxol#vU25M!hRV$U#x`2-Ea#T>(&-ORPMTCrHKNMe5PJG{O1IBxL(&n=pmN@PU z*wpL0=|^T$4RC{;fl-scrcUsPy~|$Jtku*ivy9PQj?9rnc9lmsxQ@|IV6+a#Lg4vh zv{%MHVV^7AUirJ?*qhrPxB1ib(Z~wM;v}6iUi&;y`#eDVT$$a&Xj^cHj+b}(NOe;* z8}>=iY^4VY-4ya=FxsqdRws70!Tz?jx7BT?T1-$)u_v1k{etd#Vc#bJ%B+uxxBjIX zm#RSLL@V~&nqrt5H3y}i+U0@E7oj|PoUky=ca@8{p}j1^DsGBu$!+vo-oPgc$~QBb zP;yxJ_U7^Hf168wm>QumP9CE5|Kf2~htDdPFJLx4^x)t5_Xb{&))98nostVrg*;Ms zk9Uik!j`LQ)+=*oc!P+kOC^m_n`+CVN0VT2pE7-f2j&P(D^p^@!~9uJM~kkE%}K}= zuU1a$VRnXqAYGFyO^6P6-J)(%8)z6btU3~H=oWK)%T}jd>el&;f2wqM!(Mnh_WjWA zWMW~$Y$`)Hd)`(79%s*zwEzYmGe%+{g_dB`j|+x~bnyW)<8C|Tw(S@$A}(RW@!i3` z${7Pddv(6jI_pAvXH#rgyd}2Ss7%_!g*ZAg-_oXUd5V(I)i@41WHak3|32V0e|ieq ze7QV~cscz-;7jz!%8XQQDl!%3*5oIbM>sh;UU4{MzuE4BI_$HR1y)=?92Es4adj0uj-5Xo&HKc)ak2bd5ExiE`X!W4JKC=HiEUKX z@AANn{=apdEIV>Fk+iBym+I8-aKZkH@~vWK%_uL7TLh-AxnLeh^w!Sbw%_IgQ-$-9 zS!}}YY{JG$`-k(&QInN#Uq(()nP~%?qDptso&Kh{#~~V=!k%Ja=!)yD4tFw^pS?4x zWPO(PKILoo^dn@H-9II(JmTF$OKZ+po4-K-Q}cy!%e}aWiQ15{6SX7=<#C=bMv0!! zZ9kcnFjOy)3S7x-XcqV+vMsm@_+?qcgZc2M;mNW|KUs29rQ2*U;G(Q2b~ZfMM8);O zp>a`t+km4oyrg_J^;ELcXnV_m%4jo7X7!z!vBlHzxq$tpwu$=4?4UBK#~U;lhPo#8 zU?hsn^o>)v*dORGi@G0wS^SkUEd#}t>O#429@J^jabZApiRB&X zP|VqtPk+6%@NK30Q{X3LPu+1bvrPTF+EVrLM{YY)v6F!XA!v0glxbg)Q5Kk1yH7I} z&dNNgv$M4EXDZhjU%nS}edUyxu9Z_znQ&!p9$2fkKW+Vn)fQ%bKG`^k`@6l{{%P;R z$(ls(S6cs znLgpyQI~P}k_N|lOY#a_PszEkt@PxHGsiK!@dg2BC7GES3K%#qC=1RGE)Z{4WTL^J;8REA|_wxdwXnU`&jrbFjg`;OyY+zZ}hvoc`qcxKH&JUR^hUP8f` z12db#24b_vpHZDyr%cz7sjzMe7#dBBJ~<9dtBb?T`qPuuWXmaP^>{PYJ3lHXwkmdw zcH5Nzq9HXOD;YP>{y$K41ATDochN&y*12ONIuF(FSCyrnz>8gXBdaWOZ{z{h@4qQm zzKI&_mwp5%?OKPGoNq6Uueq!OLEgpm-8o?|^+$E))KG9NuV+d8U#Ew?-u zD0WQdOl$_w;_s$+1*KptnrpANywB7s!N6?p6b62FcrRv?8HTQ5- zc&Z+Z1%>V(7>v2;aP3Lf%e*y;65NSa?-5s^e&Xaa#&*%y5$_!8}<9LQj11f;712SNd%7%ivz07vpi_ zWzI_@*1a4V=nThznI?P zl$RPBKf4b2Xv%B7KF@1~S30xVYuhjekwswSn1ONjPFF?0>zj|Lg7l z`VjoLF);7Hjq-m%_}uO}4g{F$+cmK3k9tAv6jp^H0^EufGIN_;A%8 z?7$Xm@T?S#&rHi81DY^pf~gf;stI$-utbEoMc@D@u~rz?;$dnP{ukka(IU(_ia9*o z;R>U?b}xi!yFUQm-6RU**Pq(GcK62Jf9!s-N3h3ekI5c$?$2_M^&VS1y!sfAoyNg_ zJhEyIH}Qb0E%GBlegu0C?zyq&aU<1e1$5*3#?KnRZoIVnRT$h|2=+}hG~oFlM}!MG zD1zHfMqq{Ijzi@ahRSOR#Jqkib7~p^;wEFT1jDBGrn^m#nxWaa*`e95IjT7mjGMn` z-q(Dv`5(>SG#_d{+xhuE2T;g zWw`Qfjc>9d;iS{Y;S>UtS zXQPkK=ZMb_K7+oLuaU30uf4C_H^4X4H_12Ox5W2t-xlA0_#W{+<9o^XhHt;`Z=;Pz zJB$t(9XdLCbn@tu(XWqQIQrAkn?~1-ZXCUT^zqSG{fzw_{9OH%e$jr(ev|wP{AT(s z@mua!?KkL8`J4HV_7C;eg9?tmHr$3KllIA{~P}f|8xFV{CoTd0t5k8 z0d4`RfN=rg0dWD@0fhmx12zZj3}^~C6mTNoLcq0v2V)GzNXPh$i5-(VCVR}RF>j9f zaLmdv=f+$a(?emj>WJr|V-a%Kn-mY8qozHS4ClPVr+&vG6-=T?Fis%BL}DkIeX9U7hueS=h-!^AW!M&h z&eS8)2z)}l;NYj!uN;ixn!+P$)N}4$P9a6S6i4?7srVFL9%R}FqhqbZ9q#-D{H1g2Ai+u;lE;IAA@~OeQB*dF zunDwSwVbkQ^9x0iEALI@9+?~e_B*2UzQOv*tecTnM2k1tI62`-*D~EE1 zA(-;8%TBfrk2_(B6AreSfXENe1ZEG9E)Q_bR4^H($U%<8u-agjbeN%hK327|fB4uN z8M8v2@kZ2P$Z;}?9+IQo5bHL)--opG4-UbWkBUPS*lI{I;;5gwf;ghJruY~G#m1@1 zl%bwX8E)N+B=mwBDjJEy$*G&=PzDqdIdDSeh!SCQMJgD}qaJg4i9;Hi$V1i~?NyJL z3=#QYn1~2P_P`OOP=+}iM8iRKI0TDvNQWRA$e|$vIRuuR=+J}+?~aF5@ACl9Cn6js zSvlN^43cm}M%*amL*EF8#Dh16k!dnyL`$tWT$I(n%^1R!4|Q`Kf2en%L-K!p2;ueN zhKq-(vm}RzQNZzaP#vIE zG2G4wF2@!gP9)ru$__)79rPI@n8Rft6mkv-U^>Ahs>vybAH2#TApBPq)l&c{9cpbD zk|VGB%ZBJG*sBgHdB5o)*Gm;BhcQ+SCp6TM@a7KZ81m6Qkx?3tHmZkus^f>n899u) z!gyHnyq~#VrgQBKKrn^A7Oyl4eJft6oChTN#4F=#1f1dzKoA-P(b(i6%}qv6%H4BV zB!4K8JKX67oO=eCk3Q+|ewv5&&G~CmW$BP_!lbocEX1aVriNiT;iSx66G#T>DbCc~ zckZ1wtaffTaHC!~D0b_=9_}D{a zLcBDBjJM-baX2w&;w_dT3I}Mnu=>;$K;!!Gvi zp}OEGdO4*J&u4$mV9yJ%<9x*c*9KTLAza2GI4b99H^eXG>&QO!h>$}eH$u%rmxV45 z-Kbd}stZdA%VC4qiEL=3AzQ}QvFE~l!^egfMZFdNPWYnm=7_c7e?&_ncoD`CZ$+$# zxEawG@i5{^rNYGr0ERB2~`9|b|$XbnYtPeikc%*(k`d9UQ_4c4fb-ntA`WKCX zrY_D_vq&Oa_NE__H{{~thO`B$Xi$>c8h z>RwRd|-|Leyc7YxvJTdCEzvBM;Y0cNe`H^_kFjk_!i*6Xs{cnb5ffwLG z(9iHwoDLoaMZ%-7)$llIBK)i_g@M4U@QeBc{2H_no`ii1gX#)+8tMnn!gs)Lao6B? z^48G}1=w~~ z>F`dX=y*VY57zqyB&4C=KvIlg5G1oZ&XKZtk22Y{Aen#QitJsGd~vj$y$q5oHC(;h z=X(hXK0FpZ0?AX3rk%J-$DRi}WJ&Zh*o|9n!d@Ui%pM23*&F<@g5A02f-$FomO8y;i!b1lvzKW97 zLRxp4h@<&4g1$<;lQw*}gn*+5N(jK?sRW!I0u7`+X(z{^v{QFnR)cpt2-niix^@Ea zOfd(3XlH)}=_P$VeDsK7m0sSW!%XMQ+Dt@o-f=`AYeR5G&hABUL(Sera8pgn@Te#O zyHB>umV$H3aWag~2T+jbz`1KHX;0>gI`IX_6g7p|KLow9R*-pj^s);<=7+>SLeQ&| zHG?d0BVkGQ;iD+oO^_`=%-t{q^U%w0$#pOH>&JJ+tLQVb^rOGm(^`Y84=0n^z<2rgV^kAeIuisvVg zKR8X=bFHY-ZM{ahR&G$R>w!G1B$+kXzb)Xp@g(UHIrKp?ADkAj)d=pZQ43rLF6h{L za7#X{lN|-OtlRCf<=~DY@Eve;M@8X4Z>H>La7PnG$$OZb)v+5u5p>W*_8usvw{)`K zfnx6UG}#ZJm|x$^c7Uh*7i0iDcO2`)PJ8aG-%mdV&)ukXAAo1ewUOAY=l%mTkuxuB zNK1X^h0cTS1+UI+?XpG$w=U7&@UD{)qh3*0L(S)N*j2) z-V`98-mbm%vTfk)jgnZ7FSl=y)7|79_g+%w_o!3$0=xsy_tQP#z3RwD)JX5DjYL1a ztNL~jP>qiBF@oA-vV{n?>rm^ws~c=(Z-RF}j!`RuhJLyayazUIo?70H9}>`s2Jx;( zA=`oAb}xMuyq}}l-vVVcI@KMZ#ABrNHBhEtr|;saPEK1@C@lIpsIJtt%bEb6!0Xu0 z2^c^od{XzT2_P!`BlsLSBbQwVAEF`a!Pj-maV+z7JvN2|JpwKDxvyW3qf86Fq<_DI zFZw|gi0|BEgo^JHr1AuO(HghjrhNN05}Lk!Te3)5Uylg~?sJ&^8;saP;M>1JE+e46 zQ1%&+vq=Qy*-qWocc{^tYuI4kaM-9ET)Mt;h%hD37#YL&*VHG`Pc$|5B%GaYs@@`U3mun=k;Yo zX^%qyb_M%D&gfzFfYtre3DCRyfHjEn90F=i$uSeZu%MIm!kE)D^`mhG7NQUN%`$L$&3yExfzuC^ zkg^%*F8+YPIsN@eEO1`^Q{*#n-hKtaoY(gZi3QF-lfr?sT=lo>=VN<;11EK|PasHu z!~Yb5 z;r+FRco#`}^f^QxttWktJT|B!;QR#+;82;HLu6+!X#u}(p_bJ{<9cFguIpNReyK)7^(i<-4pEB zpeBZX5!9Fa6tee$yv=}Rnvbv#m7u9?)XCle4GtCZsac0(+XI@7^}XyFh>E(_fyx(^ zx>tY|qB7e$WeXts_R%;~vdbV^KSf4&HY;R1@eb#zwGiF4Ng=xg(I_5* ziHgDjF1m=myroWmSC1e@&6UL@V;{bTnA9(b^~X%Ck;^tg?1xCK7GhU5*s@O{wz8oe z%VH~g$fOuseT!hmuH#xD=bgx1O)qIEb^~&D3*yki*)30+-yl>F)cfAIL-4h^1SYKKEFck4@Gj{r>h29ldUSrKY!{x?+wUzq3CSTnM(k&h zT!>VjK=KUqZUYEVB z9^WJ2Tn&kZ(=OKc5^$-TLm{TU57LP9XhYCKFw-@t^9K-Im&@u17)vr!Z;=Y=nV(0? z7D0Me9~qoDX*Rc;E zqYr6bf{gwi0lObE9&AaLZAWm*h+qzEiA67*i8C|%8)OoD{{}Ksw*|5fAv5zkg={l| zo>-iZGP7EASUr>U=ow_`5c~?0HJk(|Pe7&p6|(EM6HO*B6%lY6S8w|=&NQw2J(g!LFs6lD0>1q$99kb$iZW#I8?bGZf?gr@?s1D zX!ZoG!cOmm+%=rG=AzkeeMXTN_ek0L-c;Eg$VGoGYk?_I*oS=x26bCcQ&Up$?g>oE zLX+7Cc_<0r$M$(AI4XH38bV|%fxKLSRPxZf5peP1ZrLfwyL_NfwixorgxLvs*OAI2 z$j6s6*(S)h-g%y_LU3R|tAl(sT8=H`YfvEnL~zZ7Jqr2J*Cxs?AVANKAQr3tf&f(t zLGov$q5M>oXEWqyUQ8u-I8?3F^86ibYW5`L?>-(Yt3+^%%>DT-_vEC#19-O_@=vzt z&_U#%tDi4>3-UV;+RElb{$*@oAxs_FLB?q6$O~iwn@aTa7)aA0=OJS-X zr+oM=BDoAvG`Rd2KyY^nGBGvwh@7p3scGM~%YK2WMQv)5Is5ZMlDTTXT!vresop}& z7eCHJ1c{*D_mM4vugz_Md<}?{?M5mO5TLzpgQ;I_XlEN>>Xn*)*_Q}*O<~`K0#}S6 zK0&}m>3B%{4%Q=Qwe{e44jk^#17fKKuE&YY$f3_xstWW#M0(7#s5ujY2Am|bh6u(pItxu_fG3$wADHu~TQ+6D|&hy;e z0VDQ9ygM+HySr;kKp;xu85E?~_p)CRaGQY4?;en5m!KBdK*7=@emL6~EXQ&97Qv|{ ztQHEYuE|ODs%wO6a-N+4G7W+@PyJw3m6&^=@xd27Twde&Dt=>qe6ydalm_;?{KledVAGanc>fa#2_KF^$c=kfk zz{P*!OZ`R9P7{`1jYlfK0XbGn?#QZjIZQ_zv-#b+L_cYi6y0`32T@X7-;0ZL1V0ln zV}mWpoOOOB0kgLf`zt9sCuV)WRdSWw-w(DdaFarZD?8D3}N99;y zR`OBeg=Zz-pD6nPW@T<#$KtcT%o`WjJ3wC1!Lr$_w4JiAp%kh3K6EH0f#A1LdK&HM zK9rIHSO=wK0G=S=Jannv36$!i(bBF{ow9Zy9~D4irNkF3fl^#5$j(CnQ`E`M z5kTzkwQ5c$UaLlBI0kS1@S+{3rFkd<`bU_D`!6_R^NB81!Tc=De1P9FJGDgC0}Bxa z>%{H%ra=J(ZB4gBDKCABlqBN+ z?Qej4e6vsZ*9|)?;U!WOe=RSK;!zp6hmDU^gm3Wp1fQNGqy>pW3T`wAX}GaLND0>P z^Z7ir2_Nt9Y;%JE;AV9S1a%uII+q0^4j%oa$jvO*4(Q3;B@p&x`jMYiBOQA(J4ZV7 zWF8%9-;?P((#a%}GFj_xGTPllJlaGHv0`3FH-1F|4$fXH?hcK30^OmWUQqF33$VpO zVS8!D1Ha26*hdbhn_QJVNX>%IKuwfju3_>zdY|GK#r!-u`EFaDoX+QYv#iA{h4u5O zF-4QB>?&HR;zB>=%BW%GGFW^tnhkIvSfX_bkaa zN>pTzyw%JW2T$Hhvq~d(dq2`x|AI;vifU1Z38^2k&@zmzdm6S^BrBYn7Dk(-C^(wI z>8rQFxxMDjGxz23MWglkdJ+1O7q6d-l1rEk!e3sW+sr3-B=?HEsQ=?L3%CH?g0smo?%VuRQVR~(&j z?iW*~Mx9BfPVZR6v-t6v3!e2aA1YELj68&NEH5U8`1x(uyvGa=+n|!QYPL9i77j|0h7B|FPE_P%6?Uqu-H)LHeoZ|q zkkWsxEa=b?-u&P-`j(^^)Q;=@GeKb|=6^ca%tt##yJa)lCbD&H>3hwQD?WaQFi)71 zYHbTaYnNbAG(*kCk1;XSSQ8$Y1c1q6$X`1;qnXnh;hENMrTY~geisk1n?I1(uWg1v zS*0?|!`Xhn(IRUj_rIc8ExDz`vrc$vt4?W)4%bC#nbta{QA>3uE)wz*7a2gehu@~uVBZg5apJ!ikr)-zKiWoKqMqj7=%&WQ8ym!djXI?J3a#dLQnUh2{nW*OOm&dQn6OkwC-L1U?++0{V`%G*;)A8{*549v;Q zlN;c{t-yJ9-Zqcig&FmQm)$8lMJ#@?1D;ct^G3LLfrt9!7-##+Bx7tT1Dyi@RU<{7 zQ!C9+@^HCMZ!sw;r6x0$i?UKc{b|R+V_f@0(`oDUE9#S&_t(pMbQoOn1jr zHSQ%AC!)Xcl^ErjC)uRpD~WSmCBb%=c!9Yi6Uv48lxwcJrwy+}a(j>)<+p}L+1yow z<$~PQ%=~08%1(%O7(Tr<|71KRKcAi|?^Ky7=1ZiqRgo9-ImW|rSZujr$-`k`i?b5< znpH-%3i!@wD8#^2Xbl&p@6$+yZ61E$XLut7^@=SHn7CJbrD#c+aKdc+b3ta{YD__A z!Lsk0ajY4usoJ{Jv2_ly-J@1D?AiqT_L@t^jg*Jc--1*!uMB+|Tm1eMHp4b-ulc9* zcoy!V_yVXz1+ynqN1x->r!?*Sf-TNV*lXHSYMp|w3~F150~^vA7Om4xUI6kFOh?H$ zeyTbvj~btgD)2>4f?{4E@yy6WSRN{G$_!h+rN>BRtBqk`|0~{xH$19H)1j$*joxSK zHClnKL1A>W6( z3+aj%U3d~xe`)1MOytm{eY@k%H%S#BxbqFx0}4s<@+qEXjadUtLK7|ACNYSXkB{#h z()k!~Y@jHJ;8SN4@gzVu1}<7?H=(ICN=Q|_I7+-C1_!>Feg?+FZT%}W!PA}sFcDGR zp$zr5_U%RCbH+MTb7nj9wA%8>ZsoWWS|t0Mi4n+Y3L7?`GnNJx!b2b5Xc$XtO^hh5 z(Ig}yr+HW^o-_Mp>1-^eLCcH2^9`>8Mn~|dEJiq6OGRUQ`1!HKu@rU`OKc-3D#b(u zjBsq?Y?4P25xhq+e9#JFS&GsSRJOPxQL8%H?dkPiafU$!^}d!5=}FV@gPS-qTA0wS z!~z4845*-rw2_(3B<$e(B^gQ{f+8Qoh=Uc>1TGWu$cK;0wh_z73CoF7Ji*D_4-rfZ zu&%;n516DX!Oim>jVOPt4%NkiUn2v^kZj?`}@|X_qa#hiY%`)-{8l04+1H za+efyI+|_=1ymKSXs0d4iLR%~z%Q~6+g907inFVsGEgz2%^5hjRh${L18)oUw& zt~YG1)-o9*m^xz|w5BHxJwU zO%qT7{Y)l)%ggp`^T7=~mKUyOcv_Z^fUjkWDJFxb%{ax@g&#weX=k(%Mq5T* z0&=yP*3#`2u!P}dF#NjYwa7fi?_6HlpS_{`q^Tja5Lrk%N$q+Ji1`RZfB)0l*uNQbw20&nJ zh?Zrgt%{qo$QaicZunD&sfri38R1&SV3LUpOeTSeIcCpx#-Fj|N8$|H#pxL2oMYCG zMkziVEymwD;z;ezYYers+fyZqO98D#RQ~9-5eH51Be!Jm#%lw*7%D((5r^4PZ1HQ~ z9^~@nNCaah6j3bLwQs|vhn|*2#KC5Zwn5#kJ#VBWrBI35#N=k94?W5Szew(;6kgCB zY?_lo@!D4K)LSS4E}dWwesiyFF$8W5+!jriQfm_<5~+*LLq7dZR7!rQrw!UhQ=*c% zv+{iriM+Cx6<0vuk%DG(i`?h*7OAJKIwyLXSr8gi9yQ@gs=~$Ab3dtyRi-zaFU_5u zuw<^qgbTdSu7WmV-@gi+O<$!+vWwsl+uPqM&p*abO<&o`h#xwUF2>?pIk3i%(m1jq_TR zH#7T{#$bgCThgN$FIg@rxIX;d)M=d7X2J9yf4${1aC_ zkciXwjg>g9p)3RFM3UeU;o>naAdC_yN*Ccq298HM9!Ew9b8#FHkq@Lm^6aP zM}YOCQDrN5#Y6?Qy!dXV&peV?6s1j!M$cXWMnmE-LIsTrp1{)QV^1aW0*_RHisaBG z5s6Twr3bdZqyXy$9+{wB+(DW~Q7opk ziAS)R03zGO3hMiAxvGpO=Ho10p50bW!xKnnMbsl!#M7|!le=x@MtVw`a1n2^cJWCp zh;Cb96nNxxgosztw%90eTafuw#*jahj3<4#u`LDSXheAbHPSBiCf7LH#6`XWe62DSB zNh=`q12@LeG2P11&J;9p0mgKWOAJ5&9sXn-TZkT#3TFL*j&XjoS(E!EcTvi`l5zNu zx^wzDJPa>NdE&uEf}Ui-l~!1vZK+tJ4om5l_1gvJWD(OEYNT7~JBV#DBe*T0ia%DN zL8Hqu>@+E^U@{C%s(?`$GV2W+)@!Sdc8B`>158w4vL5Hg(TT`GYj>#sdOaK`(8IAS z3FJP=if-Ql=LGC(68_)Zfcj4kd18XX-6`BaYT(h3fdOS2@Pwt55N*YX1G#KiX#i^k`2s4m|7JrVpC%zHCV*-baB~}eBan21 z7Q8Cpg+=h`wip2)(fNnh(52X?eMTJC@DFlIuJlHHYy>=7%e(5wqXpHs62> zP5co?Dr<1wiCWwE$%_|$xD!WodoHRnLDf6ha)|vk@Klg-;vCGukLBo#&_QG%C8NLK z8v)WlcY$iJcNds=e>`y)qQAHcQQKmEkU36ZCKzW^a&m_%pVGEMq%Nkc)+TaXQ6`vA zl>R%ShkOwZ?XAQ`s334nYcOd|;=gG*9ogQ;;c}KW6@5=CM>1ik0aWAtM1%QK5^$W? zip~&h5y8fyOcnCeQW$SPyl@1!O25^+Vs6_b76yN$N43~X%g`HsNL!c@GX6VPqTDj93vEPc5VOd3W1xrHJlC%i=dTZ z83H4NbXrB!5_erIUYtN*NQGZVB>tUNt?+WVP!y9UG)7GpMhLB2Glan~7Zs7I6nqkX zU8Ik&I7Q=r&}tLWsbh$3Ktyd2Mu`&L0P;m}l6232h^T`MJ1DvGa=?sR+Qxh`A>fpb zb<6BI&%aP;b?nxuBHPKB7EE;)S;vya|(G8b}JZRVQ z!nDF$dMa2>wv)6F_mY^lF&XJqzz7oP-`FsOfeBo_z`aB_5l2z2r%3-v@#;&y{*~eo zAraRU5;8=F(Jyb-I+br=8>wSjp~l1snh=fF3XdTf8WjkM;DQzm@@YYeR(Jx(`75GT zB?MC|%t|t#Aqu4Em<@<+L^DEz3h-`QVW6&z8^#UMBb8_78~DD=^ym)t{SFB^r_nU6 z6@=C@0vh|9PLqvwuE&cEaP7P_@m(_OYfw}0U^zE~CB2I-jm%!y1xj5Bt`LW1I{^ z{Gkl6&R@|z{hlsz`xk6<+juS=L%h;Ac+mR|tl?F>cs#t5fDRNW46zfM_1kS^ZoyTK zm3eA8jX`7iel!f;T+}8s489&BSDwE2(U&({lkgR1K~&fV{0bLohwL%`ofthSKL$wT zE4ZcmPl-u|MP&CQDNS@BK{3TvQjEL|6DQxN_;)VUXpvTwsgH5(6t`ba*%;;ezra^t zzQVW?pPH;8zhsflFK8qi7jfZy>Wa286BX1zYjB**YWW5b&5&q8iFc^iXa^&pfaUYz zQTpT=;X?v zej?R0N`#vr^RS1CQHEN>*klg z0H@Ac;mPK5UAML0G8CFg6{Qs%BmF?n#__21`w10et?pnG(GOXYnv32$U0Prxu9~;V zhE~l%pY14I1bkrdw$`$=pdypg&_iigYui%E))%K@%fOPwR5j%JFEvf52Z zHEMaSNw^fj9P(()O++=~96fYtsZ;2A_~>tF4A^l$9g~I_5BhB)6U0807k80pVJKq2 zEweVEuzcgiXt8?hqi#@5)H7~tImybHe` z*Y`~&cN>bHO=}RX<+nF~AkbE+wUdt42`WI2SP@jx@YcjpvRh(`3Tl{yQ57*zWC~qm z8cvcGFJ9+-0LHCk=#>6yidV;L6b^CCfy89)i6fneE@%$&orv<9gJYs!Y~swFhhx)# z;T`(g?8IdLzhBsh+Y{)JA>S9{T&2GsAIKd^;octaqJmM4cfWq-d z(KqBd^BRZ!NMZ}O)aA>0{MI=w+^%KlFQmd8EZ9fn%uGYGHYjc=emPDoWfQqU&SX=J zly{?UzB6>Y1#F#9rpNL`lWh2ZjJ*kXQ`faNy!X+OY|EA-4`5j)AP*9fj0^@d*d`!> zE!*-e*|H%vNS?rEh)LYWq%<)}pa~(QOa?-pRzSc78a|Lo6L6czxd{c5(11&U4ykE@ zroL?&QhGD=e#!r?b7VvA?Y;lU^CXsZ&e>-VYp?NLYd?fvAnNRuYq=QQ^-e(XX&~Vt znh%8n?wJ=9g>M%IpL`B;0z(R>;diA(nt} zxvE8FkRmPCng0;j9K1IKx`DxfD@t5JPzF~JP^T*ok#~d*!po@$@U!T~h1IJA!eD@( z$4|_!U=I3i&Ya_8x=NU~9^$F;=w#(29lE$`I5XF!(xbh1HZzT^nxb;kPUTL_`LC}Z zU7M=3MXa^0$XZTSSD2oKUtd*+Z{EdWU%~yJGQPLRM4T<*H?;w6jA#D0@P0=LSME&(XF%`Sw zRos{Ne&P16Kn2=>y9SCZ3&?Hku~{DemyZV`2RF0tkbsA(*%DScf2--_|LSh$J|XE( zVZE{-{8-^;mP<-OduiaAPR%nItW{F03&OXU2W^E%@SdIm}cmW z-%uBA5s{m}x=1q{UZc^;wJ8l)E>+Pp;$Xny4^(_eZS3wLl6==@*{!L-$kpM%#9IN& z%0NXsR^n!$p$YsbD6U^_jdsEq^_5)p4^87`Iet&8Cl%41ffKY~0YlOIl?FI8p5}oF z=s&aHBTk$WDEOdk26V?cGqhR*E0sUa=&@sGr{cUWl~{ylH?wm^&j^nM#7`U|zwxIx zMcVgZf8dJ2q_=0i#44Uv!qd-=u;BQE|9NoWdLK^(ciOQ~VQ~i6C=k&(20QXNx<^gVe9PYRM1EKe0A|hPe3PzpFPqxiP@G{ z*rr<`SzzChWP$z_5I+3h=0Ia%n7|Y0jWZC@vmhXRj(!9RoCsPr45!RlKggNQX0f$Z z*ot<`_iA>tQ9yMP6o z!?Ky(7pTC8-H?GF%`e;5r(JcY=SuCIm}$*e9yR(KcoNyBCg^P zgldOUcqiD~8WeCg-DVcGP!I|sFQ8b=zuMn>Fj9K?J^!QKZ;LRvwJ}iU|M)Tj5R8kR z3MH|Cmth_U*ty}%IqUJss1)aa_#@CA^>zzkUmQls?yhZt43U zAN=Az7LWgGQI`*era=$rK&6EwC0t%Ksb3I@MNr#6rSONeRKVe@|&k^UeP6L5VXP=(jC2KIJU3~$#{i(cvAT)Xx%Iv(t>>oo$OEOKr`-J>JSIZ zgW`h3S+Nq+kUmP{j8syXHa^@m|3Pf{mJh70*yWqQt^gU(KN3OAyiZ+d+izyCx2UE<6E>lyH)K}|z~O`)}E(eisPo=_~QVe=%F!S?i8iJtum zzL;2OFX$GJTxgrYUG#khzSCchzYjf4v%w1SxJ|2oQgBop>w$|$g#wn}f>agK-Vf2Yt1zKbgf~qgEtJ0>KF&OAT=k19KyUohcKmVIJ2^q zlS2ebLhdJAfuP9XQH7S->BAABSIY)nLbRRejW$RK4lD-!&&lT%?49T2#)6~#02cJ? zzcNiS*Cew3c;~k+b=X(cP28P74-s%pGNWH{9!)8?`jhfb7{T0I&kWj)T^bX46uG z7^8QwX<~$BOU3H)_2WmHa~kbBT}np=eiu&qujOqQPd2E_4H0uPb#p2K&)8T}SkssE z_Oi=nv0+n!*s?MHMvF22hIw~FM%Au3!W8u;6wKL^bhE2(G#FIr=FmlWQD4Pt!ML_O zD-5E|Uw6|l$LfDl8jSxwHo@kvm)*VAF!&TNnpTuH-*bBN*Og;^55|FWueqATt=5zd zW|(%#{3?4RAtau$tzP16{f+s}U4h~G<$lcb+ozBBQ?qP%(pD1$>DYrbW@C-A`v=UF zydv+@l&n>S>wX4hTz)JRjHes2%83EI)-U&!{iH(f;_&>^VoYrK*H??AH;jHo=rfWP zqpQ~+#D`$qkw*LdWqI8!)OM>fvhi{YHx9MdB`43RB;@0OgxWhU-)3$1sT8yI-B?Co zt$<=X+xpi0EjPnAQM2b>U|)YFHw^lT3ntvC+7-{@-k$R_mMR~L1I5A#nHjXnE93SUT z#BZTvLAIs5D{Y(ja^7-)tFi?=;7iI>FYUmRgdP_4o!>7*8;>59Qavfzi0uj(L_PMX~b&;A?!Y1f!vlDZD4gsr?(lxObX6gapraMRLPX zw!OB-YZdo>YjyT1oRk|vgS`g>ePS$w*_aM)zSeTquIf36OE>}bPs&&H?Fd%_ zf|GHvx3I0R_t!zuNx(M#pw%~PD?mXU_xAPo76P+#^WHY-cW;nQ*{#H7W@@fTJ*Nj( z=ogFitYL1-6cAv+)hbmL*D_Wj(w%5 z2U=oBrW!dtI8Pps={v5WQfoLBiQsql>Dp7)!9>_kxwC z*deiUI#qsjj|Y1<;K#vT-qT7K!ONZijtQ6RL#;>LU$IJP!q$l!8x0r&pn9)b|Ehl- z?hOP(@NV2TtE-d0XTT*iy}H+1?M0;hhj4Xc-G`H>JfZi#3?xR&B@+_?0^@_ z!Q;5gy^9@pdG)NL&D;C)wqE=^Jk^ozW%6usRjVcwd7S3S2_E1;Jt@|r!9??#x*sWV zYcb?GRMQ}>_3?ueDC@`YDBXR$f9!D{V@liJ>?Yoi*_ra?5+6+*NCPO`_}8o_o2^S; z96kw6@`38{UQCVjDM|W-<&zd*g1i8>xV$1^yV4<%A)Z&FzD4+7Kse{{8s7nc;$ba^ z@2tlg5PC{q78OtquMX~1h)T}pRzVb#nDmlFT}`7Dg6ne}!pV1nMlHTYtnU=ZS+z!U|J{>W5) zSLe8Vb52CT*-#K)b)9KBe$YFbZ^47L^dP1H8`@Oy5|>)I=dONn!FTZ+A@-2=HE{s**0=n;@aimArGYkyJdYDghxJpQ81L=FP5_NB10zD_ zO|Q3A0_CyK00xdVI>jSRM^eu?=P6>#)d~rErCNOh%PnmK&2GoV!pGhQrwTSXD>Zq6 zrmWQZ9&GJ?HaqW;4p+m^3++u=tLl8b=C-E?H5-C>$&H|BY}FHT)iT9=idn*pF6WAA z9y@3Kbqdgg!32rvrfpBHZ84@VA{H%?bQnI}9|s(?+dsbRlatuZg_^z?SKovs!8i%U zs$7p-iE}pPbn^SG#$T>bVqb-?Z46)YUo3hD%mHg+tbzIma7;P=5G$L;*l~RHGw-FK z3^j}4WuQD_g4&pRVuF>*I}iJ8t`^m_3-1L9&kx>LXTF#}!g8{yuR=yXdoa2*#q>4- zofz1wW3VTG{^$0;&cA&HpX{&Bzidx@pFXA7HS_<-c4KwzgD>EgnlQoTHP3dhv<_Fi z1dR6!`S!n#5?7ayQ@*KzuZ`=d|2#-f-2N|Dy5DWM@G@Pozv>PXWX-C2_FnRk+)`rr z4bU@QS4>E`K0iv%k0whf>Gg~3Ia2S`r~H1UnRB& zHbMVf-o(2e02_z)-SZ$g?}y;WZLIgo=z3_}7BCa+J0S6L9qrR=f_Rt0U^abR!D0)4 z3C5HT$G*Oknbp(VHR2ysBl_%aje4JAoCoicNp}EzeH?pSBk%)Wh0{ClgW;6QRwY>n zPKWmkuzveAL=zNn5aqs0I7NWAw@`pW|F~jfYaJo1N3UEZ!Do#wr%r+N75#kmQb6-v zis)ZU9VQg7b*I$_6LR!gxOEOBnw_j#TV5y2!F6n#2a|VM?8+a|o+j!%?H=nFyu-BT zfW*B!GS5>F-vZPjdFGYodF&09%4vwm)lbV?DZaYod%D}*Z}t|B*;f4)O_e(JBs^2p zbl`{Y*mOS(=+-|PAUrxiyKFHQURPV3yE+j3->KCDYHjTo=Zf8JJAEP8vB=)!n?JBI5$k)wucUGThvZ*1sXZcVx1TfT~S6ZSyJTxW50RphgG2{jVoR z+VbCuW3_=XdvQ9Nf;B%#%J3^T~ z1-OGeOgN3foZHD@Q-n>f++&r&R z;-RL~a8^m*PgBy-dR>#UG%uUg!mJ89`c8)&eaaAtpmBY~6wq$>7_t8^?S$dDR@=n_ zp>9j-K~jy~DWL^jof=P%vZP8;Y>e1nom2KYvY61P1740n$Gp6%A6nkN%Gj=G3y#WD z{?&X=Jk+c&fn-UQOa_5wdAHluiLDKkyo|tav`6!mI{J}p9S5IR+Ao&owexFG>Ak%r zSe!-DN9_la)-=jKD*Fk=Ocoh23KySPLA1sDnKvJc56@bajA*XJp68%{7mpABG9&y; zzjh>LjsW;G_PZo>D;Gfn>=!ep?23PS!j6O+>%_EO-d!nKQ6DurTT8@~4|j|EVs~0^ zwbrxT$)4;QBoh1OITK!Y!fPEKYy(B^fi#~$A~Yoz?FPnQx(%9vU0+|a_q8r<+y)4N9{ zq6Ssu|Ke_(eKUI~@WY-$u&#&Pv+{%3YAc;D`Gf-KDsA&vkuKits>BQr{OJib%K_K22alLJC}x$bDow3#Jy1MrlP@A!%iHqSLVbTA##N-|hrCBB zm1qanAZ$X=CP_0$n7kF>N{X|uvf=wGst=!#<=i`dL(`7(Iw7{1>Bi-50C&YUL`nzQ zn6LXZZmc4_;4ojFu?s(yR8+cS5}W7pCcG;tl+`PG3(LF-fdYiIXzxpN86>S!c<#rf zwK{^|R8|MDog7p1miSaVY^@|I(B2v}S1L6}_8-{fJ*_|!rB>wD;n6_)A=x!e!LX6V z%`iG70VM8Ar({5^NfSmg_7;#6-jPD-dReB9Yk~`*<3+eCZ0H1VeCme$gK(a|O`Y zm5X&T?q?OVbG5yNZtZw*o1O}IuHpUvz#)j=qDb^HPzPHPRQ~q9r&vfmC%Hp7k*sFc zwy*e?u49s4hzRLXY=o_-=Jxui$t%A^{y#`0%}=XLZx+tURad;29hhG> z-TGUJmf{u%<7-xpD)z$2DrXTYg3N?_hB{rd$}W+kL`S2!P?_V*A1G=$`~sP;!cXpE z)|t)*DRB!LrR>gtPX-nSEPBZ&W3e{R_JpXRV9%65Uqw*%%g7Pxu-P$nk(9oKd6_89 z3oLB-BUh)|+rFYT54YQ&u#vRCneewJUv%I6CHYqtcARd})ea?V*bL=g?%Sj96i>Oa zXCz98FlH-c8?>hx1QFhtN8SsT;)_?nN;+NMh?4$z9LMu=aKD!~?c?l27Z-kA&DHSw z56R?#(|X`79(Y9nlJ{mCROj3sFrp+s$nT^rP9llSFvyU8|BnC}?bk z?%q!Vp6m(*k27E4CqD`A7K}}@-Lp>n1Fgr(k+OL2d+fvW7gXX7%(<&C`q!?$@xOBQ z#b0vurNGY`jgCoP4!8tiJ4l0;+?I*89t;`cybq?jzt0QxLbi8$$l7W1_Ic#>W~Ny3 ztC3T2cxpv%A2i8|T(6WVP~8_x<(zqSUleObtsXglI-v@h^{AQvDXys)4R(>TIC{Ya zkNYZcoWa%JK9KIHVx3b?1VQ#l%=5W0=Dns=JLQvB{WI@H#69lKJazR&LM?UPKdP1{ zR1s^}H@2nPqpnMWc`GK^RqCk7VaKId=*13x@BJZZ`kPH5YcE4#E4A>sZgZBus6G}6ql zn|P3Nu-gz@3M(QUgZ#`F4;vUJWTG z!b-E>lOeoBZkt|sk|2C3x!doVti%G7g1$gf(EH=MJ~&BFRrf0gms?dsRbguerqORm zQ!qK~`9@v7!nT4Znw8x-ADClroFq>85SSC22cGu`r0y>uHqGv}C}_Z6#w&x0ae3K2 z2jN>eAB;-f+>Ct?L_Bnt$Mf<*5Me(KtyjMX2FVr&BjmPhWvp>jLP*5l zPcqx5cAfe>7=8qM3~bd=3Y~$UMgaQ@3T10!;9$TfU~{yq5taY?L##QJgjHc}6YnUg zE@GN9w6=J?w3nxjQ#HeXhra)ls1h^qpuFLDP9}@d6Y^4A3$GgX@HiqbUEazL(~^0L zdL-&&3K2AsW^pZM@pa<1Q@5?_#cIDRyWik7MLdG%EdK%==#F={nP(>JhKZR{AmJZ* zou7jT%bfXM9~CA%r?VM{@0G7ltZ6+~L2K17Ho(4G?A+ZlA>S4?9TbbclKv0h9W(1k z5CTcDer_?SR_WW5_Eh~eCp~T3glpm(o&0!(Ze0XA|DG&N&pzjm#A*ZH~op5+7rN4O4-^VV87m%Vtqdb%D3VW);Mwe70 z!jcd70alHeARq&Fbgc#|3W69D%vU6r4-pDBp~FM;LNh);FzdXn*Nb$!qq3UUFC1E- z_6ZY0E+l}-0TGls=!6$5N2#M?0*%DO43x_PV_~#vfW%)QH05o+7f4IVM>gIK&itGg zO2RpOSc14ofLGib2ng^ZY{{M-fk!?FaJaJ)5om561f4mwXR93E>1BaO@TByMUwdLD zJ|55tl>%b+UbY0M5Ij=tjr9_J8k;Y0obh*jIH~!c^cPJvV#aMdgqlWs>!Gr7`Y!-K z7MLNut_7~ppjfWn_rQ^4L-NMF?v^kety8L&SzV`CuFZKPh^2i{Vlb>=!_L#F(aVANXcXSu5l$+PZsp6p3I;-$<;WVs)Sg^RB5 zudrO(X*v6XJTFbh)=683ugR@vU$OMGTmB+%($pu^F1j-C?sC4bv8iHw|2EZPm0`q(3~a9j=(42KKE!wFxx(n zq@wbzb&FfiXBeA$)ce;h?pT{!(z^lGYJv$_CH9)D{(31c3$z&N6qK#Y0_@FCHAa5a zs3Z!K{{^N=dfxNc*$->IWL_PU%q!qjBpy`(Q1R=qwn_nOq(z~OUBwdmGW;e}0>TXC z;&C9Z!y~lQb#cv@S@!-ttpE}S$i~lKQJ@hJ_h?W@YEo&(BLM0|-}~BcJb;xIA!Wm6 z5-7e;WP61`0drF|p!6z$)B(t01um_sXMUjeFEGkxJ)4nGeKnIwUNe+z5ckE9e^T95 ztMXS%Wli9>-z$uEU81ELC@0_O8#J4GB$?fmbY;8qHFufdK*PCFuHME!Hp>TA#Ft}OX1dvo?HGY^vPti8sj`s-F9O%*(M$B1rqJ} zc=R6Z7|^oe{QNhQ|NI1Iuai6q$67V)b}|g?K zQ1S^XUsS3U;4YWGJGDm75$%CqEL=)SP}~QtQyd8Jl0f1ahxV2O7XreCYONI4CGWW- zApD|Qs{s|91r>lS!`FaF(u#v<#8EAVZ<&Tm>d6PjsXEBs@ZMR4+BQN~_NiSrC80L!p=}Cmh~TrWM>y`O`7ifC z2XJvkbsO(3$Cdjl%6m`_>rFq`Es~th9{iYv&q-`XN%(29{UxWvevSZ&wG79aDA-tyE9^#SIb5*s6BKU+!dQ6(Ezu|dk*z_{ zj}TCn22{w?Kx$AVGN3d{c$pOeVg7}HaxkERPpGmF?%HqW@X0I!EsI!7q&s>)%d|oZ8k(_edCB~k8C2{~*d>THX`aV1$B28a~Pc{2R zRyAxXYXgi8U*AEK`$X}r&0l{Py~P|f1fqJL2DG(jKawNAvr|EH7J)fK=GV-_#{M;+ygHm&9?ywn&jCG0g}`Fz0TenR zYjP#BCTGVV#Q1t#lFf>Ao{HRFWT`UN%sk%?@Y_Mn)+&_BYTLn&?0vk+^Sy@PXkePn zh+UsCHEp{Q_&1^^C3Y!^mUiExXvNkmGz;@qeOuTnO2y;Aal&ClX*QGHJ;g)a$uoy$ zF#T-vxzCXVh1gMEm-t{1dKfqG5`jp9@;v7i@2(sEdw7T37N;n?6pDW5; z3gp=e`xJ^dKcl=~=^u9d5Jk)1$em;{G+jV-DwtxQAiR02n#a-?$=X0{vks7A z?}{x|(#vEd6#vV~SNIVxt>-?_-oSB*PLXsW%fLoe8+t29{BB(Mr#07dN^~ifV`+%b z4@jBEVH%+>xi}eC*ks`XFpvN?Re>!+3*PRm7M03?q8vqGhBGVLlp^5gii!@dJU{xC z0_y?A<$&^%Lm6}78&|l_1rAMJ#6$mq8$S(v`yn*C!XX~642o$kQ#mkA5Gi{6D8;J9 zAWtl*XI7oAZ6~e_1e9l2Q^t^TbuSR7qtP`L>xK%^HL60a2-N;N_|@##a=_*Ii;Y{s z^1Jd;pMuI=QSm5!HmiT^MJVH&+5GT!wg=;%7yGMDvQnwe6zW(h;__2h8C2JM4u(Bx z_;Z9NCAdi`DH)rgs~0pvU0H-lU0k!Ogw58$Z$W-*bQS)Cuv`>~@zp>f*K$Xyx%XEl zbA-eNdiw>yDSez(L2(#$c?yGQ5bj_g9`DJ&0_0aTD#Xx3AR0g8^Z((;7YSF5rIvGn zrg2g}Hp7)2rYpBV0Qq~dwP1x~S3!+$;cACMD0V2!Zy!|zR&u*%GXe8C`ce;FWvGS}AIIaiEaCX| zT^QpDX{TWazXgQ0OPE;Z5~9vJgr>KT3bc3sLi6dsw66n&31Ww`Tgv@@oGmam|?87fJpc32Hfy8w92YTA|SX@x77YQP1?+Z`NP1MQpbFz1uThhX=$ z2je=d5KYG4KLLzeEufB4eZaTowloZxw$VP*cqeXVwo#0Ru49DFBn5a*rTE*KXLBYi z2MbJUOMYVG`AQkHJK*r%e?GkbD*p6DgcM1Bs2Pdu^IeNT$)E$j-QnGA@7E5oODP|h zZ|zRBbyUW#vR7>J+w*np^}s@>T17ilU51lUWqLV_@L;7(xQG&Qk*~^Z?^ycJ9fENbV6Yl&?vF#M*pN{dn}!VDvty zhA#x!m#9$Fqw)(QKqsB0T6CM)pYnV=Mu9VuBBgVM=Q zx*5vtMKoM6mMFGRMYVobI`LMnHDf~F2s332O~-P7vzI8@$(1|GFJpsa+5W9pFwdWB~pL};F!`p|I!L){-=1ySYMd*FAY2F=z)wrp* z@MQrk5wFZ|izeY|zs zACCsT`8c>w~+rnit^or_aOlJx@U7gms-Smvj?l zn)%(`F#v}X(qK+g|W~V~%B=XK*JvdK@0Th7@y- zqSK(RcfR!V5CU#lhq-M(Ke}Jy*UsL<_vkl3yxhB3j{PTK3_v2G>$G~X_pQHke^36r z=Wu|(Ckg*s;76JA>ssX3;V=Tz$5RbaIVebLXuQPIQ{YXTYgjWx!G!(CkM4(u!L@%E zDClN@UxT(&d^J__$`1_K_9GBefqd`Rhtu7|!qGx}pqpXu310x2L#mOaD#+yajR(+o z?-sR8X!6s=jEDvS2+jU9dh%m`y&}%DwVT_+jb>aBY#_)g-VZb4Ic1o;%wWJOHx7qQL=LCC#m5eIfdSBjJ%Pzg4=1L2fc@9gD z{zJ!zYcB<|9o{32L6!<05Dx&`IFD}5?QUU3mL)aG<|#WFggo&!vPzV^%$m!Ju`}CI zh!W!Be8n!;i@wPSZ!EV0*y=t~4lrm7@G);GV4uqq#Tj+3>U+w^JRRHExCb2vA zz}*os*Z}$Seb|wnEAZ>JKfMNHgm~|(ci3$U`%3#_?_Gj^ER|`>)m9HNEC>oE_3s%- z<0JOK!jHZ|{ACMcH471Wn434k+njSGKdNEY4gjj@C)6mFxTyYd1VcBRJxF%tw#WHf ze(nAhv=ltZycI8jDRpEdu1HNx7b9iKi_D%m%xWw}*ey74m?P9n`XMB=T~>>tRwQ>P z78+EkH71*LSsel`d|V^Mu`mI(kJbC<8e#!?051%TL##9I()}ey3R6exhc)|jp{4c9 zLMZ(F2XUTq!HUF^hMbm+OUauuj0B6)`>JlcS5gsb^}n@h@?NzVNSJ5csYJdHq7d=h znPJKmwhEawUjlM@r^`rsS>#*wYbWMB>?0hHL}!Z}-lxG+Cwl_oh2jw-RERPI>b!%e zW-SYh`-OA`)uf}`yU1low_RD0Y%z%I5Wexzij%(bFK^6GNJ<7M0468sd6Iq4bpp5K zfV}bTf}=g&Zc6I4tCJEiPjlKnF7J^i634p1lZSm@yOe4-K){5(0^6^1)#d9bLPU9P zMySh?Qi|KBS*^T`3x2Nk45wHAfFFGZ>N33tUK9uaNw?J0S*6^RX2DNLBf!LabNp}3 zlgi@`sIDJi3!@K2ML!)K^AkZc4hC4xf4}P+?bsG@sKpg7A+E4gh$3b9KlWUQ0>Aw- zd`+xZ3t=V=so`>Nc6UU_-+g>Z7pQ8xRCjRCWoV3k?Se95wMTo=(e}W%8Qgh*1~wK$ zJ9ul_Yb**4a-4iLQwl3GFOE6!V z6jIreo3v3yL1*;D%{1j|&&*9j)$FV;^ z!1wMi`g~Vdpu@+|VFcgPYZINOIJ*R##R_m0ch{q|sf*&8dNJV?IXj zpPck|>w$!)Iz#u3_*J!qAG|prFWaA_UXAUWN*&6*vv0wS06oU+n~(~bV-XdJTvsu! z(C{PPk!gs%4p;dZ;WbNa?KJ>-XS)N?-0m#PIU7{}ME4-n$IcG{16OpJCg!B6Ga|Z^ zmj8(~;C_|?MQv{6Ae=id5+)OI4#-t@EEv_C7*ny$jAX0Mi*{YxE$~yS%dR+?Zqgwb zggh-M%;E%nyZE|vk)U`(h_*-xUo|Rdf6XENtJOno$X)PjEO~`?aZP@1p}qAI0;L<9 zt*o+br0GzzdMQtQAo3QQYjd$JNpS@_B!!s82`M4ZqVa_-T;0`r*39{Kw@c_)+O;CC zJEmYL6{Xs>#huqu(cOWpziE$4%en<=fszJ>9ku3RIiw$WURv_HF^Yelqe3=~YV8o} z%f-SpNwEHt7(7_?`USky(f2p*bbBLVUiSPbn3U0asDdd4=i<~rB%A|?j*5S%D!jis z_d&Bb=k--~RKM0)JU}ZbI!96#PXgo!Ghr0lm^0!ns0Q^dNbppq zl2jLOw7%vk%i)|*E@q9+V%I6YtI6|ZhMIC}hoZPWE~n$2?hmC<9pOa%0&keng@A4oCZ~qdzO?6Wk_B43NL6vB zR(&S=V%!-CW=(;ZtTPM&?RtU$LaL5i?6XIIBt)MPqAf!70|Kh}pq_gxu)45xjwH`v zHls7+P=v1~KWmpcdFB$7H1IXnd$%J?V`qEk_~LW;Gs%}_m$vq&lFv?-bEZnGDpfZ} z>~Njxo*eINxNVNOZ5+3VU&hH zS(jCEwe>7)fHy$~jp}_mt2iMuS^UG4^5QEKt;`7w0Yt5+^DH_!87kUtllmXgF0wHJ zQjCJ>NEuQ6Y~#eeW}Jj34)UbOPURxhE@d*eb|ER#Qq7)gbWvd|Stlery1Go8AfZh` zQR@NJO)ZqX)y!bN(t4sh$?8P9tp%p;^UFeUYf^0Hb3Ws;O+_6FNg8LR$6?-tnvS&= zgTJ_Aq$m{i54Y7fIpO&{9ND0nPAsIov9=ig3TxW-mcjDJ%IoxBDe5Yjd)R}D{F$qb z9@t~4!pF;;1>=yTD;29}L#gL04GDLa5UZ#a&tq13#>E0%yGF{m=Myr--!@(YszJ51 zBLiA^+E;a{6HuENm~&Zpn`7t=MM-WAp3B6KBq8)mS& zTcI`F_fvhgxX-ZM?_vhyCE>-kta7{3q)?}gvaG2cxXo|OC0cY^`pd>~jPU5@^40<%PqMIBk&HM5pb}FtFj3Z`Ol(a*#5G^+^MbdPNT5rt|CR{8?&U2A* z*=g?=r|bU&etp)xmGmO;Mia=~Q1y=Y$CG|MS<+dIcEaCIguBPlUg`7CylnofA~=*%8M zZ1{|=N6Mla-(AKig$pN^5Xy<$B6~HmJbO;NLhKBS{YF1{%p=|Gi6qBJ%k3z?T_nNXHfy(8=U4=niN2;1)$BgG-;)}EGitgr zBkRt@AFi96V9QjqS@*$WEx3@uXH2lvV#n-)@X9K$XO@`Do=pu#)#>s)Fj`1*mY%h% zHIPhsj0Wn%@yQVS{96RvUuM}-$&?Sy`3x?E897&6+q?NRgKH1fEugl<>rwDcN{3(g z(zexNcZ5aRz8Le`^M$EN$<|jfrO0(vBqv`nTs2SDUS;N*t3E#38}Sr)?Hc7iO_onG zxu(HMg7nI4a1VGs)ANdNn`w>=&8EDMQqu|GanadZm{7MGrp0-_6J>90R!p*crXueX^HXhfDJ3tQgezg05HCfuP(cY zQxUSQE6u9T3(J&MeTg@_Q&B6#*=ommJ8L^ecw%{6yA|`3epTi16F`;Dbkv zbOCAR$Qwt!6WaRYd~6Ug;-8@|oS*jrk<>5~0yMxs2=tY=2^??|`G>xb9epcCWJGVA z`2!!6MF4sL{|)1KeX%Z=@eC!Q3>+4Yn^yF)`P){o8xF0zIQGg4yg2ssitu?o;*h6j zDz?yMWb8qP$%pum0mb!8M!@DIw4l?w!VAxUJJ46D@uI2#P+sWzQ7_C_!W0)?_6cmN zZ792LIEZw>F=RMKv>=BeQ|NR(IM`FCA?E;CL(v#HL9n-Q&*{L}pzgsZLz`Y9cC5VY zMy3vU{GQX8?)5O%q?dV304qWCM6cI8s6W|oEmH~26t~cxhw+4TWtDK+m`#`mqEiWS zja+Gh3(;fTxKE?}@i%{jk>7Y8k9Qjkp1@ass)jdr)G zKwStAhXN^B{%n*kut0yeHmK#v0*1#hso1dyrB%XC9+a*Pzrm6PXTj$We!5$4J=#AEjdE!2n9WiL0b$_wj#i}Ee-4|O=wm*;R z4`0D%?NM?W1id_rRP^4c{aXH3fQpXYzYozO2RHx1 z#p6m#xvG?FD)?hIj-sU3$u0L7w)cE66lpBTp~ay|UfO}Y58lQ%9&_V32W{QJtPo7H z$d1rqDS48T(bB#wz6}RY$%^FOLdlzlqn#)(NCoB9tXDHrX>kq$LC*b{yg% z(KJOQk{sUjvS1wi1qU&_s9>Cjj`Zy(6_U_zYO_FT@BLzC-sd=|2O5!hhO26?=?!rX z*sS%?HFBzWC%Qvk>RIe*d6<}gZ@TL+;zvFtK;o5U_)KhX1ASX&KMb=gwV+-}bwA27 za}g1X2Zy~0(jIVVAA2;WDJo`fRP?|75A$BeRb@2qZ`_^tH^OW+K<@7LaqDaG{%W}| zW8IK0Ff4?3`?7TQLr33a_99s6vh=0!XY19JRxLOmsFxIU-eACStz?rOU+vW%?n8nM z1XikW(ru7)7<|UY2h9}!3ng8}=}Fn$QTkf;Ky#MUu<{~KZ!26kE?EmfV`;D%hK%Ro zh@5RP7(Ot^Q+BVEnU%a``!wCM(kYUwOZ^(D;e4p&?2VHHF;7LorjdvUU9Qb%FAvXO z+6dSV{qw{_LtxM7+q=cIZ8byPebTQiaj^SP#M@G)l9-}%e^KITITW!$H#c!uz83Qk9T~!;Uw(wHOCtdk zZge0*4;vOXOkhH|gN&`~n*xU<6Q+pMn7Sv9u}O&^QCFB~W^5}H3mBV-E&|RZ{}Wm# z(1&<7j;Thw@jt#t9&jn7r#MIcEVY_=^Z%s{>3!k^ob{FQL-!~5OcO37wCG-p&uF$?YMs=eGVI$IRxct4Py6U7;N!-*{2k< zyOF&CYHx{LzzkWYCMC64yAEuoJ1AxrT(w8#C%eD!@mS~d)wc6K8DJy6J_{M?+}d)X zh{d2TOKJ&sF3FO%Il@?3)#Mn{{&-08XC9C??s^;KFEtm@h^V6KAGyVn+y(Q1%M-Uq z9$l1`A4MlS+M8h7WSc*b;7G@^1F;foBsUV1TmJ`KCR>lbk~}J16l=iN5X_Iv`>4az zzze46;ygX66@^37CX4H5Hs+K4t%kr$hwto23b--q!Px`jLxF==f&M^P_Z3u3M4`l2 zn!>4!1dj-k!ixhwt|v~#ro8IYwOm<-qi<0ftY^(d6D5rV4zB}hDF#cQ0tl?(s{K`# ziUP|s_1T`?42i%<_J5owx(?x7FA1sY6DHQIa$DE-7HST2%327Fzlk>i*tOuhn_BqX z5&g&IyCrZj5^Mpzoz4pO*6n@6;hl)T$eBI^oIt_eH{hWkgZR>kdCLgRAstKh3JUu; zyeTUK2jB0%&%fN7bD1)mGzVdO&7FCLfd1u4W+)7*)s@!DSW0JfcscI=Py?K6_?9Tk zf4SwJmKxVn?dcHB6eQ2B#dNSUR(jsZC@jW49p&N`}P+dkT}Z*tdc$ zcOcED8FDRolLA>gKGZduT?e{<#ABj1kQgSX zqu(t%8gY0kjeZu3y*(?YDK`54xY(vyniu&|h|l)vvsReQT1P*sWnWU@R3T`v>yzb^ zc93L2pj+kks(8<$oZv2_M5+S&E*vMHm;pc1K7!FrTDtBe``(JL$M7|H|E!_8fyUW#q>bCsTBWBp|`DuUn#p=mA=T#aa!NQ z(gQo&*F(dGb0hptyVKiL2UqBtV@s|le)+`paUd}|k8{?=1?MXp>Hw=AeyJ>FL($RcDRjRs~)eKn;?%~L{<{FlKk1DD- z+7~?P=xchU!rO%CW!0UQn1XXfp_UIaDTxiKMCldJ`h--#Ks#6Cc*=HYC^O&?*Gb-T z8yP?JOte0GG*5%2C;i-l{V-WmZRy4_FKu6@XSrL~MdhPn-gK&PFal31oj<4q(ZGBv z{^-pDfYce3XCVa=7&2P#MX*PwQYUDSUnfcc>)QYpF55k3A6S^2!HQO#~GWy=3d0kax7 zfwb`9Nlky=9rof1c8bY+UwP!{BiLhUUO0y zY5O>{BSV+mu`Fb)^$0b0zEw|$+pI`-yBa3P!&^adO&CVJ0EcttZf7$aos$_mm9RQ< zOL-CL=SzY9SuhDj3RcruaSI3KMSiQ zIP+k_#`qSyShFeqhHpoFOWCG)L;TZaTNB=SiEzds2H)akXW-kb>nG{chU2^**cX$h zEUOa89&)sg_XPSrpulclKSyu|2WeiCu}n!WlI39& zHdA3!fCCGT3N?ZXrp;~XedF*fl9XVrAxVi*eMw|*(YHroa1h_Jop^*&l}wbX^avF4 zl;(fN;BVNsK?NXM^x>H2YOZi7&MubX&dA7WMn)DhX!hOLR(^FRjBzlojxTgGjF)^m zNx4>hVnBRax(}I0l%6x7d4$QR5xL!Ps3<6wTB>E&wehO^V=LU&_pMKZSe4>4Qo$YY zHW{tKG!TheK@x|@CPDw$scn1Djy+J0_@m^tm8%`X%c@^$Qng^EMDoGYUWA!qw+C7s zBnDav!$=`bD?i$bChd0mx%Fcl&PCDzM9XV))#di-T_tv9#uIh98pS5a?;c(9tP%51 z39!_>peEgLW$6X{b`QaI#iv*s&lLWivS$cp3WuG)pOsNPMITqJk4ZPWM{j64V#%U1 zT0wS7KC@Q!uKO>ZT&qu6Vm;}rRsK_-r@Ni09E}PknY_oqaN=-0YAPbWMmk9Y%-f4;cy(M@bQgePwSNt)H)Qlo~dKd}Z$HoMK3-QrWM zOns|K1qWPfvgQ$RFjlqNs&e5%@e#Qo7K&+tEiR< z;(UHN&i7w~dU6-R;XuO(>uq%OZQ$#}VLfEKQ3NnjKQgttD1_u^sqR^j5|L@}z!%(X z*_w&!bpDf_++gVZP3NMC@<*{I~R zYG74e$(zT+2u*R{ea~ZplPok)v$799))tY>1kt*-VJNi_Txib*vU{z+YyDW3q$EZg z5V0B!@Ok%afO$!Gv=oN#xaaQvq|`_oGRvkkoXrE4MZL>>u_zRMD^*=*EDneFCX~ha z6GIPwrHDs7rZOrn?{#dRq_ptKy5m+D5;Af&CKzxwiORO9J3JlZTU!wJSpr9}M3K+t zM0`?aNKJ%8HuO7JEcQYr*ow0~8)#3- z*M-JQHP>=IEk9$Y)Z0=*sxv55Q703sVq-p(7bHESN{`ckGj(~r)rM^9Npb~*m*>1* ztjfBTi@9T%wei-J)zD2;PDC9fiE!Nx_yuev>+fopg1bTGMBU*rM5;~Dx=`AjN+l}_ z!q|0QKw0BR^AZco1i_{iYz2xiHU`cY<)ALqI@HYzr63l?*>fyn5?<;+cy&A9}MQzkA zo(IIt<1Ifgw1cxO0S~Bp`lj^mWvf<#Jn>${PBAOP^Oxmw%@U@Hee#Sdc4cvNY%CP? zm{@4Tl~1)`hThT4WPzFIvRbs!EwRUA@ZZ>s1(uw_lP7{C24CU9u-wZ=jKqoEZolsw zs-^C$@#^>^Tgr1fBlMqP*KJv{UHpTsYe_{0x;AlZOI)>*9sJ3k2ZXoxuEZfpVUONiR;TWaW)G-WMX%lV zB>Ts~D061c(S~w(e6G8zL(jK5T|g0dobcqB zNjO^-D^L5X+2lmHS!1wWq%d>qzV{9~w8Lc@;r@^2A8RLvtoD!imWpp3aL8(eN%*u9 z7;>Sa8A0%pt1S@-Q`Ps-Q*cMnO>uaHK4JplnK-tZQv4)j0f;mx*0vqE|kU$ zcsw}X@zTKoFf-Tid}fT4?%6%#)xUVYX?aRlKe>>3!7}z7=)g9OyY{$01B| z1W{M;R{17&yW_KN=*<(ZSm!BLqi_iJqRQ~)D#?^e22_k~EK4*R{6ift`gA$xJUwZ5 zpdoU!L)u&pVIYC~KLoHA|M^68tl$qeDSG^w`H!J*{=(AyzJ~l48cu$!fvbS~myPNGWtC;$YeF zg%kRqV;#^U3B<<@`KXX}XZdY&e0Qwtwfw{iS7mC_#&T<8-S~qoLe15Me8Tfo*Ffmv z(4gjALmK=9_?T*&^WahDhPyS^7Cfftc?_mz#U5rx!hdTOpJS5BpC((fJoVxY^gXUX zDhDa&#z(u`HJlEBcOzCi0&GECyH@CU)Bqrtu{!YZcGH~|Opa|f1d|M6CQ5lVBk0$$ zl(O(+s_W%UWqIVU;;Aud9?a@UcNC8^0hRSHDbvr=Js50PsfB^nJAQ| zbUe`I&hiHybTu2R1P@}X4CVOS?sRiQE_wib&DujXJbjB3=kcb5A~I9D8hAx)xuI#B z#nc_{k3upOSUOEAlS)r!PRP5B2=9?Hp%wvrOpmQ`QODm2G^^OL#KJ6v?}BM)z73+r*%U#Igb+p@{V5Dd86wkyFy=o!=t87e?Id}$G5?NWbt_HYbftf`qj4`r%+3SN!4t>{%qal6dI3ViUCi;d9CFg)YH# z!oD^l%MN?1D3rF_*iA!(uup z;r`ty#j=&qiZH(sU-O;5OM^in)Wp zz{<5v$%2)MKl!wPMtgt!k;X%HEZ3*R@o^mPX1PW&X?N|O`oK)ox1I?VAUG;m#Y#Cq8(zN?hL_{@Ts%9q4c)@s5DcwYti&dA(dbJ;qa_ND6tRYMimQCaHg zJvhT|SR{NiK_Y?S38cR-A$N{-9{V;K{}`FvJ({NmTz+?W{FEoEo`q+CK75Zp;L37V z?C5*84gHel@VWds-08pAjhZ_RgnPV+g@!GonaPjNL@+Wl;vZqrtl%DkMi$YO=aNo^ zUI3>T*c)2CS4+e=s#sm}bYvKjCcigG=yf(ty)PD+6269dKA_plplT8}sUBSaQC4I6 z5BMO0QR`-2t*t}+uq#(BjCiv$m4Ly$gh)v|IX&5zY3i1kD)5ZERpFDi5F)7<9x*Yp zYR2>pVVd6aA1F1~1>7gsXEj@q!6R43TM11jXWaT$N!3B=3y^@679O*nkhsM>!?30r zVZsXUqsMV)dRi)$!9k_pnRJ14Xbg8!JgaedlUU`#rL3TLDgOVPeu6+IKbm}wnN?Rh zd{L<;Co0%0&mN3UN{&uSAP2(AlVN8GpzhF64aL`BElZ&VWl_i9N!v*FtCNzErZ64I zO0IvApkPVKF{4kTdbXq8jQkeI)q}N5IluKLTqw?)8bA}y2kBLyA4!jehC?{@nqbt! z)R2yn>Ne2jI8;M|2-7f=l+=V|3nsrzShSoLcjvdE;vo>~ZE~W_ zAXueb_Dxh&d8cnTC$2DpNXWukWsyK4R_{!xl7BT( zMI{=ebdPBn-9sflWv|i?%20-fadIxL!ci!aBWGB@Dt{*wrT`Ds2x8gMvan|>peQcm z3CNyB0f(n>nK@w-*aM0sQU+#51cl92m)s+)3x#@k2(B!od{L1f%JwGi1l#VGWZO{^ z#ROZ_EvLc%|B?3YaZTU%{`mWod=f|?2ndn{)C5RKf}m&+2woCP!bPqLHx)49s!O*zSD4zsK*7-yW+9xqNQ#%j@#IT+SvKCjm`U|;*IU^-A*n5hAjhB<(vCr{ z#nS=koG|=qr#ebPisFhQBtzyY1G6hj2Z8(B+Iw4^qPF{x#U#ccSUNyT7+{UP)jHYb zOBq4n0O~z9i9A%;SX6rIfvy*`l1j0CM-_kp>4qiE6d7QOlh~?vX*IrP%X#X8LV(;b zYmcC>=RYwa7B(WXxT>c**wW!ZbEHq2ZNAedYigMD%I(t#*#sx1!?xK@oNhQGpjP?V z)%vCI^YUVKNZXe+Az~Q|NcE$Xv)e0691$L=nRPrq;`7m!J%0>7tCD6LwU}V9PYE3kHb9-@0 zBn_$Oj1F2AH(&#n1tAJa873kxY-wT&-49x&&2$N^STZ`HBHBgZP!*lgUN4ad@Z?9fhFx2g>|?AQ)^y`Pqszdahr%lD z@rOv)`DJYS5Ol7fx3Vj|UGxgO5DAl|!sS(3!fT)?S-=@GcPis*D57?z4`-UZQ&MEM z0$RV;HRA30Ygp0h=1$p$g{5T=gfo99zSL(an*iTNuuU%-uUwdR6;~2v9cTqMJRvD) z1-j5Iba!6GdW?DK+L(r5yE5BA;$oEeEmxpZz{(ME29o+`9YX_ZH#aj&^a5=te{i42(DzP# zgKv#^+N#`yGf-@-mB?S}QZ+c&g`J=|xwgji;50ixcNQi%bSB2O!6KMBI5T(H=uAU^ zxsvdwTcd&22jLy0`+vqkbLNDn7H<6mOFNK~NVGbOU_9KJ7Ufr!(fY#nwWRH|2Qg|e}0s~lx^=_ujR_^T!I z#6w0Gbp3E^)1R|Mg<>q%=M3^ZSm25|#2v2{%YGMA4H$rOp^YoEDkLFLsjI0KZGI}bHwFEm(dGqGEZ30aEa_@$10`za9GHUL{~vm<55^0^Dj{**h+;fP z+^X5i&?6-7;KG_Vh}vuwA!H@UUY4QAFZ|9W7pW;BX$050(Ho;vE4IH zhQP__F{e|D3p$Mx9X76V!d2|xj|a9Zb!#*MZCGvW&(+sg83Lyoz|BL|2{i7Jxz|P) z+xtP%Affp$v3^ExbyZ+##u|;8>j!vmnPVgTq~F8LLeHxNN^9u)#CQWU$RYP+C&7*?)IS7M;w78BfsNHZ&W?o)OP?7Hf`OgXssJm|7FwP+>~{^KJvmP z3@XpI@YMW)atHQ=-;eB>K-*gXujrm%Uogh@w08aaB7U*>3+h_qi^>7KczI;cVE-`( zsqT+x=WM*v8AbkMrW+Ahmodoz<`gjU**|hjgPQ}-h2oLyD}Eq;(8Woyy-|B9AMkYX zb}Cc`O8(4MNvJgh z0(@)5qfYg!um?bI)3vdXNit0vU}AwlBj3J=%B-?NY&OP`x=m=y+zW0LUz8*&k+nh6 zDYc3WF8~+$$8Iz#$({xg1^gf?x$bMNZ1F&!tl7{}VB6d^qFA^|heSs0Zm>^unp!6F zIc&DeH}urVp7nrUQ`0fuV*-kQ5t=i8dAwx{`b+IA=ymMQL>9rIqcW`{qz{&c?H@Fa zrzF&VN&yf;#OIMsCL;wDW<5(49>wu0uJDN`yT4c?+~Zw5f7Qqy9EkXhabwEoot2VaeQc)A?dWC#}Q$I9GNCwLdG@~Zu4Sm-$@maYe z#-;5Pb>LH)1>kNif~nhMs|49?3gQL9@$;&UNMSMpNv$Yp@UM|)??dg?CEn<|O3Qy~ zm_ylhmTTA|7-{1SJTjw=LlZNMi8<&8!gW`Zer*6ioh4992R|T83q|{&siIyWwHNgp zsi^8BB>>-5n1peQq5II13T6)W!J7eEmm9K3BUsY!X8i>pR39szMs!=hSGM=Hy@)AU z#i;5Sac^Ur(k?kMjW(E6(In-KxjWB>k|?%#(xtf>qr;YE%c&?yv-}#?6vR6W_cT>1 zsZ{^w5qufeiL#tIKSv(MR^pG#XzXnmIo-w0*dqRUKSIWRe7->x9$jbiQwJJn5)p6N zO?gcHUXh>GDm?GgVRrt=Ua2|zWpyKpH^yCN^2I|SEi6B<(3sqs(VWfluLbHB03*bO zSWm`H_kcA<%2w29xZfdNN0)o-CcfhF*+}VS>2^Rw%H{T|;})6UFz>lEA#Th)i868; zLFsTCZ{-C0$a|ttX2yIz!=~LZh7&_jgozIRnd-2r**;zC)hU#ljE`|A4aJ{|cF?rk zkHW&2ynbK5SH72C{&5denT3SZ-O0ls$^C=|ph^o-qPGI4+D5u6x<(e~LO}=jnHn`I zH8yFIPja!>EyPt@K9``mU{@OOXpVsgsk839R_*P`adr9#c8R9D$ zbuOUV^!9&j~d{YZylXy?kGg$yGIzdbS^4aWILuY`WpHGy`b5x z(C9yNH)A~j?@j){rdB@u8V9PKTgdjyr#wSnvOZ<_%fU@?;V*IAN9`N_oXs%)1dT@j zC&Vu|fi4^6OFbU;vdJzl>^YmOJkI1)op81+E8(QBEt^5kNmvx_KilQZe<0qf-^?+g zI5kcgKeqx0+D?nzxpw?(%L9Aa;FFR4q`@4H$EJvxHE)>uLO8=Jeul32lbh)G=ITMu zp}Z!ps+6UA4sdG;_K%yv7C<$Ot|>JT|FV1zz1yMSMsHj;+@O5S=SG(E@|T`B_shN* zWtDvsrY4Y$y67;nJ5vh>+J+G4`yz3K>^J-A6a5mMpHiq?5(IhAluMZX(&>QCmSnD z3uSj2E|-p{sG+Gns4DV)K@#P}9b#xO<@4rbhfVLU71W-}_ks0G(3je#f+GqKTNXIu z4kRrXg_qh)+TP;1TGtwN?4X5s#@L$lqKA> z{xMso`r$XS;oGaaW$ZS(l5CJ0pBx`6QVbZ%I{@Ol*F^amL*DlcYQ%PEnF4&L>} z^@PRls(p~++0WAGO-VJmH5I_|?CH`li$pfWM8Leds@aRV{mnrRg`)Q5GACyGe|g_IhVXwno|1qM|GVH(>Zd#Sz&EmW$MmlF55wrChzMf*3EU0nw*<*k z4qaxQc<12y$Q)sfc;{ft3ur?*cV5*Ynj1XE)xo?_blVZo z=wn-MAX+iR1*-sC685y(DuLP7uCh<$p+64+s@Yaxh4G!vkpWi14-(6EX$BBuQNvQw zS>ydj;06udM+&cplvUUCg&6-%g=>vQzu9VNJX)Nq`jRkbUyhCp4EjdSvKszP(TsUC zXYo}<-m`jSpbk$dLpG10hcdgiY^|$FcV#zW4M8E{>c!{eZP}8 z0Bn6arv9CB+ZcyuGzVqtu%$e;Q>??+9*93X8@y}=zAIT|FmP7%F2x)#_ zQBqr1z1aVf^e@;VpgA^BuE!()SG-+`I(Q8;-RQoPVKc7)h0{kcLR#sqDP9mh1&~Q- z+jN3;oDrpg#y951tlfqW8t>^e^g=OfbHU6K>Dd4AGWfhu3$slj1~)~WG}>jP389qd z)`S-y%#6ZQBWas2K zkLfl2cxsNSOWx1LAJ^#H-YDi_NY}a|8&GO?u)iiui1p>ZbnK5Lz?ap%=;z*ztf2#- zd|;knB5N^~*Y#j$6V90S5t5pS%BbqaPgkMqggz8`z#$Kf_uBV)RNzc zCNHMJmg{&RF1joy>_>xf(r5WoWPx?{F9bXShBl?--7(1E;j6Z5@1U)JJJXD6jfZ zumoVvnvdcjJ`LhHa&}fhnumCPn~}ACeryP>v+U?r&uTU^tN`RYK~1gp=WK^8DjV!f zVLh}c%G|%RBqr5{uDLefnSM80IPr=(5h69SHE70R#U*wxMVH+t^na8i+k{u;Xu{XP z2<&iFfE>73kzNVX%X%S2ZL7vDTw8t5W00cLOl#}|h2j1Q^9i{|ho&&_l?mHJW26P* z*x?P58i)jE0$52fBMJnqIK}FK-i1Zsk)4HlAj}ByP!U>*pM;Lu{x_an3elds4~u6P z2q1zaTA(crk9kOh)9f3h_ZxcS?>Fwfg}Z5M#jQgE9cP(`N`b z=m+dmQbn0J9nb@AfLp437HA5?w&611B=If?iKGE4k)Z55-7e*fyd{Y>kM-&HZ{E`J@rk8Lax z+15=AI|k}?BVYr~)>tl3u0<^Hp{F7pn#fo^G$h0O<@IvZtniuKA*NZ!ltaH^n><7F z7+Ra@HthS^Oq<|6YDq3CqmnPQK2WUEoQw@eX(={Cp=V2bu8CH=@j38*0&_@-wh6c< z=v8`tpt3Vu4PJO8-HsVdsUC+m2av99YFym49-{c7a=zZbblt2x+40 zYKRl^+n#X(#lqim94}|@@n{EM*}92~uG6y<4(d1XJpo08SvbPteMq4pOobWEHsT=y z`IK(Ghuq16T$E0eUafV-fBMbAi1&}f$d{tm%itfv50-QP;=%?yRCA>#Jj#cQqob+u zZUA~CfeM<-Zz_l48@QY+O^ZkLGnAYoMSPiE&Cn^Am$ zq1eacavvW;6|(+u_HonWvly0+ohyLl2{cqd-O`w8$S==p7H=uBPa&I>Su&!^^cRb} zNj_8L8y?bG1gG;(_~UvQs#0<;@DUySD$s`6Z_96j7AN8%fd?tftWB{R{FBPvvvMQ(C$06GpKa!hNpJq2+|J{IBEH=;h~H0VF|-b}kl5?sm;0njPjs9|B! zfvdbI*I0nw#medu$FXGVSiMG7E1P%7lD2fPwK=0AGtq*bcmdRU>Wp@O`k^jwqq&Qa z-Y1H4*f#M(zO&+ZO(8SHnjV3A-h)bz;!O&RuPNZ(CB)q|HIocoi(#?d>&8evHgxXT zP+V_(B)$<>k1W45wQ^S#ss;iC<)|bSC6wg%?IPirZus_C4c-mVVhXSGwEb1Q4S{f> z9WpyeU7`zo4>0!?;Y_Km<*jx#{zN?hT~YXkvK-U_U+F)y%`fh(IozhsGyy*OB`*~T zv-WvR{k>orl#92$!5L60vBPIMXi(8{*w+qnHU2ZrNs*j%1=|~S4x_W&mw(-_ctoe) zhyo^>i2k?5+hnoj=j}&7s_E=OF$D1*49fZikb%Z^p|=?Sg5Y&6XKXL}5Ehh=iXb9f z8jeb|ZE@U8>U5)p+q17`1u1@k-VI)J-bMO%*V%L-$?W!ELxH(JTnb+n#^pt4oT|?*>L5git9zGEtb}g5!18EP!*xd3?)9hPQu#ez;n8<5f%jFH<`X<`E@#|IJ z`jwoEKEfO(pK}b8Y**OS)!@U3l!ggOrag;IxpHDe%%KQQ^=cxhU=#H zDq~wKOjko`-fTbLh71!~^#99o2k20wVUcdms2J zSfvi2g{nfFxpSrPY0HL@%XCjNKe_W->-UB@DttB{95ZYL4 zgJsg17ZS3mUOK3LYw1C()_aSUjbn}-2>qrz>UxBf5C(fA=e$kFS7_)x@Zp734g zNVUy8?Y}Mf6X^Dq~iI_77V?{bQVdJ%irY&GOC|t|P*DNbx zLg>ygCM}$Y;zjhCEk2%tuL_cd+Ebuzvytz!H-O0iDw!XOs$Gh@q99n=-AycZw_%qe z#d67x0c4*rBPs`o56A@m*63eb8vudB2N3o;#hzjtN(Kw<6953z*#4bTR3e7dO1}TgFHx3uuaDvhoIsTPiwB;Z{Fv}92MiU@putTWkoM&odB+*SBExFq_LI68gn0y zKhM(5_v!(|M>`6FB3Lj1WqO)bjO_X)rmyxY2!TKQH65i>vkO7`qHI*%YnT{d_eu?5 zglb8&O#HB#0}M607`bur?1nIe(t=RCV`NtyEd`|TsPs(ha02)~;F>rb5gZ4q!{f8( z?tj?xj8kIz5z=p$g!Fq|^ZIP+!{p+L%%J_AB5Yb@(=9QlKiyx7y4E%{X-!cVTP}fo z9{@?jK#Zx{2K5cA4>;{f@0SpY#g}yd%-$1UVCU zQ@i@bUSMeR*1+}lKU2e$>_Z?Oc?Uk<5XKbj!?yrn$u*Pw&`+fw)xsbd) zID#8{oM8i+R00CrFYzcyv&^da+#1iO7cEgVcV+}nrWw25usSsmtH5u#UYYnBsdb5) z@aN5>T_UW;fL{VVLO7d;uuVgMCKp>M*rQK2tzO6|A-9;kK@g?zCk!K8a}et0Pg+c^ zH!P@pyTA*O~`i3gV6X_(kp{rBQT*XJP+`b z4r8pf_RAuyAj&}FG&(F2NIa?p%{k1{8KmD?9v=B8vcj6cW?b~j-oQJ{9IKli2#Y5=ALFpAQo<6+tpEU#IdMeZ z#(y1ceCo=)4tXQ29T;DxwENXB2#QqTjm&*MVS*X-AL{Q0osgluGd~>gq#UI~%Ac*J z?aYF@8gDm?x9#SDvNQ~9_O#Z?{ANN`u&U09FpaM{IRJHheT4+MT&KTAQ|{~4Bd5cV zQ37Tal1jZO-zUTa1ZrOAY5lNF(Du!{2hYc+T!KwH)gFmtNnp%>rsN-#K7w#UQc0Nm z3-aW7fT@KY#|&}vzmArNA5&x27Br zw+1|qk1R^$JqAUgzDUj+6a8y|Hb9qHUs%iSk5%SM_4Wc=1wif(`mSVZE#W{@gRc`= z)m&)i^N2zIJ)$*(M~clk_WLLySBk$RR-)+_IhTS(A&8(LMcbW}k?#a{am;N{PTH2b2%xaY7(3a$ig1V7`2(`QEk=7BMA-&fNI< z^*w-Ouu6&u6wPix1SDk$8e?n|mC5|`a5~?OEDol82^*3KSp5V*$mkriMRO4;Qf{|= zz-LTbO$1ua$C+9Tr)%JNX~jS+Fi@4TB{xV#O_E3xmGrbOjS{X8qF48Vs2xCV(AJRv zI}tw`T{or>OBxMBs3jKiQHs^A!`#{|{J3h^u|g`K^T>idbY69||C(Rt+tbQBnX<9| zriFEKT^o!)GRtxat|26w^Y*p8rJ2NTp=$jA{R2q(u6IudL;U9m%OQ zT>Gw;n4phFcDNcxvn`t~Xn#EwYPcxn0Z~q9ujF;EMne-s%q6$u=+E?z$);122=0l~ zTIC0O6w~8-Kzm6mxZ+xBm_Vc(!f8Jb%?toj)LEeNTaR$oP>8dKDT)Eg*}BUwktGW3b^-H`xtEYiJzz_K|uS$BEPp!$gIU%j8n z2IS^;Ei28Pwv4e8D3I3IFy(u=D~A^8tKL}&y-30Qh~m;k?#cB)xTY8VV%P=`lp+&SDgEylY^>7TxFmL03 z(z&|(Zmtc)3n~&4(vg_IQ>5qHw~LH5IeC6+<Nxo@=Xn5nZ1;ZIeS}yZIJ3x?Hfq`MXo|70 z-aNS2jK0Mf)Tj2Gm~Bw@FE_3+kto->F^S`D+U#bEP`YC1j1Ogi{bi6;1*;Cd|Ua ztV2p64$N)C)e}G4VDfqc|1Z}~@v*VY91I1gp(4H8YNu;O8YFAV!6JmV1iaODV{tu1 zeIS~Mcr@SLxWK|I;Z$)?TAm6A9i@^Fwj}c~vl_?J52o7qs&g}mdw%8wGr(Ys`vBl$ zs9T8bz+z~`t{sypT2?LH?UbX%)2NnROPLZkW8C~Xlz{VYn7bNICxFAKzN8*i}&nc3sa*35I~P5=tc0s{74jiPD{p)yCy&!7yk%6^g507h4|}Tc3c; zc2spDH3%UT2wT%*qlyW^jO9rXTSU5$Q-L0=G@kI(g4K-?lWDesP6cA-q3j|Iru(4e z$a|5zqgh0H2H+5jCp*{0ctfDtW$WYz0w&<@ZhTuWX5k7M@{+j&^1b>GIwbQ=|m-oBd^WSnm};%%xNn=Yeyy1Q}#2|5EKG}d+(G;j2l$^_jRsIp@a zkeAWiO4O=t*voFFJ<{scP54nxQ@RGq`X&&-9ZQr()67Dn^GRcj;e%FQ`8JxDOb z^XO=@tzedz4h*;v#X1M(pA1UWH3d2yF-jLB=VWD&Q3duUci1JNsN{v=_HLKE0}UQA z-_~8a0(d8YvlOr3x&K>*lU8!mI!A8cl}m&x)sK1h0|Gi`CzafrNy|~slFip6{7kBH z&iYPmk@&8RkABEM$QC&On1tq;{r%PnRBBw!Hr-)J5j^t-L>`;^7DNKh~<@|>bo_(Hxvbh^nX^2 zLNFLcB1@>DrJ+C+7@LE_-)pR$3?OukC4 z9$c#l2p{_rHLw2;im|%VF3iR@P=B@iufiA&mw{ahVGTC5a&V>}RIl$f zI_HNyhf)5aVtG+N(n|5gCVakQbKW!A|9pN269~ENh}l2C_E69xl6F zHi473>Az%Ub0Lf-hB~jq(0;d(=%m0R+%G1rl*Zi5F0i|&7xInGGf!FC?TR&ke?lI5 zlaN%ecOn=Ntkkx{IY^gI)AgV(FQfxPM=IZ-uu_c$?T(wk7BkA0-3um`AhJO=EW~`G z8WEXn4Q10!6TxV74QVBe4Kyi}<2Hx0}>bp&sOgv*`#U&rCspE$qMH zn<5mTG}G4e;u^5ZCE3C=HRi>4$^Zq(hl*1sO?p5qBGijyACN4VKKaQQ6`9UsnX#X! z;yzh-UbVq%1&H53H6-5}Ks(Xp&|rTtpd{pvabI#ZVOEEMysFoOp5o}A_5k?078*Nmg+k#9DdT(+=K2>pH)n@?a&SD5mkS? zk{GB!)a|ZSbUs_*N(xuC@SqD@Mnvp|5s_0xNx@0P9>dl(;E-cAE`v=b*dK?3)((TI z*)h$KXJBnL%{m;Ml2(fjbJrUCkJxv;Q?Y+3E&|0z$l9kbvYTSRYW;=Vl~j*5ZfwhB z(PQ!)zGl;7bW9EA5zl8G}yRO_3(6IEwgkmjdPxuzC0e z3!C@L%^s8-c@pM4mZ;PwZU(@tV!C@ewNQMEa#vNF7yuL$#bm5!%17`3ay-Z!4EvcC z?_6B~oRG}`JT`z7LH#GS7v;$|crk1TLp4?9P;4z2;`&F_kj<^_q-O(;FYD|^vH*?#U16fTUSXzSW4 zl;!xI-p}TMNLHrFg*h6HCoM!SlOm^90OG zfzeDqw57>E33S6ABr~g)UETt`jO~+JSzbZvQ2~`(jLeBlQ{P9~O#NER1eZuQiM>@y zq>e~~YVVS8Be@bhW&PZ@9Ud!U?L@7j9Q-+sn_9Z~SeK6#kRgsXxhURE zo$X5FkJA$X`n;IX9~{n(ma-hj2K^3y7HaX5n=GUmhTMl}!c6fxTP=L1O0EHR1p2CR zbwtMwG2=P@s+u`RO1ii@9Si#hd~4P>+l5T*B7LhWp3i3Jz5W#!v!8v10kYGJs7KXq z&j5fGL{17Oq{IU`kZIO#|1L2BNNeDykbTM)HK7M1TV?@5Y8}vYu!_*eR!(wrzWGS%0JR zDt7Y)dVerv^_GV1H@aNOXBbV<{mc@%tsUhipD;0VV>tjrrs6APE0L#W9z_ykH6tZ| z9tAj@s(h1G(78BXv$UaW9T`R4K-_1`k{pYC9Gc41xTm# zt;)HUk|Q#uk^ecL{%n*bvHV&ljFb$n#1)l)Gl??9U6iNPFH!uF)6xQIE-B}b5b>nN zG|ypyf)_ukXGXyO?0tbRLAY;o+$Us)NmRNo0~Sg)_}VR$h=bxuXh%tjkqLI_H2yjS zCOe#)82{Dvq-NFYCfjlqHOb2KQk53pLG^#&9E3#|6S^ud@=m=x)uJWMMwW7v@^byK~z) zWoh(fb`DflEY2#n4d4IhC`(6cAW=8n0cU3zAQ@ja;Jd|Fz16@9dQdVQQT?Jg$Q|2m z)n_PhESUX{zDeJ4el}TmLOYPC@yn*cOn|#Rz6?bSG~+LH6RMf`xw4|(El#RBU`N=j z@hyIFNbZ8=E^m@r&=b+tg1Up}q-r}dXgDJ$)rMZVh*OmBz!~{| z6ykaDhu*W~=0@8j*-fCVfZ&n}QnY#Dqp~gynm`A2&slJ+#KYbT!0?v?XI=jHM-Y3a zvJO-a?Z!ekejc8`Yx41+3^|forTQ{D$7l4@FOOjZqnxH9pAmDqHkyFdX!F_{k$MTC*LnC(DlM0gZr_0cVtR^?U=LXXUq_H=0lFgt1)#p;Vx z#!N%>QFp}_ELMT{3z*okf0Pf?#f-HdOQcygVL)|`avbx!<%9>MJ!7DqgsuHqs-t#N z{2*_;kGGs^JX0g25qv2ub>?iP`Y%$okpScV)C4CZvW#Qq@SsRF4!LBlw}MyYmv^15 z)-qC05=`i;#ht-o(y0(0AgzM3`OO94>?`x`J&3Es?3(%Ci-P#hh*JkopvJ;V5 zE>O<9;~rRvbb4JP6;Q?KR5dGTukI|ig~Zzt*(RZ~p&gn*>t8e(@Z4`$;7$l1*~xe%PC&ZGqgFgM=oB7pQ?Svbej zy0b*vP*;*Au6o>#kg-A8T@#*Bf?JjB??Rv$a1B|A3buMS>Qcb7asQ^Fc3v|-AEI*b zfd^fu&mu1b5?BC|T^S;vY}6^?Y)4!b_mCo|aUP8<5+lk;I4e+Pkr-<0ppFU@F>GJx zDGxn0@CfZ1)b zg3Dx~iK!0|$%G(TBo@%1XkI$p`4;I@-yXyHq8*^E9y$GsoEJ{#)W>6uJ0CwOe>TxX}mh-c5UuIt01Wf!gadX=Vf13U?mJ#L3?wtoZFbLc)jBARS zMvLa!3p(v%nitsUOwR&g!!NQ|2oqgRo@boNC@zB$!?hWL)+{KV*}Wsm-JR zSZ=ZJ+&_YN606}0qcAwBgfwI>+PMpG;mQ(qhZ?0pv>=I%*s+@SHN_Fiwz)s)fBVM> z!WbiMf_(*eqm@MHE&)R#j1BfI7k#%Ad`?Fa)m}qzsKbR02HTv4*%VEH3BnA zE)uveb@yh0(AL$tTTr+a^Pc=!jfKfSeOoRI$nvTJGI{9`{EEaQvYb5GRydCk4FsJO zEAKKU(hw=-!)5F{Xt7}An;yr2axF}AASH)4l!ru@tbhP>0Z`>Q=kLaN|0o+qgZ3_2 zeN$hi6kWyShoF()e?DQLh15GX0|G;2XIiBaEFU0qxv8Gk=RWFG$a;w%rC;B>1$PzS z-Sqe09o2d{kH3J@#ObdjznW35S5dAg#Nfp;Z#>mb4+K6UbZk?tH$E9N{&IvgFtyT# zMjY>@=Fq&d17LOt#Wy*vl`Nr2TK zi%2L??8C0&=dN+AZe4XcT5DBm0)awD63|rJBaVBJnKMZ}ejKr(6@-rEy;b{KbR<8l zSff|7aNAMjiCZDkevO&WAkBihU4 z?31U(O2Y8;rE&b4mW@38E2L%lT$Euy+X7(>gcYv*f?(ee0NkzE@mjEhTfK7MCOz6+ z*yv<;^DdIV8b&cTVf0GqRc=Qp2?D{07AhZJ`W7zl(e;aezaFn-y*!v(3u!B<>61(+#9L18~jb?uZ@j`kcN{ek^VGphHeem7|F9K*hPj_O^b znxpB%uPR0$loZqc2fRfw`sB~h;h z6aIB7`WJ}aZ;mQ%qz_wSi-Uf_U}%|e3NODh{@)UW?`;}Qq;{Gp$))}Fm61Vq5M5xf z^Qs`1C`-m{G#`2uM}6d7ynRC@c(}W+zl@(Z(a+9G{_)@9=XDPx{Co9iJkQq7e6pXd zejs6_9?XWS-+c4s`&s@23AOc5azat){vr3X`41#m>PO?RrP_j1-+Y5=G#;_oFPkXB zcrAg~R7QlK{ZM!lMhpKj%E;yfmL^(T(m1S5oP)+QLvW2YUCD$24SAjL7ZfK>|6mA3 z@t20;pO;O|s#^WG|A6ShfB7g&S6&94P7(Pke3G3${E6ma0Vys|b&u>s0EM&eGr8BK zXys-qCSi^iQ$d;w4O?cFXRFr-*#v2$qLZVBAwo8#aXJ6OEWwfV=NfImwV78#N&PL+?Z8KLG({f+}{l4kaH~;RMQH;j#?`uO#R9xG?Y#x)| z7;%n53D5KEyRHS4S3gEm9#0hpW|?>fUdsh}JM zDkRl(q%vUXEx0ZK8cKlk*c7Xhv_zrJ$R#p~krO=*r+2_@<(f?f5^bgS9_{;3?cge5 z_mNkfBpuP?U<+jYS| zhU7a2d~}SK86et2(SteqokQZ(dy8h2|5$_gYAq z3i&Wj>no=HdofDyioFhJ+>y5Di)^SZ{oNf$ioehCNARZbF6(M!2KjrLgsa+j>bH{+T9isV}m;o+H#KSV2O}QDip#)B&wYu$@=yjSi;19 z%ST9rZS!tw^T~S7uOevbtle_@OcWI2&(K`zzig3|Yrl==7h=N%Mz>7?r^Z>qGj2qb zCe4zI#lp8JnDi{^d}tox{-9>8H!K2oO1w>^6*Og~aqaKBS}G)Epx-0|Gm8aJCK()g zn}D#_TB*UB1wAlgTbW#F(LX~jfzEJLY}*Owi7fEI#tCr-;V2U8g~?7OlJxJRvd2d z$#v_?AnszS{}rkjgv3LUEo6)FNQ5D}`-^^WxrZYq78Q1)=7H|$Rf+uziM1qD61QjT z7S_R>;MPK$R)^Mv(mV{9ttpzv;-mNE(lZ%#g@?8n-%XXv#PnAhK=?2pcV^byC z<~>cgH`L6*7|t}%b;I)%XHIGZa9|>8aUh8#p*i%AM3KvH;2M1dI{ zyQB&C?{K96Z39Ri0=vaWE;C?FlUtlxA~9W`H;k2@W*@Bv`KdW*t6)U|{Jd}#YihHG``?`$@Or6#}!gi4`TgCH4XUtFF@!?3T|W@ zVyY}?Bv#rZm9*PlKX(HIVQV9L*!n*d>|q&Y7?zLbnTZ%nrY6MC!_UUpJu|3peYIc#P~!rL>-P zPwQPN$d+WBf2d2*A$}Ko+P;S|d&Kr{01kyob-+87ez4>N(7B8lzW`W~mpQcmTztem zD&mNoSyP}D{BX#QH`BGViR$m;YVTjOLnopeQO`<%#s}Gv~}qR(Pv3VQ-yV1tO0W{aFCIfPU`3VMAjLbbAhO z1h}!6uyGMPyWQ-exLcdoYE`XQT2g##EO1!IQ*zmBwDMgKq0o-*8dq-p)(#r?s&|KD zC+30+;}BrR#%aTK(7K`lpd>8}z9YfPL0)g>z(4F^fwA?5K#IQ9?Ia3>>|Q#9=7GurR44NIeMupxPMwuXTpHWYdAB|6ImWCcHbYSZj-=!JyAh5Yh>Z5XIhX&rjqU??hW=z+H2JzzGKo(K8Et# zqi3-$fhH>UcyKL}OA{q*Tz8|ZgO&WM#RRzm;)2ThastY>X~YvTCFJP+Vk=@ln$e z;rfejNlg{gb!d6?SPOLfp;U5sTOb9Qx9Sd!H(V9zH)bWAY0&^J2-PTCL#HrH0|Dcw z-5+7}3YLY9(*YlZBKu+gQb5vys)tf@SP;~!x4=x0v9Qu=O=_9MYW756dv-K*-L0Hh zAC+1J=oH4@NV@NpOHL&;7qmJQjiU3fRkh(s94yL32b;9c zZYf&3u#N;rIBibJ9dCYI1w&bo_yi3!ZJ>KosLaNlsq^hlTOEZWx|3&WoL_k|X{VTy ztuIC7Bi9t|z&Zn^Gr6v1@+j1^9iXyGoE&DBB3EJp`LlR!=`{8mzsPw&L=Y7(7wdwb zT8(MOZi1K_<;#k7$PXN-1DU0Lm)RsfC#bc4Q4XMpQ-OPg`)N#uHzy)avsV5n_Oc;2 zMRifn*sslP5@s?FoE4h;JkOJBykGJCnSYPnoc^_`E)<=A!)D7AWIM~ITNIt=$cPA; zQUSbKAZ+7d(U82--=jOJ7%yqkywmi=wnIjzHr|G)J^Bu8pH+2(DetCP#V8PWWn0?o z!=?$JGWf}^tY~v1)C{^;2&dX1Ex~EPrPQdD=b}lxOH`qMVP5d7wiWUmz=5xpK&J?i zAduQb+DL-6?JN0*Jq2-9v3&dE@;WFeIlE9q} zJ${@?0RA6`O>j`GI%qW@mLy#R0Hx4aBa_MB5YU}w@65hUb|y`TLb{(z%lf+`tgXjR zvS>1UPe|DcMnBg$B&|U(x@K_v6fKhOpkHEm;m#5J4r?KpR-~tlMtgm$(>vvE23n_i z!&QnLvtPAcB-Yb9+*h>vu)2!lpy(nBtmJBkw*i64Z?Ys5IYlwo+=X|DZ!JA)p*RqZ z05ZT~GG!~q0CYoRIZ;{ii7m}JE6GC$|&Mutu8beDKdhm{d~s43sGHN5R`W|R*+yS(d&;XxFG6%W+d zCBEG%Pfd7-XFXFm0RTKTo zek#hZx0T(^5a!gcH@66JXF5RmkNRE}upx?K!g1n>03izdmK4OP)-83VUhJhK9F}Ji zw*l8$4+HlDB;Tnp%ce^vi=i<|^b!5E7SDt3hvHOD??dHsJY~+rQuL*4_^Wt9|D*it zs7r4op(gVOyN&FV~Rf3cBCK-ldN92zzB;gDu%Up z-S08$W{U3MlI;g;9#BcS9nUm=uXwvNE9Ix=XSxAiMG7$ z%&LI`#X8%ak|ei*cixfrDMlsgpf*k1u8T)Tk>s&uj>D~ma$864 z!Y~Na-Ca(_V*O*nfHz0~$B;5BSmS7I!g>q;bHt^2s8Sv39Iwg^C|hlr8t>XFy*r!& z_IzJ*YqRZtV?KAgla?M#xk=LSa^1#F(7r#&YP89=KOn{`%D%|V%8q$Y2lTB@(vk#l zJCE5l5dAPYRYq;Nbv4LAYGz9*v|-(6OSFk#M&9$X^yfiUxQOw*G36k{>Zdakw9_Sm z_7^$-LbzQ?gk<&}as{Y8ope*tn*Vdqe3nEROLD~Iwj;K$LU?ZF?-#6~-cIFD%H6w6 zV!&?5{TlDGnywb@t1*=P+&d+PddfpsJ&7BSaSP~3|Ck(IF_57iLq`=>$t5Y);cNrE zlujpo@<`m`VCLINxB0v~eV-SKX9drY&r@yk0`Ct)(b~IJYlhsjH6SdZGSE)013G%< z=wAYQ2xcHaVW>F(XP7(@NGY7ojKsp&iBLyMlyIl2mfuziivTo1FKCf?+u_G~w}HGy zzlm#2cy%=4Osi9ij-znOvn-X9c;PnmxsCH$*`rd;330Mb$`x}a7uLy;VGg+ydOftF z@pz$9qt8ZsBp`N|+5hK;>&4)0(Ws3OrxIu%t2FolG^gKwi!Y?@zebM4l@d$Ija?6~ zn>_akWzf3G_tay=iy1INaVS>N(*`YVBUYFO@t25Mz~*BURR0$0s5?k|FBptX?4}zs zWmpevq>^fv&K|4SZEB=+ic!8IFIHAoXdW(`=#(dAB}o3-1Eg*PvPCqgVlcH!f~aow z3ZYefQp;;U7>6!=3A4wxLz?7n8Ae&`NgDZVS=@`uIJ>rp=DsVRpBdXk-;BLkdgqAxR>|Ywl;)_H zaONx3{b-#w!+r@?#~Z?1uAm~2fa(>*m|!3muo>ehpLZ<jZ_Yp3G-R zuVNmLFnMk?VAgUST$?erhAPoA8?_j5B|OuF2p(2ITue#W-83e$UCPG!)&E;U#6DP6Z5xdNgbl2y@?CWZnd$5{jxIUj#R?wRZE z{wv!yZ3U(Y_Q@TBhQRq|UdcY0_6%Zc%9QWWwc$L0$?`{>h!fxN*)piMPYpcAzMFWv z_UXhYcfF7_l?d1b+f#DAOoZwE1YTE!p0(xGnsN_!pUNCd`6TVM_?;foR$R4V0z9Z= z^_P)V|IoLO{2(ZD*TWscK_1Fs{XJ!Pa-lLZ6d5*qJ4Fl z(8GWPP8T0b6RaD1-4d#=E5X+YPR;tjrvqCx7bcPTF4=U3nZ`f6ZW&M7t~{9vIw4i_ z*<)>o7t9EMAd8wiogu{#K&Q4qzEJx)`WAU1j7v~g(AmMW6TrCP=LtEi-S)yoKxPP< z)I?JBrtuwzoP~BP)n;^r@Ov)U#BDkFUBRj>-`>agbHlepG9A=vvG>H^sb-4I$A+CI34qJ)z?E(gq`b*Zi1(05 zKuviz&|jq9&h}NDMU=Z5Iz8`|D2+)$Xp=*M=4^+3eHbp87sX6B#mh!^tiNuHQ_9)= z(AuL~mHr}V9=DfeC7!kY#k+%jw`3B^7s#)?(}Jz@SEvK=Z%KKtWLo`twwA_5*GHDO zWpLlyIDjKBiDZCU+41f>dG(uE>YCcSb;J4}h7_G-HZokK!E-LrPgh>(RP>4MWE?V< zVoZC^r^5;#6a2gwRsiaEyYFm%pFGj)Nz_Jb_{c^EzC50kh;aqWYOz+49xHPHw@_cp zkZJ%_l|oKpBuT#}Wk^~@6xwAePxZ0corU{mKUsJvVGygx;lvU@!*5ST{R}Gw;wqJC zC#h8l+sz%eg>|5Sj+1ix?JUK3X~ds>=|K<>g{*H`8RH(VD{7JFSg#`!v(*%StMXuE z@R|ki*slbCI^`8IUTpPhmqhsq;w9>AdA6z6HLX{Wl`Z7RDqG`G^=aQpuIX!0ye8=` zxjisWit?RI%9z=i?OC(`?)Wjcg3}exW9<|=Yv(+bc6xS>zK^9x-%gu=`U&pN?0apB z+KYpTy8i!P1r&!bCQg@)R~D0I5Hobuxtk=PdgRF8Ix9$kBU&-!~8jg_uuC{bz4F6s@;QL z{!M$H;!v%sSm)Th@df~)F_y_7tRuwTO9x2Xg}x`uGqBm+!6J+*5w6%mCcZ^HS(zVm z67K?YJF*(4Satf%eC{TD0B67TG-1VoYmvq=#jqV^=lx9`}pA2MrUoBAJNu zz65>`@H)>O|1`?Rzy#@RIUIkx_J!F~i6z?o2|qQ6X?qegOAaSKSt%MY|LgZ8c7_zS zG~#&V5dTr(r1ul#n26O&G8CWc^Sp<|3#sA6OzoirQ`+IKGwT5iP652H)v3A(S1bDD zq1rV47y2C`uGtM5?3AOyeMQQm+CNmBragSK&=9m0vfgwvN!Fw4>)`c_1*Xy#vr<{9 zCZ##5uf!a+dSy^U0R@=)YHlglQoA;rsK>$z8m<_D>!Ry7pa#BV7=Gh}s$2Dl&e;k5 z=Z?)-Wf&6*^ZItBAL=OBs6%!hY>}1j(=&86wB2_)$J6#^+bfA@W20kY?V&8%+)+@Z zD-Ac^Ltbsm9E%vIMV^e|Bg;&D#RIU0Cia|oTQ}_Zk=B$uZ1cQ-8u^6va4nI7`1&w zdiY+2^p3^THb3sZQ^8F!{{*r@E&D%57awiM*fWVwmK{Ny{Cy^kEiu2&vpeZIFqUa- zO)**RH5M+Jw7gZZ*JfyY+Gk8D&`kj8{N$qJ3C`Iz)SntiO+(&I#_WM>jW%IylX~gF z%$VQ?s)3?9F~!90LEvLh%_(=KD4{@yo;0$_NX*OthYJlSKACoSwlV2c)G>+=73H;j zr;DFt3yGll>KNVWsAqv`n-N!{3OJsJ)VDZwa~2v%!t6<*1Al;-Dk{eP!z!rfXCVP--l$;Am3RVpw~z^3L{>}^MmfB zfxNXE?;7i}wbWgEYr}=x6n;(LR%!K&ibkEDJ=$p~rE}bt;%W7CowUP!b(NEN^)8jY(%vB%mY<1%8um}mJu+rE~WBST*p)|y8?Sh#Y`)- zAu6N26$-O^@HgQ)@3)(PR^XcuYaXD5we&s6uA@I&Lyr({i9uBztqXVFPB{O+t_(xk zCFR*h&N@!BC+3)WhQEb+5;)k3&wW4Po+sqh17{@IqORpXquCqObj?)T;5FXCk;~q5 zr;D!w0z+sUlPiC==Bm^$9JgDTrjLS=)}8zODbWI38VG~gYmNV!B93tW>5rXMz{@bEy zH5o2IB#+6J>W~&4ohl!zJNWRZ?+%Jn4k@|7@H=5+3p5#wEy3nPQ|QC&g=%uW?BO1N zTiD=rRfxVshB1mb;pBC%`d&o@wUV9>-64?D5^x1N@5A1x>>uXVCnBUfmg5Ebi|1$N z3^0|Ejx?2~&!fsr3SJD~QqtLrX>YHFJD&&?5D3L_pM^WGj=~|DXYNv{_y6;qewJ~t zT}xP)516fix*t?=pmvYqbKrXeiivP+Zq}Yq9u?OadS=P1u;M?@^ivnmNO|K(yAX4} znWvk!rkq9GHcTvy9A9gXiEANN1mV0C&g|Wo$wVuk8~_3>{#N|Egg(vU6=#BVoK$1+ zGI#ebuAkrcv*aBu&N>N+q%(Eot_k{9<%2cNn<1j{%wWkWQFik;rmmQ=`Q`aB&5q5PzR@B>L6TKt7d^+oc~fx zz8gr0n88=}l)+F}LrfCk3XQYpEleuF16_sgMmGwL=L_jFy>Qh)6VOE&+7hG)8PPRV zLEE{8Nd*Gl(hvl$$rbwk*!bpli{i74lg0IQCxYU%1ygwJ7yJJ9?+TWBSk=H51*a`$MzDc!~JG!pefV#3IKR;W0m^q^yQaJaXR2pl-3Wwr}#*vh_f?W3rh0z?wPot^Ms>93(>Zzl+H=e_f ztjXqB+mr_#5mJ#6YETp16Ly()cUFQI0H4M@^Xmm*2!5B?Eom48Ck^K+CUX5D=B+?%LG(9_dz>uwK7_1WKXFXos{T_1z}sCQL?dwD4#NK+vNv`flZt=HwJYXz0c z)c2?va~w$BQ3ZpehFSwh>!iZzJf?6~ok6`!)0LY`*dSBho1i+E@^Rf3iER}jh=|>w zE(>uYwSsxOA>(ieW(b=tf-`bt_a^c)}eThG1~`9G<4Lsn{}hPCczRY-aK<^n5o5#SujggCJ|HR z2^rV0@vKWPpY?$SC%PfI%4&gZ(K%oFQ+UIi-vU9}^0v?zskVfBth2t0m1d;*gnc`u z&?{foec&0A2fC2Z_>x|EIdFlVSJ7`Gd%fHzWT~N+X#OZ>kb8kdcjpgb0+o+mW2uk&D`DZZLJ__CAt*0WXM=X`}ubIBTsZ+y>0>*5;Xwe|WfY)Ahe)8N!7C5j> z4eL2CNtOvqQGx*qgWL06FwMaJ6knt4No2F#?_-Yn{6LaNWr0!6CuDB5lq72_*;PapI3w^V$W~sIr zuSBSoy;i5jwV?F-gC9Wad9+H-Du_`f%#-1`Tq4x;aDgbo5i0^JO6)h2d|dnW0Gm~? z3*H}`y80Q^P{636bOuMYz*Ie4!^W!+|5?N(Yo4~~Gi@+~Mn^C9Sjz38LCxI4lx`W{ zvoJ~4+msNIQ5sijew1#5+TevA2dNK|j@8VZa=G5?64YGIxg6Pa!~~TB{U7H%RJ!fR zr}j8Xz$A(*tpV#llS%jpM@E>n7Nf$_w!&ll68DO>vOLPkSIw57V_i*i8(tKBVh;tL z#=Qf-c<3`(VxX_bZ0TazX@Rk7C@4APF7kp1ues z=iS-_JXh9Y;HN2h>#k^1gMjEP_RMKnb@s|iTwj6}xRKad#6da@cOjDu7`0Sm&-<>w z!K@AVzSZ{y@qC-y{NQ5tRvlD}=sl9oThhw1>xEa`UMFD|{fGM;-2(xJ>mv$uqsmw_=?A+T_O84v>b*MXPVUc;+ zGhaZ~dD9}Ng)_H&9F*u*_CV84Uc?XnjHwA$H*OP!<}UjPEL3>R@_f4jNP>9VblGVu z1Hm%7AKG2G+@{K^WuZS=nN?NtfFSI2h`BQ`Z#bH(HeVy3>}B^v6>-L>2hLHLDXjyu zv?1`(Hj2iJqrDFuFId}x5%+ohT3s>KZEK<(69I>y-OK=geE^j--(KW@_qnO1H5^wN zOr(1kN=$@dJ0nVS+Ye+LUmAQUv zW^43xAIzASJLerhDnwOsA(F^(BzSScqXrX`zL9oY=4we8%nw!f=U|_nY<^Px)qr%B zya~sH5q(S+cI~grruq8%ufRX*W96lU;y@RO97KEvR%MA{rys!%2Ywi8)WGKGn)w%} zTq)C;VG|x>KH2;fm>~z_1IDLQoo24X4Q`>pL?-1Z=7nYXp^j;`pSKXkA~zJyb@&4g zvbtn=p3f@sYC9bGCU5v+*uy0V|KG2u)SR%|^vjFA zIn;yZ$u0ab z-LLeE9BOnTd2esiw~aM3G&{E;+ecQ$s!G!F{^r5XMQ;S{7&yflWHSH2U5B$Ac7o7_!PFve02!x@+c_T;CX49nF>B?X@BK43?wWSML^8gcEdFvtxj(*j z@SMsz&T+ssu#fb^`flA?yxaIU1pbU+^VhnNq!OCHX|3LNhsWMRnkgUF>NQc(EHHbj zB(3^v>k1v#)KpuA15=C!p}q+iY;b7ns=1d{Z%+5l`-D#&zsH(gX4C7u(-egQM;q*n z+;jH9mI@$7(hYvyj*a1xn2|U*Fwd&e_k993<@X3KlQ3@uzRQ5o4^9!AP43!4&{(Uf z@-%SfLfMPDY-f(_s+HmvzV=rkRk2hL4w_gm3K;nzU#<2Z$tbWod@Is+_EzLK6|3#z zk=++=xmIUCD_it?7YxG1zq8n!jRSU5N&wWu0yUQ7Ts+@4>S=hXEM?BJMYuY zH`+*1bYwiqYLFzBd?eI|ptV8%8}09KrJy1Td|V9HMH8`@H4h{%v$}(MM?FJG^F)L+ z*!QJN_Brt{h}==sTM3P^mIf(#0h*yeU>~-*;jRQ*xlr?=5~MWK3a?9rqX+peVzoup z1-^n9;SH1BANoH)-Cd1-=MPq(KCNt)KX)D$M|$k2U?q@enBW&923}8LVhF`=D{Be+ zkBNv6-W%~NJ3Yg@Ra#_hAUO6l2{C@zW-|0e&htQA$+Dj7sOgI|KYobxW!JF|N&g-g z2slS(`FzhZO9gS&s`p~mO|(1Iaj0dgC@2FvV7w&>>uN^f4l)#FT4j}@;}Aa8A?>9Q z7+MCQnuL3pMjIo=K*lWPhv6(ny6a;0QC} zZ*I8(tk5cmrle3gV&|0* z!&~sPpU@dH4B-4<{->{?eSK&Jc32^06dheJ0y*VvhTG&5k~-P~mt*V|&k8xIq;Sm= z)Q&$kT5Pa^CHMMxLn7ujuqiZy4Uq1~b@gB%B~=MQlp zUm)c^zD=z3&TDz4D%8RhXDJ3-7tWac3?=7C*A!W3mvn<6C})q^Yk@UpE~op(x<>d` zaJ8ULTbPV*2S?#9t@Zi9ssDlr1lO^(K!Z!0*)h*)X1!WPVOKyyQYodAUOibVBgOzU zUad$?NFjqnNoFXM;ogQfzglOgzLRld^ENODG%bEax!blyOPj?P*UfB zwmPBml9(Ncd2s}sfC%{gkyzw%$J`DsQ=>xqHu&&(Y^F9=E;xtL>bWpUemAJ*1zU1A zOi{`2o4Fz%>w;k_au*YtC$^dP=fpoPq|qekHjzqMF##+aSG+n-{Vg{}`7M}-W_c>1 z0L()c>?jyTQp3p2N#zk1HnM4E`E)OKcgo=db2J9~pwE~kWAq#t{5mv`X=d=e=($#- z7O?~^Zf>r! zXJLZcIm!xk5Ri*AuxiA5*^ZhyV@&4kt#6Lhj19X)d&B~z?~ia{djJO@li?jWi|Cw( zy?0#MGl;&HYQ{V`@KRC!40nFA@sifsG|JXQ** zU25@yt97E7%!j9#pFrQVmUm==ABYuq7CS8%mPkc4^uZK!Pq^cXuaoh;_7IDl^Ofk*2GTsR znv?$$;Vrl($+I0R(5R=#-+<>VT}eg|Q_PdSu9 zRIKR(W9#(L55qB0I-JL}sfpv6ITQsB;HS>FP1vI<|IMW6=fA+uyFa|H=x?fR9{oA)`RfEl=eF#? zZ^t7kvrefJ+6>upVCLSAYt;V>3B5s%b#LgXZw@1}ep%yH^c(1)RU+at5|r-lY?SU1 z+=ZF-g;!6x5|1IQP%T=2;hFjow+k9Ta|ji^poUv!CDpm)(Eu2{Tsc{iVl_q}F?0bu zDyMp^y+Mu>{LN5JQ_{3-2biK4+>7A}&j`M9V5>X$P}4}qG+B}?T8+^j=8zv?@=LG} zGX<11jh&9r;7H;z@~bCXcX3~}ZZ0vSV;C&-GwXeOd%t`gz!d-qHUmh)VB+S(;7ZB3dQ32@d?iw1}!a#^HgQ$x3y-_8G zwO5q%n;hxG?w8Wft%;_0xG$vRQ^8DVfAVRZ=PI&B!kLBsL4uvI7p@yJ8<4UuG3y2P zDPm=dmQy+0j?nK(yIV7Z2}~ZXRq&S#b|1=7U8LZ7yqP>M)%@*z zTaCnM(YB^sQ{Msm?bz0A7e0l_?Yj+y=?0D~JIlSb_h@Qs`xd$#+V#sg`X{ydsxwWM zt!$D_+li?s@tX0B(5AEMXu;LWL($2lxxUQ8Q&=OLe6;Nf_L2(K>8?lC$)+QV?-AH5 z%;MOj%HS-M1s?)EjCk)l4;k0eZTQGG^_?^1*&VH+y&z*HGJi2 zT|3O|`uo+)s)~h4@XmMP7@?}gUWa^dDDQ!1ChoIi1VOm~n6TTl_Yk)1gwL=*W=h&`|DXQ}-`g`fsnY#wvr8daKOo%nh0Q zv?{}@q8IC`4Et$S{Pn6+MZTRbZdHaC>n_OLd#en=ciUx^+Zyy`o3F~8)PYE;JhxL8 z{^eHLz9PSx3$obxCuODK9r0zmb2E0z=G3Tis|=Afs?;h&aoT6(9_4@ZzRW3Cl^=Gu z95p$q_f!APBg+rU=F}bXEzuqJRUa=0P2bLuYpM+HyZpO)WTjEl+*n#fca)RSTMaYw@5|i9HiUz2<=Cq-hjXl#6Ri)S$0BXRyRC&a?Xu!D^coa$ zQAgRf9@*&bqT^d-r;9SW3fEQ{O8qi}JF46llx^#%atg09jP7jo4ePD=HC`%m zyn9-^-xs|wpuIBB_EvDhea2Q9`I3?bk{|g~00q%tQd2N#D2&1>lAJVKohBiCedV?Ml)zOJxkBg3pAfzrUmpWy+*IoB3eRk(c83)meWdF zO?9-E)>8v*Bm-@wM*4t0q`y%!?Vw$>oA%Lu`k312ARVT5I!4Fo1f8T#I!ov1JYAq3 zx=dfwHM&9H(GPT!ZqXgO`=5%V>_5%o)zEo zn$34a@igcSvUL(E6WIonC6V&Yb+*GrIv8Mewl3l z!Ei#Y$o6xhBGhu!_KKLbk8FQsm4?1eb}Dl8r5ERsT?Fy*7y+_NW+`$6rigXzAiIS? zrhsy?YZQw|k=@_LhJ8hL=NS_|0P+o>Scc}2-R}&UXrsM_uaaSry^To6X{^0Fi|Y@P zeJCq67g@<-R=u75RFQHp*}upNhxRYDe_O2LIkImSaSF)(pcv>O`z|pskori(z;WuM zVhx^IN_}F)lv?VO?DM;#is)pYDU8l2qCV5atQV-yi()g*P@g(VKwVp?&o(h@F!ebg zHp7$p++{eSuB5ORQ~Hu(fQZvTidcm_o@UjMVm!kcNTir74kgW2JTFpKk>U-plnW{9 z#lU$|9A-ms9>~VV%SdsPAy*SQD2V4M;2-4RAtHN|L!cPAN)Bi@IKP%0o?*xVYsg`` z*o=|nuz;n6E+U6oF=aA2Y$gfOi}T6h6QWjD(BX5DP6avq%%TA~>zFHMZ70X6EE@U>IU2k30XeQpj{5#A;zUwEQD|+6`lYer0aK~p zcoAomU%#nhiUsw1L8J_vH^ovHsb4)yfh_mi&t&68H|lqU6uye>l+mwSEFMSw?uy9O z5@%Pj_)>BnOcc*8BXW*oDf)bJ&J(jD$ayl8aNBpt`B|~lXmWmoDUmgh>sSZ3Pax;* zB62%9cZgMJ$+?H63?g!|5L4!oi>Js9A95KYro@nofQA9f-6dP3Y@6sZL8P;aTwW3b zY2>n0%%U!r29{zwhg@2T;u$HIvnUR$d(7nrmKAV;-!3!dtk2%+$ z#6Zh?Zhgc+2)U(-fgo}->igfx4OTLVk#c*F-9u-P+do(Umm3TttX2TIU0{@1$~Ccg zCAmEi`STLF`$C(1mn|T7P=*-GecdOEI19;r2{EzRN$$;J3&F(C#H=Y_x_>Dmca!_? zV#*NmfF*%>PauzAu@sRV}5BMv2W{7k)kmp#2Q$pnVoLEYCz!TDfQWwZ`g^2SJd15kaWp={z z9Lv(bN?yDe7(reN5Tt-)@=}WJ8Ao1`EGzVR^3t&^XCkkBlEzDyBk+nya1eQ|5d)XW zYnNE+2zi|qE0vPhHR92)73B4kNT(lp^MvUkAe+41#Zs;0Jy;CvB5$Mc^W;62D4td4 zJp)|9yieW`5~5{Ip)8xHD>|p0|?{=~HOu-xWr;4`!g}i@eWdpLv2lfZdj3u8w zVx^yxk4DUDBOgJ``jmV|i2*hFJTGQlA|InJFCiar3?1r2KA%Xe?s&L0UTfAK7HYMP zif=R%?*-TFCu#A{iN0gr-D6BulnK7TFj*|=TR!D9be5MtQk0Z~) zn!Gc>N-OtTos2k1N1vF$sWx{+H|xGnT$1>r;vh*&0|R}1k8d@E#%)%7uX!oJpJZg} zXr=bB*v;ZHk}}_!)#YRBZR=(0Y3pI@ZtG_2YU{Em((tzCuVaD@(SctT65r=m)1lC$ zEw{eibn)!Q4GntDJ85dN9^5pP_=(Bu0&k}nQa*?qo^llwgHw7Zpsn69uUu`~x z&9KtEy=buE3z9p|ov5ZIIrn11FBp6VdIx!JSgYR6XiWYHj3~F+eM0(%m;he+sRAcR zudj5D3EpxeFsY9CSxXzn-p=UAIFmR-w_0&P^U^TlXC6LiXwKN9doJM{&GPALlK5ll zlR5Uc4REq^3%_OX**9YSo#E^63|oK4!`nSBX8oP$^>;L{o(W!maB_;{iuuGXHQd&A z==!I=5MQr}i6y>RchwL&A|>R9{hC=V{?wPO@=p141G`IV%TgtilXc#KJ*aVG#^B^+ zMWg18gCs9VB}jlQH0JHAhRP8g+QEu0&G<|HaH!=>W>Tbq^CxsRXxAq&$yv99@oaeg zgJEkeG|$f}&*?alMLd=Vs!-iensF1JZhYMOe%?`Di#=V2uJ7lrb&Gf1$6CJb4)NO5 zW^Jg}Me#RH)nbfXZj135iYuD(??|HR3UJ%EA~b0{W;u9z%HC_8p~HWG zX7xw)635LGzGza#tvu%Gq?y#jWYFe>lw>I#qZVhivc^j)tst}`!r=@$XA~R)b9W_>v<3qulnad8`5adEPF^Yr{K`NwECH} zre5z+UQxmIHZV__KLxKkq$V1?>X3D)Khflb7gDWMF`vUn8^BBwM^gXpviuCsy3o*! zhatZ`(2N>QG6!9$uS4I$o11HWoIF1cUJ0#r*sL2?e~9VZ1pLezHxA*kO!M+*qd(Rr zjdI*QRMK}zW9WJ(CT2C_T~l@w7;oB~g0zv`DayaQ{2>K6-Mu8!Oj#1V3f8)CB@`~D zO>t4P#QrI*?I~nh8)>ZJvDOmF3YWyYPJS>T_M)~jetX)cr;1dHiZ0iTkuWAdR`kkk zW$_}Lbw6c1RGiWj4kX!xPQT^+8_+$&?WDKTKT6z`*!ti1H6LUf%e{)Gug(@th|Iwy zAa9;jdHs&c=bX2{XS}Porm8A<-C2@|3sgg8hAP>);>h8Phcq#tL)b(u^Img(xmO?0 zH(mM$Z|!TH^i=x?i(RhNgK|`h3*={CNPiyE)-ut|EG5uWZzRS%8>gl|TiMgIL zDyYO^Zs@wI7k#3kDGn7r1%5z9d?U{(<&w zV2iH*woKT1l8Cz#Fv{O6HfxqXkNBu0tJAfi!o-S8*RJ+xro2Q_2ZxyH^hUk4=Pr+t zQ5#z{uRRLh_>#2`Z16Cng#UV9Gh-jg6lbTUPIIIBy%XFgyG)MU^kwj-nSpCz?^MjT z3BK{ALYijI$0V~poM|68pYEQPHOFbRlhU_k_ul$eGyO(zO}?6>gKG){YGy%UHpl)f zpQNifV{of*O?9boP7U5T)Y=|G{YIy%GG(3MsCnUY;w?;ScI~QWY!1mCTwZc>_jRbv zjX7!X^7ot@T<;QiI1FgUUS(gs_Os^23ncZvE96St)=UpgTcY?$Q`L^igDmW&`4ble z(wkXP?o+oKd_MGA=@}px{?HozNIGiB2d6co`hh8dZoUq=n+#qB?jHoNl!Bn2 zlAaixubC!DN^@D)ZwtOjQ6mb{`-Yt9)Rawx4Ga7m#(kh>_6m}Py{pQl)8&E8;wp4g zvRglad)6&&Ji(g*dz0(ky5T{u&8xvVCCboHEG@+NmAucrax^&5J(r zwI5u9`KcA}!8M-+UaGsEVKt*7LouU*c0~j9G$^L-qV{%bbwZ%F zF>`&$?!RkZ{$2I*?-*G~0DrS%QDsVP(wdM@_G;$cRl_%uDXtgxW=P$8R~x*~dCeTQ zG2DG*%*Jq6ZSaQE&|wqV|85GIh(5i^%&O6<@ipHZGmg6k`cE*thd4X_3w-+!mcIZCoYV<&NTf!;TJ43)yV_#dAd&q&e0zOTu< z`v(K-Gw%8dm{iRiY;`b?q%w7M+%?UdXPBi!*9VG1U8!A~z9qY@^%G6SO}HDXikpA1 zWR2d7y>@xVd1zr*_qpyenbmmA&X$3*e5EtHuO9NL)>J12!!wiid9$zxTc~i>9)|pU zml-BL>GT;xWzzMe9~5^qBX6sz&)^k5kxZ0(dEwn#@RAA?H%Q_gSZd#~x-!l-(GR}% zwyf6dAK!PY)J++oP;A${6-g2&dlhV^&*%{~yk_(dfzQ>E%=>OuFW>Bb9y*mgFMXTh zvSxk)Qeg{H+9NvQs%w`f=ViXBeW>`_q^^JV4VdM8%FWZlYQN;U4?bQaY<8SzvnLq` zTb9xp`L4fZZaGFI{?!n}SDIy~V2En{=QxC)zUDJ`!IggX-)d&R7kFgclbQbm&Fed# zSSz2H;H^=_wRcu(8x#JfS$r8he6uvLu!t$y2*Y8|mxB$ftiO7qTrg{;zB0dk*KTX8 zhX1CM#TC(?)e0T>v*Enr^^wb!Cx%lt#xEr!x)E?25`Lp*Ucc^p!HoCKj z8@T@Tt=nm>iDij}i8J6L{YP^mU+TB-{_V6?X;;#grM>sg^?#n|IIf903`YAEl4J84 zYoBKCK`w)wn@Qq+J|K3Zs(D1L@J>9ZxUN~6jmgNO?po-G#*k}Qja-mmiE;tUjl0Ro zAyIO-^UQUok?^dZ;=y2%2Y+^lm@xdoN&NuwvA8UsR%?%9J}h|o?>S8aoC`F| z^3v8OAD%oySn(OjpV4InPickS54>Qw%~puoA&Jjol^DDNMqf&N*xlY~w-d8d?S515uq!FyZg`Gi&_vB3J}c)vOsd5s^U2Rlb$Q(;{GuJ!Ss^=GG;hD~wA<(@H#^9h ze6}Qg%BnrBKQq0xuoW+vyq?96two9G_f&Lcc7`~01~%))Wy;dGh1^6F`N1_-0X6-= z*4(h`GdqXSGVS`3OEr(kZH#NdDlw#)x)C$opH@LKaTTO6u7d7(j&>fcsjw|S(vbtR zFnmH_Xn=3RtPi!7Ne5un_ZY2uU)T^;#dS8+Q%5vvraSzLvv);w_Qbn!QT{}tx;^V% z>Yclv8pKM`4t@T@5nP2ww_%4QhjPrDXeSC@e*rJEM{=Gk#eZ|k>WM;C~%NBh4b`9^c z%w(}2+rA}m7M=QHrEOZpPn`N9v30!Xdd(CIYZz9nh967`haZFmL6_#4^_VQRNx9n_fj zT;SCG&ytP93)y#H?QztFwo~=QG^!13)CGPH=XPe?hap?HXx=_Ncxz|C)*nftTM^Rl zbYN)mhLDRFNK%l>Z-y%(E{Ft zub>>nm)2ZV-A*soolN+7#KY{{w|>-&{6tM|K`=-X=JJ2*=JWSqO%_p&e+zEhBZ@($ z`-)F6=%y9x+Tc?_tr72<$*)3n%h%YPf?<#!6&D&I*MHNzcn{0EF84<4Sm+*TroaB! zUx$g*l!W|INGo--GMyCln&sa!H$JPEGxhSaX39t?lw5gJ-5!047AN~%Fld&hFva!@ zEF~wmHC=HVYCO+HZKw&}FcuEIbp9ya+9#YmE&A*hu9H6dz`<S53Ti9jkr*_f}d+N4om$*qRwq^~la(gQSq^){PT zpJ~kb^_b>9?a7`)78@V73V$+y;VbV2uRNQUVuLZuxBfPq?JoUsLuk8b3a2oG^U9Kb z^>;j%IWL1Te3Kc&ahMdN+~%{574JTty1sp|0SCM%*P~xU*yEW6*f*g;t4-gTeBtMI z3;908nRfaYnJ`Ug!S~Jh*c=Y?!y^Ur_2`ip;#3`%{MbVd%YYFfU}D{0MLc5*5e(TR#z|ut0B*1=AnkEGyMxaH<7<}6 z*$0ef@DXZ#^cyzugxK4|+}OE_e_#Kb92ok)S=s-2UH|Uw|M?L7k2PT4{}|=}hVcJc z#s5va{+<1A4*c(P05kZii>tm_^$Ye7sn({}tyWnZSi1p#8*2@a-xwAPfy4PLFLNvk6$Y<2wqey9FjfMPtaI}fDCwFe>nz%Lbt5eq` ztVvp3MM-@2#9fecl z@id0bVrvW5)~r2+99mbmu3_DW>-MZWxb7GpJNc}^;fFOzoAsM<&U8!vEy^tex8Qjbo_w^T{ijK~#ea)- z%d{;k7{#VBO(jjwH!W#e9_AS45jHSP9Tpju6}BYo-LSf_pTd40A{*j8WYCb5A)|(j zAM)0a4~Fa-^2?A%;fnD7;ep{p!;`}E!WV?U5&lm2`{6C&o#Ed{$Rg|`Tq1lU0wTg9 z5+gDq3L<7jyb!S@qAlWBL}$c>h-(oyBkn~~q*bJQ zDe}X}zegU4?2fXEa*rAiH8@HeH8Ltcswiq&)WWE@qH3akj(Ql~CpsiLCOR$pndlkO z&qXhYek1yw=rz%e(L16)iar#5BDyQOC;D~_ALA4=Fh(5{850+i5;HEQBxZWd+89I3 zwwO<1+G9?|oR9e~RvPOS8yuS)n;knYwmkOP*uTWS9eX0SE4GJ&4tqn5(#*R2Jjc^R z=+7;8bIL0cj%})h(0GoWLT8W-D4+#TKN2mbaxr5rP2>JaPV|7>cpTuTg*26W0K~sg zI8P|Km?C(>Cdp#zXRMWIhPe*10jdjVDp41M{khw$5_*d>nw__~U&MH_SQU|qGny$C z2a327c7@pRV&|0zPS$`uGNgC!nd*+6jL6)*%`N-;xn;L&9tw_C@2kmG~Fc9b#xdE)uMd31Tg zn1#?Y;Gzgc5@xYMlCQZz#S#>C_Pli`7!7lV%mf3~48@l8k(&7EVQRW7|GP;##c7G$ zBoG`Xz|JPYNaubMOQtiab%2oq+eA^BXzIyCbM1p!z{n_5mOX}36t{k+1S(^E5Sc6z zWy}_{LMWQ&?ucnwCJD{r(KK(*kUNV^ME+tXVhNVr^d?`bM2ZNZI1q2(aL2|__W zH7Ura)K6p`REV|R`_quuc=}0%W|Aw+ofyLc-e|}m4(%{jA|mnhoEceamQv@(0rSAC zOx=o+n0gau;`g(r!q1v3nr5aZuoT9&5Q#~)Up19^)kN%7Rt>_f`KADsfCG^BTeL)^ z0?3%f&?lS5b+U-+MXiD6dThiLH@Y>BNgONeWh(4tOt7TcVgjICG&n?48731&Q5eE$ ziiiOCXOT2gAgVCcW`^ucyYX^mvPqsHpKAEuj8N=l2x-h1@0tTT&cwXA<`gRl%n=)v z;m}DVG*TUIW@nNab(yW1^ZZX@FZ0BD?$R(0wiVZC4z?6mu4wUCI^L9arB7Zia(@scws5G|~^^b6)J za{@(~QzYmi8lhO6$i~}yu_A$~FS8DB0!4(pXrEhY2om~1rozk}>1(Rc$xN)k99S+2 zqG!ncY*wQW3OVwaI^5lE-SI)=L}+eHin&lJMDk7_(1cZdBtlFO#;*Q~VJ6-Y!)T;M z5Bs>@g2xg!YK=mTz#Ig%1 z6P*y{)ofB zP+Zzr=h*(SgJ+zLjgB?Mej0l<_QzSvWA6=h7#cKm<=J}$*(_?Z#DBZiDf8}ZVJQ6tVJHYT1L z@zscjaiMY1al_)$w4>s3;~vIa#Se@5&yB^BdCO2U4~FD%of%sD0P#U6yXQ?jO1%x(?lUx>LzE z$==C>k~5N@Pf1S-NO>h?amt#MEh*ome3$ae(5}>tsaC1mQ`=L&OzlbiHuaZOCXxRi zucY%&BMz%%v0+9_^>oJLaN=8!g9yYW{INXCi=S-75QgcJjiLYQu3ccxP0Yp%XMZ=w zlB>+A|HII6_$j(M;!FB5V-npOF@|m@ETTIj=Fm^NH|g&1ujpspCHiH=O1hWu54x}W zkbV_r(t{)={g$zke%E!;KQmX;!wl%ABzUmWf*;$rZAH1TftEEblIz1OGMG#^mrhs9pUKT_ z|72k~xeaZc&k`5!E0f|3PFIZqF?Mu}flw6xncT-88pmR+S}e||0o*6|dAIY0RpdT@ z-+ASakakpMO7=Vcys&Qo&hD7Q6GlN_l|XaW2X7Ho4#HT!cgqMU6o?O&+eN ze^oY;2byd76M4j}u@lZPpb(Cc$E^BrhPk*+E35{%Z?L>acyHMtyicB;`|X77{%>)gG{&!aLXWkg*oG*O864sDkJS((<%a- zg=ew4LxBuj?+6wa0bG$O?~r%z8Wl_ItrMyA)~i|U=KE#BTJpZzmaBx{cK7pMX2C!Y+k=N0nFKW-4-1?Wgs?jWCW;QUVj*H@r8d%1^!PI1J1yS_Rkd{4ezKcp(J zFwn{Jy0040Y2R}V2ITc0b+Tu&xXZWM`_8$Bs#ep#7TgGO^SBEEZm8t^7Lxh%F)Ia&Y%s zfc8$|1i+bYJe##r7N#Yq)N#==z;uED^7x zFmU&ZOsEF9Q>NTY%7<6{kT@V8^=+mB;F9n$4TR{sK5!m*dd-^{xPhm^$jvmcvn5#h z0S)YGH7Nf{Dx8~Ewva0MVmFGb(#{x^4@s4N@SJdgRAV>X6kZ3oeiy}6&+N5kD2H3- z3UvSn4J>vX740Hb*Htmr-Iy#KBvp6gRAC{&nRCKXfU~S+sxQHUAIWb;eW0)ghbS(v z0)BO9ZUYV<9B1wHyLmz;)RN!LAG8=Tzq@A)LL>P-6ge>nuXdoopxoxs8@}NNjTK}5 z=wH}x-uNbf5nxaskpDR}=`{+lJ82N=DRAh1JLPQ(oVv49I7@*uE_?|pf%6-$a-v~^|7xG zDbJJo_-++cRDHa+7c|v4|0`6H8i$>QpNXA*XNeclmNp9ZJKCu{2XL)XSx>>i=-5&U z#t|Z*1jp`GG4Sr;wUChDnzdalR&#wb1GO*-cL3fys_Xy&>1u!`H{}ru#+iDd72vW# z`5^`0Y@Egl++DX`I0Vpcu)GiO%{6H+X;NS=Hd6?gC4ERCU!mNG6v`yzdkXzbWMn8C zoHZ2YcQguA!eS9?qp(Flxkq8|LPA;r47tirD6FG6U?YeI2yZ;eFvG#Y4PS8KorYR5 zwnfGOQ{TT)gzm>f%8e8;>hL0AK1Jk?P zn#;~lfmkGXt=v!SNH2@k7*dtHDRLz+A5v6q%R}W+ips}mo&vbaibsQW!e$x+r)1g5Bq}bWLy(k_#uklwjDR$o8 zKvrPBfemr&{Lgbla8ji5QsaEo5c|@8d*yo+d-u3OxsQg*Fk-*bP?gw~;S(A|m3s() zF)J?T3{xkeP|Oq9jPMQ#Oa_mV(A{GY9uYeV3e1GLuvwQViS^+_V(*h6ksZ!r0H@R$ z=)5K-b~S62Us2Ne4m;r`(&B8U@G5{g zd@S+ox1R|gk(O!B7o=N)maPD2OI2PX-HPo7G55k+2Vl@6KoqvB< zX`qy@b%Dwgl+u0qOJy^F*s)Zt$feX#XzoXpI<8Ko{EpaJX3$K9@c>ABx5-WTmC{!C zh_N-N@3X|ZYhtV(6S@e77>;$s-b!Ye ztHtpfvu5847OT5p5N^_#>nPq$V|p<~?q$H8<=t&agFVZK3n2VP`AmU6q5Ry9vBE9N z&%cre^P4|*rvU}>S&tsjSOdT>^o;IvW~H7PxxrNg5Tajb!U@b=y)@wqpj-k#lYXTM zU+yah3VRnxxI+c07@RI*FV3;VQCquVuM2WI%VCKMaz!;P$XnBe{uSgkhBA~q7=cXy zM-IWOEEs#WOIQzZy;k@u6|}dFQ@&3HIKmFjv)9p3pb*L>f&;9E!X*a|DIZbcyXfCm zVy|JcL?{;nbsZ|@=Tx{3qW>vPOh&8s0IUl{-o#wQ?g5B{GjZ%Uhm@PB2z?Lx)sww( z1QhlP4+CtP=%k`cC~%vKt-GR-SlsubR%oVTm#wFT8i0L!1p^gpA>M9O3_)Qix{G$g z5dd+hic>C*Q=X*a6d1=dRGbE4KLbE|07iWv7RUt$Td0_g!%`~Vym>97vbjww90#~$ zP&QEUw$BeKw*!b4te8#So2hu`MJ>bJ`ISm}7T`M-YtlaCflB+QBb2+S`1no(w7B?G z<9ua16?g7;Qy!yYNF;o>68m;Ga3%I<*i=yB2O&C2CBYEWj{yu?VKIQ%hmvq3&>dDO zf1ncGWdl5Rmr0HSk=cRy~RW9$_WBluke2bqeN`gN#;E&z>`hkfB8#8pf)PCZj8skBGg5k3@h*1~5OM_~bg6 z!#yX+j2d1C_(sM+BgFJ)s@!%>t2{-OXoT`_REarU*aoo4AbbO0=u{p8 zIDVWZc8UC~?A~b*b`rjf#s+TAa&f|(0~bztjpo$AH@!+PeD&xMrippzpY(g0*NLwD zM)R4x)BwQeJ5BS)BJU=>3>0*FK`l^D0(?3Wjeqq{vjPBHWcuLGzIk;Lm2%Y9{7oD; z*cf6tS@?bP35omZd+@OI$%Py~i{m7#_|Y8CjlrfO_PFKG@e+nFoh_Hjvg90HZjwvs za=o0Bt(4%Ij@C|s7)O#Oe3-&Wt4A{^k&|4mmvDTOMD~RKX!TCXTy>GOSSlHtS0pde z^M~{1$>$+ScDY{0b4{`=Y*H5!m((O3EtN~FA9dkDvGLLmI&3$Hn)P+wi|^)313Y^5_xW{ zTE0ZjrJ%lQl4m7w=qPgB5;-o%&XUyfgsdejjbki$l~YSdFG~|RPRCG%h1FU5h~qs$ z{h3jEtT}_Gpk{VK;-SxykyG`7I9TTwEpL*;eki$iFykm3DtM zH&RTbbY-A>Pvypx=UiO;T>{DrZ)R3aUes>bZ)btBfp%xe&Yv+=Yv<32y_V23iD&Qt zq~$NF;xm#XExGE(o)+d93TL=OEj@-7(nwnZ=f|#i-xn$EEp@$Uf6IPZOU9l_{F#>h z)%1nlnohMiDM#-q;-g=YZg|#D(4xX>`bcki6dbbxJHS>ey(I(y5%55!wjnVp6QfpY zTh^viS?lGLg&}NPMkB0f(#G8ug3adSm1n9SjT06Yjn~_D{Q)C&vW@Mb>PN$cEnGSB zCvUW&wno~musDTKG1(piNF&QQ&omo{qiHsFz;zI;B9a>q;PvuGtCm{5dWMa6mL>wBWh zwM3lVVoqPz+O~2b&sDYo*jf?;R&p5w7B<#x5ZDrnCJS~f;^|8*Td0=LP+N4gO^=Z9 zrDm}l-(vHrZxO~)C98dEB6>SCWcI{G&5-J)hpK6jnxCPTbd1`KF2N>~1r~Pt^-$>KSr%heeZ=?3o7t>t#H) zB(t@GzB%i6rO#e(3%eD7+ZKeXAAO^i?^avPu^T{kbI8sK$xD>DC9s>PlJpIE#k-?V zsdq<%i?gE@J(`G{ijn#g7uDQ}o}j@m=9iGZZ*p<;?!^6eCrBL9;riiSYHql`Z#t5b zg@tnt>=)BjAYz9PCGLh0c^ulf+f1u(;{KMtZTAgV|F*m&yM!y$7mjMNITBDMd(Qpa zoU${9{mrvWIKFKeultm zd<YUq=%x>sLo)_nt__z^ zi`)|{&??l-DSOBG_1B&RKiXdBhu=*&eaNl_iUK6?M`Q@n==baoIdnR!MbR75&&)vm zu8X2IoIv0N>E$>Fas~(Eb*i_S{i^Wu?C5(*o~L8X&tLI6rXe zR@8^$CS=Zt%t|I=I%cdW$tU#*X44@~;G}wvg^433K6PZ-K6<3K+9ygh-#ME}a+4^@ zKZ?Z@8ACTM{}9t1xEU!eWC5PB7fRt(5Ljo?yw{7A$$4Aiei(Ta~quk}koLe@5SM z92qHX%dCeVIFV?{m$$ucJzPJ+yF_gzF$FVNA1-e!q4Wblcx?`=mwR5UC7$^Z_w~}} z)KWMy>xtdC+{^X3c)?H2`MY3_{C& zA)%RKsER4*t8kp4!q1?Ey4Umck{ob?ArC*+ZYP5*T+vIu))yW_TUoF2?VPxTvpjX6 zAv|l11a3jAaF#ECvIO>G8h3PGz|Sq8M?;>-+{&}+aZ;$4^*0un(Zx?SrePIfNFp_QZwR8)3v> zOi;n#c32;Fd0|#SSF`c^x*qb+p5-IJ!RLkR+Qds~3>-~-q*Y$Q$?5ARUT#W|JV_WW z<)tc_B<{p^lUK{AT;3TYwgrpLRySx2Qvv5|pM6Q>&tEFOkmeX?QExGRW#PZnoQG!b5phM$DuM0E6-j2ADu5X8WfCm$WiH2Qz2)Z& z+mc};Yn@&`t?hNoxYfdUmjPM(&apa}d0-Qli6hq3#M#NwnOXWd8TwtR#wMK?>-7aC z8Y2cxYLlv$e<#M7WM+t=c!aw0IJWvQ+nv#cd?8jUVm9k1S_2P3)I_tASV`b@i+p+b z=AV5&Gym~%4ACyJmS&Sg0_oOPQ&`1viByorrmN*LQLjA@ERzjVv?fSn;}fNt_%SkT zi#%xv6HBb+svms@TgW9914(kDRWCo6Ah%2%Ew_av%M;};tz+cFXf6a%-6DG{so2u! zV~K($-k`NvFzRrg8jz zEu@r8zA(%dOgB>0atk~$Yg=Za+nH|09brVO$}hHvdYl*l^Oyoc{YOisQoSthJ+(}V z{^m*9a$W54dJC+n7iXq;W>eW1fEv3MllN$EX3UDk8*h#=FE4af017gevi0^^9(~)6F)k)KZkz?mJdGX{ zI%|EvpRi%Jar_7_Ud3{ru6;r)`RYe^;6aKP3@CAMvPZQz&Mn)PSrz_8UM_R$u+~vJ z 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˜_rIj z8L4-DQ%9ziD=>Oy-eVK5kG{TdF7HKdE&ft>v}`DCY|70X2TlxDt-w;JL%5W-X;Y@b zGMFtP({;Z+I1+Y_sKVqn#MCs1d(4SI!RN^~2d%rBUw{7-E{mXJ6Xq^v0DP%BZaEId zm0=|mS8jJ0Vph2OmtOlR>}=fhNK;WC0(7Ne<#p0|5XX`Ymho1?peEorZZR11k7O71 z8RIrjy@^#2%tuELPbHBpvh4-6kjkWd_F{a@*$`$ zwiFt116Jps!bCH>H&6imMcgu^NG&PqKkU>9%R=!sb)om;I;8T0?H@ul=m8JPaX^3u zlhyjz51>R@V<@h{A6fkN<$FeXsGwhAG}ra1-h zDxScrw=~^g4}JF~Rh>er+_v+T{{?5w9Vu1SyAo-$&~ zX3JRm0-Z*NONz7^=8wtXqX2VWIK#zzAMUutx782gYl_aQ5 zRyGap~s>Y6T= z2hBQ!u{v2pM+?mggE`@(ujXfDI^t{OZRFJ#Q3+`DXv?2Mx6bw#F z@0y)toJ|SSL&9(44}FKV1E=f+U+tovk9M}7HyT^|r?f;n{TmjP@86308=>UFa%=5X zUjvU|fsG0s5=XTHuqwaxM|G}nELRDjE%GNghudyDG1BWb0dk&R;m#A7a9pprfFPZM zFI53DH4Plq3s5xCm$A2)5{VO#8VPwGHqG1iV2O=>mUtM_*ou*xd--VFmMgtD(Y<@!axYj`y72CN>POA(^|KD5epMsE2JMsr4crTLqp9YwFqIZSr4<{HcQ)vrAZ&5gLR8jlwardaXu@!L47 z;O<9iiM*BmZ;w&8b`g;9YP*_!tBdPYe>qz zH>J9;+6e|qqWh?#Zrzn!mJZ@tv0>>2bFQ>x-HoQ2zLYmPP;@^AsnWWL)!#>%3fM zgp6%2L&D*fan#H6@3-GJYYNJRprWcqP*SW<$)7~A0-o48cwz`bmDdOf`0u?}B;>^e z73~nl17R3m{dM4E)`ZwEL7F=U@(UO?1785w+rj2O0Fv%2EVS$0qXN#U{Xfou72p&U zH66l97k;^`iUOY>yd`TOMdJ%6F%f_5RHnP}qh|$nHz#eQq-+mD1UMA&ya4dsQZY*a z8FjMS4`wkjk45VtQSo*#lG%g6&{ovV2|dvsg1m`AWkpbhj54G%Mk8xetprM}2#Sk8 z3@V3%Dj-EwW+gj*XWlV2i$Bi`62=UPY3H0#)usTl)NW6#z`F0i9 z^gGxJpA=Pw?2R3)%qN}10h?2b?Z3w<#e7>M#UnxPYp0ZO_Lb<<$Oxj>lF;qHe?dag z5E_8m=OwmmEU#h~h_m?mE}rv{B)z@kyTf=2yjx>1rvC|0>HD8YqV}eJ3hIj}^aZkb zhhLJSvT?s=y5EXmY!z;K9=Aj@;9N@y{~1(X9m}scFObNc17DX)9qm)#4w27e7Uzo+ zZz~;7Q9to;AIf*3U-m2HhuOe8ZTJM@!}v3?g7`43Q1?Zu=D(H%1`dP88om!nDGwYM zArb&xJ3}YMRUvSl$YzI3$X1-KY^@b>dvI=MjslLxREbu3a+33x-E5wD~K-(E#=JJG2r* zI`9W+S$v^VWcSXd>B_Z9YUI#EyV_rf4zG1BNQ9#16$&EbM^`$9_}e0F3itz3fvTz? zN&~&=sF6fvP~k#Rsj>XZ|4>To1oWL>P@bRoN`b^N&gv7;NmvKU`HEi2SMo5X_<~TehQ;lHCeGb+@)7cL5!0M}vrRyDz>Sb^Sj)~+pQbF=^s z#1pz$a-HvGk;O)YHmZyVmtG^-GP6%!HDyQDl7yTb6u}Pj3gRud9Zg2Cq@XyACT*O; zb$l{92kO6+1B?Cv2#b$$1bz6zvnw#j90e$tBamsZ18{Ogp4=j($=uGdhk}~?6;k*% ze)TKFGMgg%ons0T#y^o306iX47CL9~H{-ECpj_3VeD;MQhwgL|7GNib2 zj~71l)S%`{`x~FUHYm-<3kW)koR{3)-b)^RW3eD5sd8EN0XPyM@?jMBQGZMt&`57$kn7I1{GzMLNa6x!opL&936tj ziV`|pugjo<%IzK}+euNN_bM@kc?h!_e5^^W_)iTYrj$Sh)v6x?6_PSH#`M*mM{yE_ zzgCnQjy0gyO3&N2q;?|O+BzuJ0RHQrgKI7hg7R=DhTu3r&x<}KJm0`4Vgz;aK`DE16WhR{9jh1Nrl*+ zlSAm>n=kkXvpX`M)g=J3)0oG$QDC;}Zjk`L@BWJShN&;9Epu3&l&H(IT-W5lficvt zdK7Vp(+8glNtWJC1f|Zn!fGh0jH4%@D3J@Wtxu1ddM9ifGplTYyPUzD@5R3zbMQ@D zU}~KlpHE(vkse0U3djunv=un5E+oy$H!T&G-9Uyw1~HUTt|#9$5~vY+g^jQkj2D)O zDCLpA>;XJzkbQ)EX~WTfjy+6gEr4`odi9-6#kT0J6hXV|TkZL9Gsc{~qZ2g^D42uoH3^nltB{ zv}s_;t1nrCov1&B7Joa)3Z?fPKeB=7uSjw~Q#{$=Ae*NgPHD`!nhB^t=y4CQK)Xlq z=Ns;-@odh>)Q@XXO`=f{hU`}9oc=2?3^qmk3{FJJ5HakFb%CvA6sm4z2^qU8Rn_5q ze|4s2Q*2+J8zlA`RHHx@d%Gd4uxxC~Z)$TBn-2#3*z}rNSW#+kJNQ?Aka|l_pbT||D%GEZOi>0+$C>7J0^)4=K7kYK<#W> zI;~^*?l(3Du=c@d&dTsiFQ-}fK(_+RT0I6)kBkAWLUWsoZxe*uPo?V>0r+PC1hX>8 zGew%!Sm=Rn2`Jb{cN_~zS<@32SiEuW|E>QwmMtA4#_s8ypy?v$3Gokl5@3}60_8GQ z`w~H{A$C5->dQIT@^t=jmKXm}-mdsVIv(7nk?I9hqx^UOpXbz1km$V`1g_?snRTOr zfzp1dynOyObNYe=*R?*OGMxHIs(02+W3F@$Qf3srGq4sIV>ajBvn*yomu0-#(D`vb zOY8TJ8s{RV=MQ#UMrPq|TC>F7Hqhnew!31Dzu5c5*kUB{gi^);C48a1m*p=%un?5& zja4~CW+7)_LC(W3F{$inb|flTc4eAHvCWZ_!ICAfQhP6@=&a=5K_;c|o+V(0rA-L& zHpl$5FP?h|?W2C82_+D^skjBKxuDcvRR{2CZZzt3hE;G-v7q0xk3O?16!VPgLP9d| zMC7`JTYlW8>`c}lZWD^f=ZqRrDqnijGA_R1Hj@(L^)xbAg7^3sMN%eY~yK^C@dg7{kHfv_j#!h>ol84?qA&XV_`{r zS==Y_sQp8IcY|Gf~pQO2E{Jk6=b2IRWTBo6_2NvB{D9?q#%E z&^{<-)?Ae~8^Tn}QU6-=KbAP!FIO5%*5vC}m9@WT#~iY4sjvcc_npgUTE&b@9#5NK z8edy~SABY++o!3Li|cTmC)Q#SLFj{w_AejHEZE47Z6r+?G@Tt=?CId@VdiYDsF=EX zM%*7fo}6;q6M1tuM(~2%9G@Ogi>;x=QD(@jvX9Obm*z-km?QH_c8z|_ebfaC1B}of+s3Kw zh-RVo&KUFotpkt1j4RA^Oj{&amXEg*({~4RLUs^S=Z$&V>tPv)ZKcu~S&Wp!2Mz~! zo)6J(3FOdV-yYy-pS!(ozXH(=WhN`eojXwWJ#-rNLde9Rm2c^}oO)I#{kY_hrC~&E zknO2N?QK-grn98){@V)G`ghJ+L1$s?5Gn}uE006nmW7g*o;`d#lq`)M{@%;MnhOB$ zeV}!yyKwpN@ktxvtbFWy!NGixeK8vl97m zVE%)EpJtqSCF29LRN~X@(EmZVo$c+)X%nWT<{w}!a`f((RnM_&?y5)1c`^W>i?BZs z*P5+W=wW>%Wd{nhx7OM+epmyzK=EKtF9pT{44?^_1KZS6_g{V7d z10(pVy7`!+vFSQ_9wM1yx;~xKu}3s7Vw$5Q5k9$K(gmhxJm@wGzkf00fxUAuIJUTM89Q-Iz$MG&OJH!-!6*mKO8y0JQ6PL|3uIK* zqRFdIF<~`jX$cAT;O=ua;r_r`MZeel6N@*t6b2NG^t-nqBFk;Ix65w3IaAX#h(5FZd;lyBS( z4Obw&cUu=H@ASm>A4a_F%f$yud0~tLkw+cXi*qTJZxi;m~m+{`0Y{-wA7Dn4w8`ecEnnyHR3 z$K2g!v9VA(?;1dwpGrt^9%-dbJ}9OzgSAi(B<&HZP(M^lfm7*F(K*Y81*)aON?m$M zKDIu}8J!tbT3%FJP08}jj6!H2YM}0@uIYl=*gmvor}VgF;89ZiE{>NCyDlLx_FLQm z6|8NVxhwAIW$Hy~^6kr)hdhv`zKFnsJFigcXC)K~_2Lt7)&(FcBk>RkAZ zKK>F2+kGDm)x7=GM?z->45lyNf;{Si3eSBXvFars4Ne{d0pYy@6_Y2}qi{uEi4L^~ zDH32b#9h}Fu05A$m>$*&-cr@`!Vw3ucA5DJC7txbP&WPdWkGxyO6TW_IM5;;T?l2|m^2IkzU|GHrR{ zni-g-6LO00GkWg)KKJE2zxPD2F~~J*g3KUgXnEoos6g@*A|Z#Yw#|^F*^Z_{eILt9 zzSS&fX4lmhl(lI$y7TAtAV)wGBu;_Z+kUi6CDO_q^_yK_gnL1R&~(|HoN_3 z$rwc8eb(GlZw3t!$PMeYjT~SP0EKWOUi?KJs;ot@gpM=wUj}VxcHymR1J>;wtFPpo zsTPuVUQpuXs!&cDY&h#KuE=v5I&SqPe13S4$*-q(U3L314x|_Qy7Q1@pC4{&@H-U+ zc~``tR+w70EmMCGVOvu6kKplC78@o1S`QH=@nl9Rjt~rRYon(0y6itl$N`0p3JSn7 zZM(Lnfz2pEBy=I7ViEMhCQL8;@~s!S-|s3=u=OJO)?reyFk7+3Phg}{yF+lKjV1-O5J^Z1SF zbC}BHFj$ri4`|?9%0sPxl)1@JCq%EX4jE7eua=DOlsXSH#4foE8!VmwVIk5*rN&eA>9eI ze&V8@Hwt1^^Q}gsdrW0Z?QPoIY54$&=mqRG+<>hJ?(Q#B?*>IDtLz)|1@ELa6QwYd zAC=ISE6uQ9ZTFM}1YYjkD;V7?tMZHbw6#~3SlFDZJ^8PK?gKcRnrMh>o&Sv+q^RI~ zX!gDT-f;W*WtT}=(Dc*pU9O8zW4Wk`~Fd$(aMi9V-i!e1yag*tsrg+ z1P5q^~*`>=6=mgnrA;7#%mML%;@1Mg?LB1!A3t=qxe#%IWWXGV;P#cs6q9gGvBVV+khloAGi3N`*{b{~9EHn9%6pqzsrakJ|n%q{*K%r61RE6pH6`rQ5QhG;F$6%cFPDh#c-XWI!v-aF?S@Fu^vaY?_Ga90BZ1K>hot zY+Y9#``p|EvX44z$PV(bLWhLL&l);VpJQBaRTe=rMuWxNzzlJpl^J4VU73lanMb|i zQsNJPfu)@u2xA4}Wv=bxtKQQp;qBpghQy@^+!3X0GzrtBvt$C#Dl076NM(TRfqU0{x01NINQrqEyLO~7}rozR~pW` z)rza(@(pTOb9RUD`cahY{06zc0i<{+=_#@_g9Cg;h{Bg zkS&}xuGi9No(@at!G3{351)o~bvJ@VL z));z*eAejA`WrBs!Bw)KO}7|x6MOw*gy~2=@s={PsM}#NCR7?FABPuAaDziBUu10a z3aMaNOE9%jjq1Qc3o}?YG>&H4lJ8ljojcHm<6a?0waRZqohLpia5v>nUc@MS6u)lqp$OpujCoxU+rW#U#-t)+0#r}jJ~4|L}tcb zfZm>t8?IO&6i{r@*VA_{oP3U^a`DJ2vVTgabm?NfM(I%t1lau=&{wMzwe+l2OS~%F z|47M(Jf4fKeB7Bm@16y*%wYG-E`aNxuj0SC{oY*3oJTIdsrJKai!rLy3CQ*(a$hiY z$AF8>jXIOj;`<$h{-`$tZb!B@&i#yFu^h_Cj2hMtEbu@u;cLS!$m0w$9{_hjXwvC~ zSX9iwWQ)7b7WJI7^dVXtUGk;h)EHwqNfk@#>We@2>elxosTS=@pO>s&7)t>@)!U7& zfcIf;W<3qRJk1kJPn7f*R6*%i4!Nwpp;hP)gMli5B{(u#)_sV)k}CN2CRp<~8jXf4 zc^e&z1U@qAc_l@tTy<~86=teKT`{9jea|u~M~!vu@Vr(v`@? zKiI<_8s-b@y5QXY%21r5r0{NwQ4yV|Rl6!S1CckFGJhQkVPB#dkDf3*?|U`yIC5-DLX(gbWv@pHF+<>=NZkAlX`w)N7uJE(Fw^Y8k3O;;oa?ZC zH1r*^v zSX^-KyBul*=j$>EBG%(Mm|RO@>UCH140qSJ zAN?exo-4{nkzlJp>9vkU&;@*+!=iE6M!J9@Zdp!KqJHA)8qfBoa$)1D0?QX?56f}_ z7C5zRay(}lH*!)yl^>|OdM+jmX}D%6yZiAia;u5cR3mN+#BMHvn)3Bg4@L9}e**Pf zfZS*ob0aNZ=f!LxW%!AVrzpd=lR%bW6SDdj<};JPx`;B;Hf4e`0?}RMK=nEmYV3b2 zZEb7rxww?=a5pHuEzh=QpC>=On(5U zbtMQ!wZ<4qGp?=ggB2!yii_4usrnmrE2sz~x47uWrwERwmVfzwYZ*7*crp*Ue%ZS8 z@VZCh&XgJ3k5sXQ!)IT&ZP|(9Zyz?vH=TT^iJQ|6VJoge3!A{SMwXJ^XN^ZLfV2fq z^?u`r#?U|xG((>)@KJAY>(>*VA;ymu)l(fw#N#Sfd02t+hie+zeu3J>dnnZd_SG6{ z8Z^JY#?H(550n!?7h9%2X*z>P^5Wx@)+05GGI|ttY_0g1>g}=$y$q&7dJv10|NH;HEn`iJ{0tbCI7SAVHc2==oNZzi$ z>FU>e_4nIXuYMhMM`=CN3avU_m-N}n1lMk} zCVyCS6=0DWkqkb3(oTKAD-_%Bs`*?lx(B@M!F|twR*lYEgU6kJstnO?G-TGnGzE_G zAXp3)O@JsX^Oyg~#s}86WeK=6f|bN%R#m^tX;efNM5=nCmoZy4*FRV_HhuLUvE?Hh z!OP_x)sJBi596b@LXM#JGzPbxQ3r$*a+2M|cqkg-Y(sAETe8w5XzB-JUUQ?AKwnzk zMtE}hHC;4e;Su?8kAmctD#3_(CyTY9?TkMp9%=lTmywJdRSXQ1pcV7lHtD7d00F-)p{_ z8N&B1G1J16R?}Ymy&r!+#@`3<*M-08lc2>wh_PDQET~n2xkkV}U{OUZ5t$GpB4o=d zLpNgK{h2|fm>)}P5odmw3IUI0dL|({3MB(YM`&s$su`rAg5`K4>;OG%6bTUm>GQ$? z7H+>}Rs>4g9)a!=ExlB)u`!p*fl9BKuP9Q=Yh6}^9AK3aIFIp*5OYN>nPH}KX)Gt+M4q8YAvk0E(aN;E zl(qGg=c3{sVqh&*cI&iTx}rV-%iS@3WwZOgN+%#2KT9!lvZ zc~I?;C?N>luQc6Q@1;_z!mDMl_S%~DR!J%YPZbuMM8z;FQSx?R`Tvw3R$XU1n(iyRCm}QApYoN<2I(pkhcl+L1>UAIJM|P5xHYIW=1-TJGa1qw+*4>E(mS} z{FH`O&!-Z{FtNmMjMK%k?05nFhqA;{iktE_8lcJ{9_HQ`T+%%1Pjs*kSr|q; zx|sPA4*diK-q!!c9zYpCvPNjROtUytEw#DHJ0Y+n1lU&@Tau=)XltKhClf5qCACn6LFfw1?WOw!%xd~~ zFaLKT|Mvm@ubTh+@kz%%fK12ed_fZ?y;{&3w2p-&57-7)<#krg<<~=uBGi5b;n|9! zI`g4{+4X{O(7IA>1${@>k7;?gP+*cyDio+tlzf!Ti35{IzmLAwt>!!&Y}_*oSM%bL@Yt(;2gz=9{KVIjuIRSd>HQZ^YyF_+j^w3_=~Z?Fyk)9I_4KJ8}bU{KZuFH9%EZARp6&DANznK zoYyIjjj^W-gtvpj*dgNh(pC~vy9G^ZcDqMF>`gFA^=lz1S*e=l=FEkojYHe1j zYw0Dku%H_uRRhidwJ*wsVujCiJPr2_{_vFU&RDxwI?K@nqxQ47dzPvFYhY!99;jL~ z|585q1d>nJ;d|BXHwq?rqwE9Eb}l+TJhsLO*2O0W@bOtT0_(=(G(*e2-; z)sFJ7c2~Inj1qNJkGo>??DR+%$8>Sp8DRyhA2Ci1?Uo-zAF-WHPW&#e(Ih$VG zNbaQ8{|%@;@aCCg`ihP{bcRF!bC|a&|tUz@*A1ag`4;A!;Q0dzyUx;dWbnC5jQ(PJxL4lgdg1guh+3dq~ z%Q>@DcLBi{+=&(x)CfZ2s&3Sq>@T%^OCDe!DBRIKldU4r94kQ9)2Q~W0!AEMzN@;> z)xXOwzZmi4V^79?@9d$PS$shZxzK2#19&HJzLyCcLW-h^+>5qo`&p||dJx#(jV!ES zz>(X22w!M;aAH;pu`ob4qqZNVB~&QJ8EE071CK0`W@J|A*Nq%PYob2U`-tx@7GcH9d!u z=+1ZsZVqK?MvX=iepF)1OiprREh#jWH;6ZVSka0jB9A6uOp)6;VNFFWd1<$7S4N*| zdw)i5;vs#k#XIv~&4RA`!Ezwvhtx+=@lUI;RL4r&ya_WhxK8#ZY|cd0B^0^91h!$H zUj(`owG^0dk{~tHZdH%7VxmBi^>k6zh~L{`QObQ0YZZmL*?iUJ^w{irEWsVE7FW5n zIX)-uaS`-UdI^?Ib5nJ*lSOBgtN{rZ7$qx@zW)6^R-2!+%Xi^68nUcP2Z^3HC`T@2 zyA2@*Vh8t}GlN=_wxz9BdAZKxO{p|%6+QKpa*wY^k#Ir}DNz!%qltTCQF|?~o~okf zGI?x3zNqc2uVyjX;6alOTq=S*#$;ufv}*Hj`vh;X%EYW0SDMNdyIjS9eKIeY z=t)sBY4WLvGc+L;%L=L0SVj3YFP_r!CzjpQ&|Y5|>zWTVkW)R%IFq1YG+!qYfiG9X zcO)pjxD=H$y`pgv))M&q#gOySxs4VgTEUND-?$$$3(E?hcz0rS$mV2=V=u=2mre7* zL&2Z-g7U#Q)+uur;PB$MUeAg>irM(JrlVB37mTcO#5a6b2 zk2zX@WE#jWO*Q7LR2`iaRD`G3=Xj=WZ4>92u+7a0jzY9cYNYrV@tI?+;CoMKxCH=KKB3W|B=tNwB6nK>Sop9O@j8902-)t23B!Dyp>vRQV{RsCz` zE7*ZTi`}-n4e^b$LE4aqweD*rj@iN>G6BpjbPTNwwd$`H8Pmg?D#T!bF?I9sn=?RHDPDFr3pwkf1h zL@H3McHtss?n1Fr3+_r(=MqF6S?tBwYD7sUv&>IG#+Du|fYz0zL;y(^@a2OX_&7-B*5a{mcMb;ATVgJW3)6`K9%UtG{K>ux_Vu(y(fZ^b!l+Ulwuy=}T>6#lvEUQm41zs3$_;n#ymFI;q7hjG zu6k+vhAMJS(pL~eQTlW)Y2od9CT%Dge=#(up-Y4*yB_i6k+)#tmm1R(^XjC{kp@03 zHnsG^t4Z{fqf}ltncc*P>2X*E^+Qx57k<8Qmu@UpfHf8jq423gXsDpNKmKi->z+IhY0s0PQb&w93_ z&;>t|o9{7HfExD@(olsVgVtr)J2K7M^V6Zz@xPB%KPf!pJ8o({*)3nVO$PP{j zP>pYBV1=Y~L|_mJgGKd#hUd8hDA94GQcQ((v0J%e>0#Y9*=wevZ+M7arCR?QN6ytx zaFp7A4T%br7RF=LQvp<%IuVhCFsA zA43t(>?m7yaZf#~fivo=bFN}YrAo4NWRA{SL^@NUGA+VN>S($MuhLlcjN*J7cj%qZ zHuqsAk(t4>wW(`Rn*_E7&3Rd-kg@Ot!etsPUJ-9hew?^CeUZgWJ&3S0t#5S;amqZu z-z(5su3x|%Xj^#>^9jbdwcXFKZwxkPk?H^%a6wt=h*X)`BK3Yu)f`a}mxHrlo{ zq9tshMfFES%i4NykIhuIgi)yIdPWx2V~cTMW#UH_JKYuq(gskZ**N#PeAHt^3k!P& z$=krH^+EsyP#XKvaKwvIO~2}m<63XN^CxCO-J^GsbW!4*WQFr-OlB@n+S*a73zV}| z(&z|x<=fvkYnRbpF~CY$9<30c4}sy4uA}-dA+V}b7iMA#Z;~!@NMt2Jc$91xDYz4} z8W|CM8zMrf@Pu@$g5-x$Ph%Y+T=35zJRX?+oV^DtbrnbrF@J;!G$vh}a-=lnb`zDd z6nQ~R_rQ}u|8Tf>-6BL;P1g*d|NCg-5ytECa4Q9ZkwQZT+ z1^*t=9+2FThE5gN4XO^o^w`&+dJ)_bR+`<#;Fq8;f?NAfVj-woSjhf`v*KBa5;&|t z&>)f4kcC}WhRQpsy6w`ZU23@OVGsf7_NtHR>&?1tS4g?Oz%`^R3PF~0zq{#Ym|j_~N9T!0$AyLm~mJb(0$Dz>$w zN)(>26T=sTo5}ZjLY2<<`-J&^d9SDXh9S6_k*wa7;ef)KU0gumH8LU}S_Fg%>^J@n z>N8BeNU1g`UlEXnfegZ5y@3?-{}o~{fv|N%Y1dlh4JlAD2|HIcFlAe_s!?|>&!A0o2iON@h4f%#d$3GRC0~aXfd=&%3fr-} z3H$Up!QRhJ1bxH>nMm5Hpe?~v^3F(P=B_qLRNpISjspVx#tz|AJP+WNl{wB@lpw>z z!oErfWJKw|v3O;WU}ieK7GJZ^8h}}`E zuNC5#yBw&e7K^pA%id6&t9c)2ZMf%EyDhQTlaKr)ah-Dx7De2zkT{a0&fdP+D-OAc}Z90I%5*36w@%r1^gPq_M% z!Iw5-8@L)j#3E&AH5G7Fn&rS1fK@Q(5{;RBeP!4*uDADM^wPIYX|3gKd6vKE*=B#A zXn{*2b@_`}UR`}jX2yFc;RU=KY`B z6V$mJyB(#v%Gfcelm6L&icjC;h|ix?yv<9|3{xf_@@``57_z2!eD@#V>>}fR+duLJ zo`2pN{r%@3|ML6Kh#`^@z<|{Vp@auL2y^QVPrQ5Mt;7&)?NpBz+c*I-YNI+thi|-< zWJ?k%Lt^b(tktw520k_51&$@s>XKS*8`1HC-9^X4D(*JPjb7 zr~QgqTuh9J^pf4gp@h?Qz#L=#MxssIgyI^Cv>`j=M~{TmnFv^ACSiO5y>yj)mP*lr za26Q;W`28|p4Dz2epSUqgjB z1V#0~$t%e=WbI!1F(eqqQ3;k+4w62!`VV#_38MuUMC9i$Fb9HPeS9rZcIB;5%(Il* z8>G(x<_l~QHt-UH2Ak&RWud3GLZ63zd>A!H9Hx4B73vNEt#hR4;;UzePODRe_!Y5g zH;n`hK^Q)f9-DULZJ@&G5sWvO+P?`EDz%0fi_4%iiM7>k%Lcsx=!fy{nx#UHW!iOZ zfi^8Kw%TC6(*E<+D0!u()Ppk<9g`FpJL$8WYu+>fecWmSYI{c5hI#|hWO4FR4?;ZQ zS=i6f?f}{JiW%dDbzAGC7-1fE8mVc$JBS}5bgyl zc%rhvD+0F6bl#B=h;mtkPm=E%8ym~o>Geis3Lc2124(i_6p*$4lC#Uzk6q{D8?$-q z0&UKR7*I-cN4piq&V19}RZMx2S6fFe=M-$))p9hukisYl?*ooFXV$jYc>xizCOALT zgPc=ng~+T+y5Dy{V#{$Q{#Jt&XdKNU??19%*;0DBCh1Afp2_s|&39b;#$#WbDS;Gd zm9)7rtU5=bK&>G`y0_6{NKA1M#om|k!zf`H(oVysSa%%Q<*oKHeoad?iW zyJG!TrLnziZm(mC`2DY4iLBt=7CAHBVBiQ|wk%SM#2>Tmss%_OuWs z5cT+HS=e7Xvs1FQfbNBqDt|U^VhFdN%(M3t&fJu9b-AfeC>hty?}r*J8iZzdAcAR? zuKG_d1RbDHJ7=^{m;&oj@UKHUBjU3kU_(DaV0)0pkK!6`2)$?qWTAxliP>!4ppy}Pyy0#rRh9(r0?%UkKbl?Ru)p}L2R0b z{+9b5W3y!?`pR`3mMRm6d65zI_JZD3nVOvXhGIeF!>c;PR?$`@r=fA5s>QmIIMlNe z0Z6%@1eEM;=+fy%^9<@I_1KRmooUnZ^`!>o42%$>>tunHAaHV8{VFm@^RaGphLR|& zzh52;Z1l=iAKEF$_2o3sbZ+5B4Y*@?=L{rWVF}IHNYuhD{?$mi8ri^Xw!i~ek>_E# zfbWy(u$Pn3#GMMQnKw}>Jo5m!*Th~ZUKX{WU#VVTp>q(}c2Et8b?E7=+EZS!qpHC&$|_w?~gXW@trB@ zZ;)WPYziEh6#r%AFD=VCe>}fn>aV^Oxk#U^A}iK?YF9QTlRG|~W>cK<5NJJ8&T;NL z1=2WAJk(;h{R7t@YmVr2%>WgW-h_9g2tG5^sOK2EI7{p z-IU;Y-~>PbFq$Q!M}&k=ah9B`zAvpiNEaaU7*&U1c-hmoKJ>5GjEF`)`)MZkcXT1~ zluS&l(6u9dW&A_xq+OHcn?EUC&0VRk=7CB?ojSg)1p>cmmMRBtv4m zZ@1>E&-;9SyAo?3TK;ZycECv&dL<`1q^Sj(gz_y#ZljSY`pWrT{i{J=IDr0DTIdfI z&h{tG#ygtvW)?we{y9vnYxit>2DFrWwmk{h*rBApq*L6ac#%&{2`^BFfrXqYVi*Tp zvqnBgf%phO_VQ@r@5<-W_nASVteQn36eY6u@9Fe|ExjJp6W>v~^k83MaE|m^(Y9Km z`-ZdV0*Hv@42zxH6&w}P0JQbRqDG>L0pPm4gQ^k$WMKDU&P~5Pv4y| ztyZ7BJK>}{QsY!_zB^&FI+kM}N+D#`_NP&GGjeYr2#l*x=|Dh7H0aAe%iO=W-eqQa z0)C5osY1|*BdgLl<;4W0FG2ZE0)0A;yDL)jiTuGe&GPBH{qmD{uT!BVs8Af4K63JD zPLDrSL~7numGeg)#5Kw{-|fNY-1yvEsy*z$aaF24oT{A5FDt-f)+6KDx`iYE@m#Nf zeV(18Uj5)2_2X;Qt!vbuta+Nt6Z3{hd4>8xnfh^=x>ctBM5b2Y&Xd&-zNmiuMRn_o z>Q7!&EBYek)p)j;N&gg)Y+QtS@QXnLst2r>W5G&-AX`>@`oT!~xg!sf>SOFb&pk-s zh|wy5E^2m!{~2P207m3QB%LUoEF2MfAv@96LiED6TaL=*$0vB$69jz}S8Pn(mGy(L{#Gp&|7K1B9q z_{%dAZB<1cTsP0Tc(T6*h9e@4o`U!;SU9;Ue@itMTKo>es|p$7wnKlb!#^;}KjkTb zUZ|i<%A8!5yt<^Clf_CcR9X3)URc&;QvE#-(O7eu?vEuv?N>(KN0H^j6v@kMJrf=U zI`OxF45#*-=-@t=~$93>9@k?GeVo)mU_Gb745a+$a9_ znDDrJ+$iLGnnGQC%4AsHhZNPJJfwth&pvgld8wrul#qCxD=FGLU+p&? z4a9@11^`3*vz$J8d&z#I(Dn)T?t-4_O>*!KEO*Y?_{?KwF!jsh(8SzH6^-N(-&5Rf z_MNA6&SjsQb5phokJ>*58HXr7*R_*?F8#%4@_;Q%H#S@`_Efu4+PP!V&0Z(3z`YsWfxX+Qc?yALRGRaeK%1mERbKT znr}5C3l=-%c38%qK%Ox(YL9rh?Nh=zaLu4i!%tT`Wd?3{`>`5IATTz9P&lu44UeQp zG+46{a3^`+GaKPEgi0nsWr<1^o{`2`&$d;ZKaTg&)k~2&isrgM;-pe1B{aAuyuyL0CErIn=&sRuS(8jm z?;vFW;T`rdSLU6vr+ni9Uawb8D4`CbjNWedQqcDAs2~%f;SmNo zE}Qhru%9koro1Wg_x_0PL)IFVbF_hB7XV~+LqvD=H-Dq#9N>tP6d$dzV$X##gF$Nj zZv=qbcaaN5bTE$;8c-|H%YpWjrpDm**~)nT{5no%YWJY$HDq`j`)5}0m2RGMY^cpj z4jXA_z>GmN`5_Oi8o}kk-W%?o?N;aU%aELjn?KIyjOe253Z)Rcy=ObU_bMGq$55aW z##V~;L+bf~WS`4bT_JpC#;!1sqNUeh(c8lJqNW|I5-#-JlA^aCgnwIH>s1GG9v!-q zKz=x7cR98IEH5!HzK7IFL|p<+_6&7}-+-q>6{>BAzDMpZh}xVR;HvHq8JG2+UxSVj zRRcJO2qHN}GBHBPz7# zwD7Y6Oqnn?jM@f1=nfibbUx($Dbi zHqO3#6Q`U=y~^p^pY-b+&)*9v8UgPSOE9n!CQo{cp$2EvmRWa%=34=Dq;tYlU?(`v zs9_R!6+?klj&o2AiRB35zKacxUJ|tK{*%}Km&Z}p7@Yw5cR`M?Ox?$6w7?!yV@A{C zDUU$WLh2?_lkzGv;{uk?m#?aV1FYMyyN9rr8B}+??Z7ZXN z&*u0gsO@Gx5VwTz22G|79^rt0wEGI{R*z|V1e_fefSZ>GwZJ8^Ll$e7sXSN6ddL)e z)p_?;hPVTz4YeagwtfYu$n1cei6L5$QIcRACY9^a!0=-J%ff}XF$iwx9lvx+Sk9KC z{mjq;L@SG4PZ|27X#=i{cW(rXx?9woU|w?bUD&|@d1s3yUiI36yVnk=QPNDinP1;( z{==#etdiutCT#xS-y89LZxnGL{d0`{6VVInRz^HyGO$+4f6?!>yUg4#F`j)qvXQnP-r=nReIbEUv<}DqC1O}w$hiYb|TJ0XN zRa4iIv>8OsQXFmiWVZ4mlzd;a&vUIji%X_8r{AP(m58*sa1G4F{lVn2? zM%qx#Rd^0}r?vef@H?P=;}g4T)ZTQ99im*ZtOL-8C`}<8#ff8Wt&rD_QqQME4sIsW z!jWpOQ)yWJO8ayWufHfq788*YvTke%BxCvneuUq!dJht6gCJJOsL{hEear0=-YopW z@X;m?_mY#ze#;5Q{VCT}mfzz}a#ZOY>M|Ri$lXfln z@r`?+YgWM&$s*;WQ?EknY$1D2!*hXtWn40xTQGT(5Kh=&8Q=3pszno5>Fa_n7<@j! zpfS&IdfF(ZEm#5w#xKTt`GO$U(IC>2&d|LrxW?9^Jn z^tX^pBc5}72#Fi&jlJcXZXle{Ar1HbiKwDRy;zHs2a5+}2FfAnWu|N9w}$$uTBF5v zuOjV~)4mg}X~YVJVb7p2h zZVdC*+Ih0aDG?t+tz)ncMc$Q5+*@@<^yTg1aAw=iqXuJqM>KhR-HpHbr@FK@03FxqlW(jHBvZe1uE-6@7iSPrYTDkM>V?|3 za!tGsn1{|RtM03Cu28aNlul~nVvnv*tH+SL(D|?f-?#;R@iad~NH81k3#=F2Y2C;O zoI+>(25TT5;SVcaZs==XsM|3ltKQSxLhgMKy=eqEB!o-^+hGg6xK#e89SKf4j{}Rr z3VO^djuhHCbMty`Bgw1KuIOjStzTXBUaUMWt z*=xl1KwsI0=kDh4s$@IA!#3o{QuU41HmwdX3`&oIqBQEq;2jrTsCLRfMIcgf{`g`J zem^e(?Ex#H4tniB|5q5wr!V{YKOYi4ePOvEfvv!QT>H(>rQGJ_;6}1tS;h@Vo`HAt zL%&XXW=ehDy~|Y%+)|PC8(%Qic#d`*Y}3a-SPP^-Ouu{HKYp)X+6Ai}rY1bi6-1MAK>o*T5-w3b(uPg%K8aH*!7C>mz#PI*-6H zj~1nEboWz&t>oxGR4j}|qh3NEQt44IQm)bSv5!xwuR5+YXjlFTov$EQ5@*nSVOlLc zh#D!?UjE`3YxI(iQ|hfv(_Vk~68G+VIax0O)#Ua*4i65~hEDz}8+uy(A~SEBhLS#D z_w)ypv}U-MOz)ol%>M3aQQY2GNRd=#sipEm1t4{<-9M+wjon&py0x7>3GIv$xLr;uP&C6M94Fg8Y zL(*fF8Q^LzC?k=6UWKTi+nIPyzfCw$kCF{PUJ6rZx|5gKoiwhNtX{L4XOe3ZzlEHv z`kl6!3N(xJYuD+U*9Z7zd-IeVuNaYtWBiJN%|UQa^Z2&)Bc2-G{^cD=6J`5Cf8?4n zFHqNeja~|P8m^rJ49WvA_`s)>6{DW3@Pc=i5t9etPm!u``W)ZL@>?V zB%Dx+;!f;m0GR2(*pS#--$6c(WOfoTUl{H*=uymuN>WFP_t z$-c>8Cns$M!CBDwqqz%sSKbHf^hBRzlo(!O2VQ*#x&wMBhiIsRFN*^f-IdhD0KH=k z%Nb?Uu#`cG8=E_!;Uc@F-GfvcxM+FyfF8csywSD86KGG)#VR zgzN`TD-WveWl731WGQ+Hr0rtQi$owL^I6|TJUc3T4V^MR$wY8bR&3)9B&nQ}c|Hn| z#hUm%-Qcz`ykJDe(`%VE@A!}*+cw~+2Ue+|aa`~pMy3lC69u;2uDGtiHsMKeml!>MbBBQL6l;Pfe5w+$ zO=uhyoUM$qr3b49)$Q*Y^#XHYA4-b+n3nn<^H`;#SfwKOe>Fv#jJ3xU6G?h{rDYTR zpSIlt932uUU@Ko(o1Wp>VEEp``JDycVfWMRfHjl z)Ai_MXusXzMAmzGp{dN2p~OB~?Lallg-`K>6fp}+G8A}jaTV|7EwREYgl2*j-+vc- zQf`|Yh8^jfpqAV^SJX^~6xr*kv4aaIphR4U?+z}A7#-ez0Hxu^k90BoCcdBI{K)`G zXG|_*RUll~0T(0*xsOjfBdw#CROk6A1Zz^y0fQ?^Z#aRyVy0vVa@QX;fHOj&XlNoS zM=6)MYw%9?9*SL}vAG^u0m_1q!@=*{4}q#{?D;_z+}C|RFxudy%URSDN$sZ5z^@2y zSETAss%Vk=o2(gfdLEq=I==%`5w0+X@7b$Xxey?5T;Vv^lApFXGIzt zRs?z%RhxLVYj1)pUe&{?S~=Q-o`{R>%P&sBh)!?GBh(L=!~sQigHe+L&@$?uKzG07 zdnOQ%=G_)yB9U-~xQwED_gJ!HOQ5@P*{A?0Ie_GD$?3YUtgg)GGF~^gC+veK#${03z`$8(pfvu5kqgEMw<%2Pn>*SM1tA+2xiKv4Es-F(yDM7E>yU zM}h}K9~-@d@Q`Eg1wV}SX29e+y)n}IIW?0Hanf^NI>_&`U2@XdXuOI zC-T098-0$d*nAKeLX;m`ZnXv=k%c!6v^AH<4nZ~*&ToNt1(tBs4(UCljpxxjb8dpt zgXy};3Mb(AyyM~YIpV;V<#chTqj%Usdf~^g@=S-9X3xJ*y&CB`cS=3u8<3o|;8O5* z@LH4iSp*=MXtD{ZqhaB4f*JdJCTeWZ7k~VFCLe7(D%!@d`aje5X9vQHtyixpbEwJypH1)gTXk zR5K&_qBJawRgdFsDk6Mc@4SsqaU+tKDx-r}`rPHI86*N0Z&NT)HPRaBr{cawQ{-t) zYpW_fX>cV&AqznM5K_B@k{w~XV)I?^7gPi2q~Ye>$A}gfmKecGVOJ2fkKc9y^>H3f z5r*XSP$k|$-T@i{;SAaLm=C=?$W>xzZ6l3!+@%!)e$o6{Gl*os6B)=4fT%_idw#u5 z!)4>xerhw&6T1?df_xT(4aUv2c>>a{*|0(xz^NG2L`>L8q#y+t@YoaOr7Ek9E*Pl= zFW<(tw?uAG@p1fbVey(_b{Lf|tVbX{fnE^W;Yu3teAfz^JtIwM+dS8RVUmON3y0BE z%G%g6ROD_o$|l6y5Hy6ofnc!W@Dvy6smB{+4(J}C)kPE|$p(Vu?*3(HQ6s%G-WTBZ zMLIU0QeTB6mA0R1toUTsfx%fr3utnm4ctmq)XOGshaMHpI+X^IcTMX%Qbt!rJ(|b& z5DYM8PcU^$F_yaas$cQqB8B#0UKmj8<=8cHsuqY(W+4u*GZiCpa9X^Qlx`UYPYLUR zxsR*3lu5^QCDjPuSC9cRwiF(jx!jO9-=()e;EF!}IKTJYCfC44?pH7=z|qj07}laY zYDSyo1Rj>Em5+3siwuHwjYUpjSVqtDCG%ATheVpxDvpEXjV*}ZUg9@Sb4MJrPCm71 zZIuH)6qah*j#j>cJ-3Gr2W<_yU|vVMZ`eK;w>VxY`YYv9XHY}pgvl9DFD`P%$cR`> zi_gG{{oe2j_Sej!88g;x*`v!$U3h!Qg#6XmKDD?aF78uUre8d_Ndw>LU6GE@F!s>> zF`|j7Y<)*^K&I}VPDi{B+S|O_aX$_e=@>;g9$U~sVdtUm@k`wfjy7yV(puUi`4iHm zx-)n{sQ-648WMb1ZV(MZof13F@ej|&B9Krov1@wXMa$z5WYM>EFc8{*_D4P&a33Ow z?8d5o4eJjA&DgVuFIzyF(ZpgCgjVnEwEO92sWFc@6T1LUVbGNNJqEHNnJ09B+5T$= zmFX+SQ?un4qfudG|4hGSOD`G#eJ0IZojrGe%tr z*4>CR;i)=O9KpGn2ZMQJ&apza-F$u;vR55!yL_VV=5~pT`2|~7>o`vB8l-i#jz6;Z zrHE!MmrW#5n5D=)=oDWV;UQp~djT~}fhI;Ve8|5BZ*vPugb~zlnWTu^T|>2FG?2hB z1v7SFX5%{*EhM%_Bc(oVorKo1nPha)j0da`&GffLDqr(zU|Y8k$eG@-I$phZ zkOGcff4ew>veyO951}aVe~k&R>kF~Ln!>p(R%iy4&|Y1ERp9W-W0VC66BUr#FK4ey zTyT||lyLD-ZVvpyRl&Il^Ez(GGuiX*I=sKE+M0@fr>;SZCx5UNeS?n>*hK0^Td^`U zp4+s1;Nm)X+ht(u71HWwuS2RD<+(!xmmf)SPKgd3u7S&gQ{1i*NLETtWY-s85sqND zo*+AUp5S_qZAJSzKcK&4PG~34R;+xD*9`4pT#kOGl2~>;3J0He?+-{5_o?q~IMe2I zAH)Wi;QPD*uNyDVN=8F{*B~iVfG|N>aokTqB>gmDRhZ8rbt8Hz#`wnuV%6(~7l>TG zcoa6#hZZZ?@G|_xk-e%x9s`8uLqh{v0rwKy5zHJ{HKp$9p|$L+@SJ~$sFT!_Ha%6) z!ZBfj6{{>rWr?1~<;Q|#91h3A5pN#u#ZwDM;oEezAO%;%rfY#}TN@YvlyOw_fn{n- z^YsVfiac!?KHUF(uzWe_wYEK29-nb8X?qzS2Nl@%qQLeCHd?~m%5i_q%GJET5R$;e z?%;6*`%LkP3UkHd@}xIBW+PW#MY??t7y`(B#SaxJqRKO(&@!xWW1b z&T}=LE4*<(G*!DwGocxhT7ld?7aC)+M2|E-IE#4s3E>4ll;19T=NYmEIU@-xStay$me~& z|G;G!2y>^Cpsn|08wEE!dVZS_aHX~HenE5OvfU3UXoQFas(>gH=(2JXF(9P*9e5x0 zY6@WMC3)PuWyxdcLV}jtISzTE-b!#F(Uo$` z`z$A!#PN~6c3qNEr(T#Ec(N~G5L^BArk*bSs^)H0sWWCAPh1ZmmASBd?;wW~1qQmU zs(vP*aa?Jl$9bfv#Vp;9H9Ei-)_Pr;`@7EsQo?KjNzAvs)j2O|k6l2nxMXRP{d(C7 z>%YPJ`vXMlS{;$#ow6yjd^w~G%$f|$Na3YY{1))gvys85T(=j6@_p(J;u7=`sv4{y z-xTRV$(eQvit~Z{?wzOJ@Xgo-j6;A=@c}6*e8nMfN+_MD8M7O&CHtP@Dv(R{VG0I7 zP_g+<2j~c5o*erKX-Jq(+e*S(6bChZYx7VyqN7od2^rI1e`Iiu?RSGt_hA~hn^D+8 zSmh?G!br!06hCrb{EUh-s?F{;f4Y_;7KXu4l~#O+308qZ2Znk06C0O!t|n^#(hAx! zS=7*b^7nX8M!`D&8N?EnXpPX3VS#bX^KBnJp)_?)Wg1EUd7`%q6YS|pxD*$SA1w)&22*_ zt?7>gU~7xAzf7y!NJBM-cj51;B(Y#5Ia1O>M(RdHb9NSp>#h+ymoqzd3Xs~$ybtAb z4W-4p`-0A*OR>Jp9(0}GcCRJ?1+jjl$YQ*tWsEu+;FJiBMk+ezrMswafxhih=$br< zb{KZId~EX2!V=?;X%R02=BQlqk$5qli8H zuxV5{TST$kl^4rfxI4oHP4GlhAyfXq7cWdp5OF^d)geFfKlO8vSfdzHWN+w^Bi zyonzQMue*)m$3I?3H8<%{K)q|iO$XrEpTlSBTEbSngHve3!fl(4+=Z~i#uZ)OL7KN zde1Fv7_O<5qurEa-b^{7oo=^bsjBo5$&+_ibxV{h1h_)p>J^n3)E@L=I-~94XUB1K zJ;Z`GJLIHj$3I)z2-H!wABVv5g)d{@e3~6X&<9WmnJ4bIUfacs{79RXQUBi^fXJlIjtxZH2R)aIHZq-Uglzj(Ggy`E1wZ5#5pKy&Ltj-R^( z5-jYT?nRxMselmMOY7g&rdPvcCXnJ^?@;PadO?kXhy;z~UXvt-i37*tjPjGk3B=w2 zaYE6=ddc;efIY(=yMes!522z!O?rphkeM|C1;soMst&wixp2W#69hPN1+NxelAdNVr{?q+jcbV*P>5qc;d(62`Cpx$=JSsTXN}u;MvQ;` zIr@7F{*L|Ro&f1fieS4!ZrhPD`+cXc-BD}4k5k^KCiKF22lDdRe9ujAfqrzU$;yz( z^0oCg)NLWz3_cN)A?rjFK{k;;o{`37Cw^38Ciz{GS;sxUg^kA3!O{}hEn@*t?QCoY2gw9eKA`Ue`lgpLdW@zk|7QUo|u$Xe@ z&Ua~33pinFwG4z*|%@#3ZYF->jjYP~HW~Q@BHK zx!#ghsNQ1n@6&n#li(p7+fI_)zH15tT_`_Nl7ttFycf=qh9}|k1AOG{<_mPtQ|Z{_ z+1kDU*OTxn9k?Jd`E_Etjg{*`MpJCvi4xI6!gU6nkS+t*5UN&MJho)bJb<{e?!yLu z3Ac2p%LGB(Oc-@6sGf=2hzS)TAjGk6qHYanb_(9VC8R!tZJmw+Bn~W5c?0RuSw%g$ z92ibuGhZQOl(cgB%5c*W;aLirV2cF&54WOf7Awf_q0|Jz96>{<32~D_A=?WI`I(0E zrAIM=zLji+gynjOqZ;^2+Wj?i#XV%aOUL9qOn-0QsKNE~nS>y_W%wjSt-E!ws8caW zp4yeH3Htm$P#ZjJeWd3Ct;Qb{>j*4$?j9Ff1QY^MN8^_eCtuzZi?)ojg;@2Wn9Yl> zkCYd4%n$VZaYPxvn2JSAtkTQ0Xr8aA9r!rrJay1UV%t3XQg%6#TTw;?HY}_M@~Ug# zk+nY=P(S-I+{8g|zU;ye{*Tf8H(=lQW`q#ek?=;OYn64NCnA=Y=wLwY2xe4pc@4P4 zpe)gPk!^rVLP>`ot?eVe(YQB+9QJu_5g3$g9(P90ci{EiXpZ;SLENRgQFycff!cy$ z7rnA~IN1vIF|APab}~b+=|-CFpDuBrem3Jg#6{U*vfNC|Sd$=tRGT8Xgr%I~_!e zgS@PskriF z@9SdJz^g%z)gF|WVaNkf#Ig|N zNOlF%#C28TUOg`H^h_K1T!aDARl0D~IPrzm7<&pm=;^;;V^1GTJ&55Zx_LAb;w_Dc zYMGTb=*ULE6QHw}%BnLoZ!(>(q_(zG$;_h}bjQ_Oj#jLI?7l}<*sqedfPF8LPL-c} z=Y;Ec`~SU6A&jNo6TjBIs|Vp=Z7t#@N%??cHbWc6Dh5gd9@c%>L6RMK2P8JN8#+oi zSH8ln!?dPh1M8lt05zhG;xPmFi~j0i_gAWq~pmm97?TPt5TSz*%f2}M%i5JC)RI} zz|0rCk(#gI^=(wf?`clo%>@1V@WTSE8KLRsX(0afdKh(IBm42BoP_5fJk<%w4YThN z&c2qE;C(Pj-JjzW-31tFA5Y@dhz}d1VFya$ny*9X_{#Xay5WZ*Aj;VNl9O&zt0YMM zfofrQ2E(T7{>edM*kmWBJa@}DQM-r~5~*32ov0{8QGQ{Zq_=ljtLO@=g)kZ)1z}y| zN%SZ6chox<$H)!Y8eeTK>6aaWB0u@_W2BjFT`!*UI3e)sPRYuhd(W@LAq-76Z@ zu*b+QkJrv><7^siIPv{XWQ$CDfU`hQ-NH`(8U(jcp2Fjo5w-wXmVMKGJ5ctcRaZx# zSGaq~Kp;k2W6{?w4i^TWRxIoCpXh$?1^?w$)e8QF6)a*AzpCwR)%r$@+?%6R4!CvZ z-8tX4#~M(L!vhN>Z{6=Hf&iSbLzqeIFNHWh$>OwkR(iG`XkEviePFYsB=d?;sN6y=lDDLDcEiLykeTroW+B1T5?q5<@px4KozypGuqolNo@aBp z*g=7S=V~ZwUXH#XmXHD0+n#}Cn7nt|&L!DPhkB4hOBGg+G0yr&%?epUVo>79;*&}l zg2jn|3FYq60&Xoi=?F=-i_EG40#gJUYGP5;Qf(`+E=vgrl`GzpL8Ti2-RiPA&If`@ zFV+QGD*!`W45i@K&5h^W5;Dc!2tJ!Q4qF8DIxrN_j)9-xD01h|?onurA~M$GUKiCQ zTlRQa?@DA-bO59b6^3SWQZ1EC3#m+QlvZ_hP6Khe$+o8zPYj73>(bGbLAu9-0yJ-% zC(km3sj2H!zIp1+sUdm>cj-oYqd~xZp#S{6D58^{3M5bG2H<4GRZ_+XMHa|30KMELB)*QOr!U{)>4IqKqH3E@^w$zM2c? zT|glQ0yS2=H?JPKKq#8kegtnYA#g(2K(FGxh~`|6uBeq?SjJ=n3NH|5e*5Cj+u6_C z?#JIZE0!f}`mZi&NJmhceMpfJtD^k`q;J{0tx!}S_kOnIne)QQ20Sww?V9q|-3hPa)wHN*oEs35Ka zq1L=+>u@HOYNc5pO>w7~Vh>7mY>3fez=n!@t%UL$1%+=?;xydIB7W)s1hPe>4eA}_ zXTcJMNzwUN%34?Q9n`s}s5A%X|2Qj^?N)cuEm%>@R7vAj30UOVtfxW&Ma%>5_t%Ft zx{_~LuVy*E$K)l-==zWoiKijK%HD(02d71O74aj;90sU@Ll>;xT7fc|6+0sLH8crLQ0omQnbfZQ zkPTB#CoI7>J*ucvz^WZpKn_Ab4UcafS?Pff+TJj8MGuqluLm z)RBrIdelVNM+6QFHEQbuZ<-2zDWsPO$;89!hV6-IoBpH1iNg4`l}9^Bsx^glb_nC? zt>^OMw~?w1Xc8h-3I8n^t7f%(P?0>2dx}Y+(Tq+BbwMjv6ld|CF9IcxE&u&kcOjPc ze1)ihxaG?}YYkr$C1fYw4(pw>-6|odG?6+}uDA~$^se%^NKR&#wzVMYwQe`0oTx>} zQj2Xf>sfy@`atLp^d4B@k@}%e9FU=UzySQL!cN5ugm6b&RD1oDZ4W zSM!L15iTeO{Qw%RgAksntr+Uetx8@5WkTVWNka7iCQA(h_|~GrR@Onz**B4SelZDZ2Ey2coiq2m1e0rTz&V0c5Spk8rI{y3xZub4d1q*HaKV2W@w>Yw!9x8&7!%07*;^bT%!+_kkNRT zqs{AYp;&yxT}iH*F_Jv_J<9n4G{opTT6jfKRt4T2vFMDb?xS&XIWp4)f@#@Kz9qovrUvYSf})6o`YF(R@1{idF3iJNeFYII2e`yCaM>*ZkJ zak4TU*nQGfpf&TkqEh*o(t>K8CUm0hqgJZ3af9#PFC#5-UjSUjBm|2I=%PVOR**cD z*X79K49z{@X!f*7=1GbL8ND0|POE%?+CdtCRRtL_>U!2_GJdWBI}{tder% z@;Kjs`#JK~5Vq;&*Xdz?0~svzjfqTkEyZ1Mo_n%xOLFQmSMqxO)WOf6?LqM*r!-uq zC20e!mf@dByj6>;H@8#m5@$h*I!o;xeOxArILYgM&sg+{ptI{v{BSCHmeYH_ z3s1H&yFK5zQ;OdQo=iaark@|J^ZV`hfgvb(nrUa?z4F4|IE|No~WCC{t0-R z<&d;86za{om&-r?;maUSaKf1lZZv_hCQ+|3Wox?|IEBZ+4zQpW5?`0nui{OpEgR#X z42%3(tE4ul(h+0g1?jRt%-cxU%8%%gOGyBnfE0mMFM+#N?SJ zJGkuXX2W|Sy*wB*Ov*WbJY}D7pl%v^9j&@M(Gr;Cu_xrb;vEkwJ3Ns6ql%#&6gfEh zJpaUBe0D;boDo7oo}feX-59NGM)HAgJ^utlk-$cJRK{_m2nt5ojzvGwfAAYW@!hfy zwqxwM?bKcf1F5^b9{YPs?Wkpw1#v>`Tu=GuYj{&B5$_8BWaD7fiA+3agTEOd=20>u z;R?G-tG=QuuaR%c2NR7e{F6CZOJz0oISVk?A0AIW;dVS}gltOb*JPj#H+qpe3Gq{vtGGq= z5db-m!yLZyL$U_{^Pl&lNeI^h`PJ^Ze#kT!oY)6bF(Uj*Mqdv|moYzMRKZ&akLCNi z!~vRQF@0Km>$`;&?lwCl5G+e?wDU<&&@3}`M18-7QkBW8M$yvlb)#~0q^Ow&qlgpz6i z?%I*a`G8BS1q)ewZR*x>qLK7`*B0BeN*_X;j4mg%++YP8?j#}mW!7$GT`tS+5?bFc zWibkP0gsf_BfCY0*S}r6pY5ZDKi&H8vA)GuHf(%EYYzh0fPt>vg z&J&4yzN<|*@m=lS;|Z_5piMmXg7&|@|E1_mLI$d(_eMB22J&|*s|y;|@tKD+?7<<6 zf#|`&6M3llDVgrVKAaR|Nx-RE?eVNra4W@e;}Px1KNT_EU90*bH!bY$XhBCD4HFo@ zW@cQK`nyn0dnbyYl1uQom+Eq?usn~sr3TW?GiCE44qupK5F3rx?Svs}k&hbfAfcAF ziSvSl`Oy`UZ?pc+qLuo~QfrKkOVy|aAX@Q*_hf)Sw=BGZx<>$@a_Zv`@ma69CumLq zvJ<%VegyG8@pg$(&IssCKYUtV$AI&{)j9MC4lQaKd7F~D#d&U zs%s$*%`m|l{acZR4@{LKXHS67`wG?FY`KZ_6kv{EH7XuJ<-NFvd3d;%uF@(3*bhA~ zVTXhVcb~N#fhjU_zMSINU}jd6=d)zrrim|wXrW=LxT{8NMXoM$R)Y(K)FyZ=bPFWrx z!zxSj^G^uD)I2YhPDP(w8zSdrqzW%|ZQc*aZzo9?FP!H?9F0DWb!T3Sb_mHzORaD( zJ)T5Tdwk5LEzfwxX^a8bHQgU{Kp-0L1FLvyq)7yJzizi@DR54$@1IuUYWbN;Uuka- zu%#63{urgX0_@+ocGsOvl=XIsTuwvgL9f#@ZU$S>$UoFX$RA`!IW#8t#w#6q@>u}W z?i_ut>VNpIfpMo_@(0%MNJ4Fqo}H^I3g|!Bb*SE7sFN&Lw0*QGIjr_fmR^&Ed*{7J zrg3(NEkRw6Rl;-W8`#z3q0nFvdvEgX0Qw{0rpS`5Gb-~8wYexNRLuBCg)U7uyJM-t zyb%2}-6Y=lwRvup2|M8u^T&PXGM_8i*B6C4$@|rijzMQ3uH!0t7_;WrtSH7|en0~i zzeR+EPU%nmX)DJO?eCB@bSVPHe4;5td%w&|hU*Q%IPw{R0ogPdQaDOLN!X*#$#(p` zV$J$gl=Un^x_0(<5DlDEp`-Yp2ZIWzO{o95YhD8kM4Eg;TTH^TxD&mC`UEDtAz7uvG@?we!M^sf0Y;2BG9wY?gs8qE7=tLfWelw|6*xt$5n-%SU5H)9 z_G6odQ7IJAMtig&H=$+aF}zng_ACGSRq8RfM1WbB$;`?L3k(B4r0oPtv`jNtd+(pDd<1ju`l>Qefeb_hAH4(K?>x z__X?_(&JyLJR0CpFKNwL;?jFMpDnR3|G*3L#rEA!?0ATT!z7K)t$@%^w(uhh)?MpB z$VRY?UYMoa?3EMQ3pB1Ous|WUW4}?Ax~Z6IvGOsJCy#L{*p+wBxM1OSt9B&N-!ox3 zWT3$N&E-a>QJr^Y>_PU&+hJ4= zjSax5tRgpE`8JCV`jk5Fd%k}`j|jeWh)Z|nkC;h-vcGQxrFF8wDopk5o6_NA>4}=YjPhqv2!=zxb zG6EB1h8`zJ{+BU?6tQ(F7s0B1h8!`;1xA2dF?M8kKIPW>rh~0%ZxsN(_WV zXqFKq`RWWiE1n)djp*5y%BEa}X4X8^!CoPbx%eg?^(48dpHgONgU>2(}#WzoM zma^5$Mw9;3ZLEEwj*puu*Jv+D+7XdPWlQ>Jj673M0kp4GyJeK1m9j#=zx!NGHWPv* zbMl0;5dVsIl|_$UFKRuOdZ@3cJ*Tx=XwXbUi){>1t;^ux3Y0y_xV~Jss^8lBGS@Dz z?Vje25nMijdhXAVvvkrT1%-L3UD^VXOfvc~ogYoTis$EwkqnJ7AyiPW!R+C(Uy(Z_xnn zxr09uoXfBh$D)pI%pT7Wi0J|kXrd=#<@(QkG-q=Mq=t|M%H+` zOqyz9VsikF$&LVZtp(LKyYqw>Vrjs^SY#t5A#O$WRjN|PsK0z&PQpGYg~I$HkXdxz zVU(Rk3<~ZO?EmUfb2GYT_;eE@NML7_|@mqDm3d6+*_h zi1b#UBc;0^nudXZ(qq0Uu^RnICW!Kd&KmeDvfbcmf2e)Si5-Hbu16-Zh&of%&@_iG z`HznSgMP?*jdb>GC$iU@wQVhEVKrYn-aFoh-9(ySvf%|8WMYScQ`Bt|gpq$ltZt#2 z!GCObhta@iCeWX)TD#;0dIi(|!{#lTo$J7MBjGOBAy@H|w4PIp~w4>IpQs_q%1 z6w~Cp`#<)JaGob%WQ57*tQ4pG1(ONFGgnuB8$+ zJ`He+$aQay4QU;OlhOUXj%V78b$ib`&_r=(yvoz|u2iLU+>h3$?ixiIOI7Vhb5QT9 zzKYo4`@>(LK0#glPJzV-7=K0WxaVjfE+f<)kbToI8$cZ%HV8Pnct%7Y3`am33+34| zu{MM|NAG+yRl)l%y0qn(VS^SG!}3Q$#f6CyGpP9F%2m%`5QtT5mpgC_~biYs{=&EZw!dn5s7#$m%t%mL~f!Bhcc-9v7RND>TVM zFej!`0j+0LyZxhR2=hsHT2UR+xE0um**GE7>nEfEI{O9{z@Nd%_Lsh)vFpolG{9!n z(#RNI44BQW2V6of?d{T|-x{PR&hRRPTTUZhrrIr)dslOyGg56m-hewP#9B*()@L37N+?DmS==yB3!bzIO+If7RFoTsCl9GjC__MH3-EMFpeOr z6duY5p+E7ONGSoI4dASG>ydZ-^)pAuq|5ZmH_spE?v+W*P?h14X9)e4&^l=30h?q3 zA;l;RK`Vvf^5TMe5A{{4TBSP1bmW7&({*2ftFu;r8Q>K@=Op+=SaIyE%}<}x5LAxk zG$oUrptxvJAm4{>gUkuATbU{egHL-oI2rlN4=gh3CMQ=)WDM& z`I&&cy#m=DQtvK1Vk zp!*D<+LShg82u3;j7((bE#g7Q&hf5qK`o=ihuAhYs}$Y>C?cRUCLAk8Pa5Jw(6$M^ zhJBM|^2>)ne8=v><8788Ll<`YYJnkuk%{872BZFJAD8$IiI*U17!*2WQh{jda1D*a|d3))@vkVEIN>&e};G)XIqao2SO*M2ZUpZVDDzh zhLIm|-T3sP>#eZ2d<*h0h`5O;jPh*k1z)t!*U-)u7_70GaJ3Al1=GxWH{035iZ@?J5f(TJlHcx)%8Tov2X{AVuV{5M*x{1#Mtqpc9?X!3W z@HgC)pI|UPETm=R!{Zn-cIesi+!r9im_(ehIE$Fz1a_qcl8wlXnWQlB1@tA8NsdI- z3;gDC)q#xC))}d;7PAL_b1fD>d%ll3Ad5hI8eI{)?g+Z1v3+0H4<;L!9fITr_9x4p zZrF-Z6@)l50<-rd4|mmezf<86LJVzQKx^`A8 z!RQdr1M3~GOGG&80+!|yL`8^^qUBJe!8onZWWr@dKG<9XjFUnHnMLEkiNP#-&Yb46 zLvotmKq5g_t)7k#wdiQ@g!LNb|5}BF0l~Q0aEd>K98=OtQ$K)|AP`BAL(t*g`5q`j04qzFSD?87PfhVX zh-;k@k+)J^({u-Alk8oQUoI`seIq<}r$u&d1#D&UO|w|(BuNWqju#F3G1CgbFv$*F zfITVU0;Z9rO4SDp6Uo^(aQ_^l0_l=naDyFV+Pt4*?JCBnc|wz`a}lbZlQw17MgEy@C%gYgYBDEiAL zBzTd#W3^ROeMhD{n`xzZC2&Hbx zyrmem$oE0I8uyQ4=J9lG=rUHLTV%i;v9}L3N;r@JZi>9}#uUx`;n{AU@y-~DNjS3- z3=x>R0B4kfii1T@f~4d#5557bIV0PCk8&G$@vyv?<_@a8*frFbOR6x!FiD17;{>;v zkR=JOjaT{i(6y+nUyZ2ljR)op{@&HXt^nl6H3RnfV1z7`D?jj#GCjCdFUC{?Mv-)t zh4%GgSPcTa++~6m6mY5`j}f?wdnW@yCL?f7e*i`ZAdcF zQFRS?W=s6_Iq=CO?k&Zb1?$lh(D*zfKd8RcFjoTJp?VV>W%Oznt<~tzNV@QzS@dAh zKy;>KD{9+8~CJQ=p4ryEhZ0gtG<;-j;4Hsu66 z;w~sZY#}L&1lPkLs6xv@^f~9VLEv2tu?nDI=<2T*pD{|Tz-85aRk8<>_Kw6UM7M-? z@uk| z0N&x?3RU3I0}89~?kWJ}Hb54Xut|+|mmi3kJ|uwejyvw!#WbgLMyAO$PtAG-4yqiX zGKVblC$Ks|mwtg(i6^zHp6l!-cBnAnoeLyp9OkkU_|#yfpP;$VzLosundA8X{mF0s z^Ks!`E~?yJiB28rptnUrI<%irg8f`kPfL!2$W?0K?dMFcqgSZwJT0+PRG%-dr zk9M}DDLzG*=bnVkHCAk8r9Yq3=R=l7iLMaq+`P_=MD`;CN^J*a0J2l(UF*YqrzywU zYFy}Gfd!YF;KPiT*q}=;WB4o9Q7NuNCat}qziNg#AMOJ@^B*!S6jQOoh>23jz>=Zv zz?l}JTdsFWGOD1TGQp^9L1N5X7;&mf;fg9Kl4U3}#B5q}ZHD}QX$Y7eKxH{+;|wxN z$Qjc*MR!4&jeCvCti>-<88d#<5N-B_jRfkGDoi#4bf%(cJ z25B7nOjw(@Fi|)Y*8D$Gy$!vDfB+O-7JL3|hVuXUtlTyyjOq1=NR11#6Z;Kp?}cMU zSCBVTg*4rkivAtn3RZ9w`%B?`Ba8yT(!q>I_Lor73T%&YvYb4!1Yudfk&Lk3aNx+;d59-nl=SOMbldXtFcq zy7^FYdHJ)+Wj};s;Y^`zZPg{jpv+y`VopVudSG}v!)cPYlpTBww zIZM-*)pMxxFX;I*bIF@M*`gL+-lf-QsHP-hB#V29Mni8P89a2^-Fnl!7aEOn1NW5= z`iHB0x2)QjkG)+Z)&h}L_fsBWt?H4T+h~RcT%`9H1Rj7$Fn!r{ z(mR?ryMhz8XWVFg4b`bg_Y5!^=ray4t-oqsV%firW5?Wv+H4Nv(FbBqefgn{Mb%uk z>;u|0@yYZV8ij(qZ@UeEQ6()U|N$noZE$-d*Ob>7zJESX(PGwGN;ux z%9yD>C-K-YUW-QzE;84sqTbDzOk#zSbTxoOWp5;lUNampIff4GVtjTG`vk>)Vx+0b zh;}|fYnyGCo0ESbss~oDC)I8t--i{l_aIMsBs4Z|W>nlt!(=U#mKtDuW@JqJu0~Wx ziM=Kj7CR0?u^%gdj6BA{P1b;!*4FE0Sr+DQ3sy)?DTuG=sp%k1iTIKq@$WG*I*gdR z(8vtuE;()4ubj|{8J26IKG1m-kb4VaMZNHP9-}4D6R;qc<@Ba6Ejjl$QpB1EJ@i|# z=I>7tD4wI$8C4ck*$`=9?DB z1=x3B8BFUUOkfmTP%N(<1uu+GEhN7M3tbg`j`T74#Qf4=3IseEne~KM zU9NS^a#2@%>@!!%u+$Coche2;;f3F!S^=_5jeHAYB@*)R82vb!q2+0(E U)Y~d?4D&S?kmVw#c)sHQ1*b{|m;e9( literal 0 HcmV?d00001 diff --git a/src/Arcade/Zork/vendor/glkapi.js b/src/Arcade/Zork/vendor/glkapi.js new file mode 100644 index 0000000000..b0eaab2648 --- /dev/null +++ b/src/Arcade/Zork/vendor/glkapi.js @@ -0,0 +1,6667 @@ +'use strict'; + +/* GlkAPI -- a Javascript Glk API for IF interfaces + * GlkOte Library: version 2.3.7. + * Glk API which this implements: version 0.7.6. + * Designed by Andrew Plotkin + * + * + * This Javascript library is copyright 2010-2025 by Andrew Plotkin. + * It is distributed under the MIT license; see the "LICENSE" file. + * + * This file is a Glk API compatibility layer for glkote.js. It offers a + * set of Javascript calls which closely match the original C Glk API; + * these work by means of glkote.js operations. + * + * This API was built for Quixe, which is a pure-Javascript Glulx + * interpreter. Therefore, the API is a little strange. Notably, it + * accepts text buffers in the form of arrays of integers, not + * Javascript strings. Only the Glk calls that explicitly use strings + * (glk_put_string, etc) accept Javascript native strings. + * + * If you are writing an application in pure Javascript, you can use + * this layer (along with glkote.js). If you are writing a web app which + * is the front face of a server-side Glk app, ignore this file -- use + * glkote.js directly. + */ + +/* Known problems: + + Some places in the library get confused about Unicode characters + beyond 0xFFFF. They are handled correctly by streams, but grid windows + will think they occupy two characters rather than one, which will + throw off the grid spacing. + + Also, the glk_put_jstring() function can't handle them at all. Quixe + printing operations that funnel through glk_put_jstring() -- meaning, + most native string printing -- will break up three-byte characters + into a UTF-16-encoded pair of two-byte characters. This will come + out okay in a buffer window, but it will again mess up grid windows, + and will also double the write-count in a stream. +*/ + +/* All state is contained in GlkClass. */ +var GlkClass = function() { + +var GlkOte = null; /* imported API object */ +var VM = null; /* imported API object (the VM interface) */ +var GiDispa = null; /* imported API object (the dispatch layer) */ +var Blorb = null; /* imported API object (the resource layer) */ + +/* Environment capabilities. (Checked at init time.) */ +var has_canvas; + +/* Options from the vm_options object. */ +var option_exit_warning; +var option_do_vm_autosave; +var option_before_select_hook; +var option_extevent_hook; +var option_glk_gestalt_hook; + +/* Library display state. */ +var has_exited = false; +var ui_disabled = false; +var ui_specialinput = null; +var ui_specialcallback = null; +var event_generation = 0; +var current_partial_inputs = null; +var current_partial_outputs = null; + +/* Initialize the library, initialize the VM, and set it running. (It will + run until the first glk_select() or glk_exit() call.) + + The vm_options argument must have a vm_options.vm field, which must be an + appropriate VM interface object. (For example, Quixe.) This must have + init() and resume() methods. + + The vm_options argument is also passed through to GlkOte as the game + interface object. It can be used to affect some GlkOte display options, + such as window spacing. + + (You do not need to provide a vm_options.accept() function. The Glk + library sets that up for you.) +*/ +function init(vm_options) { + /* Either GlkOte was passed in or we must create one. */ + if (vm_options.GlkOte) { + GlkOte = vm_options.GlkOte; + } + else if (window.GlkOteClass) { + GlkOte = new window.GlkOteClass(); + } + + /* Either Blorb was passed in or we don't have one. */ + if (vm_options.Blorb) { + Blorb = vm_options.Blorb; + } + + /* Check for canvas support. We don't rely on jquery here. */ + has_canvas = (document.createElement('canvas').getContext != undefined); + + VM = vm_options.vm; + GiDispa = vm_options.GiDispa; /* may be null/undefined */ + + vm_options.accept = accept_ui_event; + + option_exit_warning = vm_options.exit_warning; + option_do_vm_autosave = vm_options.do_vm_autosave; + option_before_select_hook = vm_options.before_select_hook; + option_extevent_hook = vm_options.extevent_hook; + option_glk_gestalt_hook = vm_options.glk_gestalt_hook; + + if (option_before_select_hook) { + option_before_select_hook(); + } + + /* Initialize the lower levels. */ + + if (GiDispa) + GiDispa.init({ io:vm_options.io, vm:vm_options.vm }); + GlkOte.init(vm_options); +} + +function is_inited() { + return (VM != null && GlkOte != null); +} + +function accept_ui_event(obj) { + var box; + + //qlog("### accept_ui_event: " + obj.type + ", gen " + obj.gen); + if (ui_disabled) { + /* We've hit glk_exit() or a VM fatal error, or just blocked the UI for + some modal dialog. */ + qlog("### ui is disabled, ignoring event"); + return; + } + + if (obj.gen != event_generation) { + GlkOte.log('Input event had wrong generation number: got ' + obj.gen + ', currently at ' + event_generation); + return; + } + event_generation += 1; + + /* Note any partial inputs; we'll need them if the game cancels a line + input. This may be undef. */ + current_partial_inputs = obj.partial; + + switch (obj.type) { + case 'init': + content_metrics = complete_metrics(obj.metrics); + /* We ignore the support array. This library is updated in sync + with GlkOte, so we know what it supports. */ + VM.start(); + break; + + case 'external': + var res = null; + if (option_extevent_hook) { + res = option_extevent_hook(obj.value); + } + if (!res && obj.value == 'timer') { + /* Timer events no longer come in this way, but we'll still + accept them. */ + gli_timer_started = Date.now(); + res = { type: Const.evtype_Timer }; + } + if (res && res.type) { + handle_external_input(res); + } + break; + + case 'timer': + gli_timer_started = Date.now(); + var res = { type: Const.evtype_Timer }; + handle_external_input(res); + break; + + case 'hyperlink': + handle_hyperlink_input(obj.window, obj.value); + break; + + case 'mouse': + handle_mouse_input(obj.window, obj.x, obj.y); + break; + + case 'char': + handle_char_input(obj.window, obj.value); + break; + + case 'line': + handle_line_input(obj.window, obj.value, obj.terminator); + break; + + case 'arrange': + content_metrics = complete_metrics(obj.metrics); + box = { + left: content_metrics.outspacingx, + top: content_metrics.outspacingy, + right: content_metrics.width-content_metrics.outspacingx, + bottom: content_metrics.height-content_metrics.outspacingy + }; + if (gli_rootwin) + gli_window_rearrange(gli_rootwin, box); + handle_arrange_input(); + break; + + case 'redraw': + handle_redraw_input(); + break; + + case 'specialresponse': + if (obj.response == 'fileref_prompt') { + gli_fileref_create_by_prompt_callback(obj); + } + break; + } +} + +/* Given a partial metrics object, return one with all the required + values. Missing values will default to 0 or the standard inherited + terms. (E.g., if "inspacingx" is missing it will default to + "inspacing", then "spacing", then 0. See measure_window() in + glkote.js or data_metrics_parse() in RemGlk.) + + All values in the given object will be copied over; defaulting only + applies to missing values from the required set. +*/ +function complete_metrics(metrics) { + + // Default values if absolutely nothing is specified. + var res = { + width: 80, + height: 50, + + gridcharwidth: 1, + gridcharheight: 1, + buffercharwidth: 1, + buffercharheight: 1, + + gridmarginx: 0, + gridmarginy: 0, + buffermarginx: 0, + buffermarginy: 0, + graphicsmarginx: 0, + graphicsmarginy: 0, + + outspacingx: 0, + outspacingy: 0, + inspacingx: 0, + inspacingy: 0, + }; + + // Various ways of specifying defaults. + var val; + + val = metrics.charwidth; + if (val !== undefined) { + res.gridcharwidth = val; + res.buffercharwidth = val; + } + val = metrics.charheight; + if (val !== undefined) { + res.gridcharheight = val; + res.buffercharheight = val; + } + + val = metrics.margin; + if (val !== undefined) { + res.gridmarginx = val; + res.gridmarginy = val; + res.buffermarginx = val; + res.buffermarginy = val; + res.graphicsmarginx = val; + res.graphicsmarginy = val; + } + + val = metrics.gridmargin; + if (val !== undefined) { + res.gridmarginx = val; + res.gridmarginy = val; + } + + val = metrics.buffermargin; + if (val !== undefined) { + res.buffermarginx = val; + res.buffermarginy = val; + } + + val = metrics.graphicsmargin; + if (val !== undefined) { + res.graphicsmarginx = val; + res.graphicsmarginy = val; + } + + val = metrics.marginx; + if (val !== undefined) { + res.gridmarginx = val; + res.buffermarginx = val; + res.graphicsmarginx = val; + } + + val = metrics.marginy; + if (val !== undefined) { + res.gridmarginy = val; + res.buffermarginy = val; + res.graphicsmarginy = val; + } + + val = metrics.spacing; + if (val !== undefined) { + res.inspacingx = val; + res.inspacingy = val; + res.outspacingx = val; + res.outspacingy = val; + } + + val = metrics.inspacing; + if (val !== undefined) { + res.inspacingx = val; + res.inspacingy = val; + } + + val = metrics.outspacing; + if (val !== undefined) { + res.outspacingx = val; + res.outspacingy = val; + } + + val = metrics.spacingx; + if (val !== undefined) { + res.inspacingx = val; + res.outspacingx = val; + } + + val = metrics.spacingy; + if (val !== undefined) { + res.inspacingy = val; + res.outspacingy = val; + } + + // Copy over all the supplied fields. These override the defaults above. + res = Object.assign(res, metrics); + + return res; +} + +function handle_arrange_input() { + if (!gli_selectref) + return; + + gli_selectref.set_field(0, Const.evtype_Arrange); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, 0); + gli_selectref.set_field(3, 0); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_redraw_input() { + if (!gli_selectref) + return; + + gli_selectref.set_field(0, Const.evtype_Redraw); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, 0); + gli_selectref.set_field(3, 0); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_external_input(res) { + if (!gli_selectref) + return; + + /* This also handles timer input. */ + var val1 = 0; + var val2 = 0; + if (res.val1) + val1 = res.val1; + if (res.val2) + val2 = res.val2; + + gli_selectref.set_field(0, res.type); + gli_selectref.set_field(1, null); + gli_selectref.set_field(2, val1); + gli_selectref.set_field(3, val2); + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_hyperlink_input(disprock, val) { + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.hyperlink_request) + return; + + gli_selectref.set_field(0, Const.evtype_Hyperlink); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, val); + gli_selectref.set_field(3, 0); + + win.hyperlink_request = false; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_mouse_input(disprock, xpos, ypos) { + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.mouse_request) + return; + + gli_selectref.set_field(0, Const.evtype_MouseInput); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, xpos); + gli_selectref.set_field(3, ypos); + + win.mouse_request = false; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_char_input(disprock, input) { + var charval; + + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.char_request) + return; + + if (input.length == 1) { + charval = input.charCodeAt(0); + if (charval >= 0x100 && !win.char_request_uni) + charval = Const.keycode_Unknown; + } + else { + charval = KeystrokeNameMap[input]; + if (!charval) + charval = Const.keycode_Unknown; + } + + gli_selectref.set_field(0, Const.evtype_CharInput); + gli_selectref.set_field(1, win); + gli_selectref.set_field(2, charval); + gli_selectref.set_field(3, 0); + + win.char_request = false; + win.char_request_uni = false; + win.input_generation = null; + + if (GiDispa) + GiDispa.prepare_resume(gli_selectref); + gli_selectref = null; + VM.resume(); +} + +function handle_line_input(disprock, input, termkey) { + var ix; + + if (!gli_selectref) + return; + + var win = null; + for (win=gli_windowlist; win; win=win.next) { + if (win.disprock == disprock) + break; + } + if (!win || !win.line_request) + return; + + if (input.length > win.linebuf.length) + input = input.slice(0, win.linebuf.length); + + if (win.request_echo_line_input) { + ix = win.style; + gli_set_style(win.str, Const.style_Input); + gli_window_put_string(win, input); + if (win.echostr) + glk_put_jstring_stream(win.echostr, input); + gli_set_style(win.str, ix); + gli_window_put_string(win, "\n"); + if (win.echostr) + glk_put_jstring_stream(win.echostr, "\n"); + } + + for (ix=0; ix 100) { + win.reserve.splice(0, win.reserve.length-100); + } + break; + case Const.wintype_TextGrid: + if (win.gridwidth == 0 || win.gridheight == 0) + break; + obj.lines = []; + for (ix=0; ix= 0) { + /* We're going to delete every command before the + fill, except that we save the last setcolor. */ + var setcol = null; + for (ix=0; ix=0; ix--) { + var obj = res.windows[ix]; + var win = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + style: obj.style, hyperlink: obj.hyperlink + }; + GiDispa.class_register('window', win, win.disprock); + + win.prev = null; + win.next = gli_windowlist; + gli_windowlist = win; + if (win.next) + win.next.prev = win; + } + + for (var ix=res.streams.length-1; ix>=0; ix--) { + var obj = res.streams[ix]; + var str = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + unicode: obj.unicode, isbinary: obj.isbinary, + readcount: obj.readcount, writecount: obj.writecount, + readable: obj.readable, writable: obj.writable, + streaming: obj.streaming + }; + GiDispa.class_register('stream', str, str.disprock); + + str.prev = null; + str.next = gli_streamlist; + gli_streamlist = str; + if (str.next) + str.next.prev = str; + } + + for (var ix=res.filerefs.length-1; ix>=0; ix--) { + var obj = res.filerefs[ix]; + var fref = { + type: obj.type, rock: obj.rock, disprock: obj.disprock, + filename: obj.filename, textmode: obj.textmode, + filetype: obj.filetype, filetypename: obj.filetypename + }; + GiDispa.class_register('fileref', fref, fref.disprock); + + fref.prev = null; + fref.next = gli_filereflist; + gli_filereflist = fref; + if (fref.next) + fref.next.prev = fref; + } + + /* ...Now we fill in the cross-references. */ + + for (var ix=0; ix str.bufeof) + str.bufpos = str.bufeof; + } + else { + str.ref = obj.ref; + str.fstream = Dialog.file_fopen(str.origfmode, str.ref); + if (!str.fstream) { + /* This is the panic case. We can't reopen the stream, + but the game expects an open stream! We'll just + have to open a temporary file; the user will never + get their data, but at least the game won't crash. + (Better policy would be to prompt the user for + a new file location...) */ + var tempref = Dialog.file_construct_temp_ref(str.ref.usage); + str.fstream = Dialog.file_fopen(str.origfmode, tempref); + if (!str.fstream) + throw('restore_allstate: could not reopen even a temp stream for: ' + str.ref.filename); + } + + if (str.origfmode != Const.filemode_WriteAppend) { + /* Jump to the last known filepos. */ + str.fstream.fseek(obj.filepos, Const.seekmode_Start); + } + + str.buffer4 = str.fstream.BufferClass.alloc(4); + } + break; + + } + } + + for (var ix=0; ix> 10), 0xDC00 + (val & 0x3FF)); + } +} + +/* Given an array, return an array of the same length with all the values + trimmed to the range 0-255. This may be the same array. */ +function TrimArrayToBytes(arr) { + var ix, newarr; + var len = arr.length; + for (ix=0; ix= 0x100) + break; + } + if (ix == len) { + return arr; + } + newarr = Array(len); + for (ix=0; ix= 0x100) + newarr[ix] = 63; // '?' + else + newarr[ix] = arr[ix]; + } + return newarr; +} + +/* Convert an array of 8-bit values to a JS string, trimming if + necessary. */ +function ByteArrayToString(arr) { + var ix, newarr; + var len = arr.length; + if (len == 0) + return ''; + for (ix=0; ix= 0x100) + break; + } + if (ix == len) { + return String.fromCharCode.apply(this, arr); + } + newarr = Array(len); + for (ix=0; ix= 0x10000) + break; + } + if (ix == len) { + return String.fromCharCode.apply(this, arr); + } + newarr = Array(len); + for (ix=0; ix> 10), 0xDC00 + (val & 0x3FF)); + } + } + return newarr.join(''); +} + +/* Convert an array of 32-bit Unicode values to an array of 8-bit byte + values, encoded UTF-8. If all the values are 0-127, this returns the + same array. Otherwise it returns a new (longer) array. */ +function UniArrayToUTF8(arr) { + var count = 0; + + for (var ix=0; ix> 6)); + res.push(0x80 | (val & 0x03F) ); + } + else if (val < 0x10000) { + res.push(0xE0 | ((val & 0xF000) >> 12)); + res.push(0x80 | ((val & 0x0FC0) >> 6)); + res.push(0x80 | (val & 0x003F) ); + } + else if (val < 0x200000) { + res.push(0xF0 | ((val & 0x1C0000) >> 18)); + res.push(0x80 | ((val & 0x03F000) >> 12)); + res.push(0x80 | ((val & 0x000FC0) >> 6)); + res.push(0x80 | (val & 0x00003F) ); + } + else { + res.push(63); // '?' + } + } + + return res; +} + +/* Convert an array of 32-bit Unicode values to an array of 8-bit byte + values, encoded as big-endian words. */ +function UniArrayToBE32(arr) { + var res = new Array(4*arr.length); + for (var ix=0; ix> 24) & 0xFF; + res[4*ix+1] = (val >> 16) & 0xFF; + res[4*ix+2] = (val >> 8) & 0xFF; + res[4*ix+3] = (val) & 0xFF; + } + return res; +} + +/* Log the message in the browser's error log, if it has one. (This shows + up in Safari, in Opera, and in Firefox if you have Firebug installed.) +*/ +function qlog(msg) { + if (window.console && console.log) + console.log(msg); + else if (window.opera && opera.postError) + opera.postError(msg); +} + +/* RefBox: Simple class used for "call-by-reference" Glk arguments. The object + is just a box containing a single value, which can be written and read. +*/ +function RefBox() { + this.value = undefined; + this.set_value = function(val) { + this.value = val; + } + this.get_value = function() { + return this.value; + } +} + +/* RefStruct: Used for struct-type Glk arguments. After creating the + object, you should call push_field() the appropriate number of times, + to set the initial field values. Then set_field() can be used to + change them, and get_fields() retrieves the list of all fields. + + (The usage here is loose, since Javascript is forgiving about arrays. + Really the caller could call set_field() instead of push_field() -- + or skip that step entirely, as long as the Glk function later calls + set_field() for each field. Which it should.) +*/ +function RefStruct(numels) { + this.fields = []; + this.push_field = function(val) { + this.fields.push(val); + } + this.set_field = function(pos, val) { + this.fields[pos] = val; + } + this.get_field = function(pos) { + return this.fields[pos]; + } + this.get_fields = function() { + return this.fields; + } +} + +/* Dummy return value, which means that the Glk call is still in progress, + or will never return at all. This is used by glk_exit(), glk_select(), + and glk_fileref_create_by_prompt(). +*/ +var DidNotReturn = { dummy: 'Glk call has not yet returned' }; + +/* This returns a hint for whether the Glk call (by selector number) + might block or never return. True for glk_exit(), glk_select(), + and glk_fileref_create_by_prompt(). +*/ +function call_may_not_return(id) { + if (id == 0x001 || id == 0x0C0 || id == 0x062) + return true; + else + return false; +} + +var strtype_File = 1; +var strtype_Window = 2; +var strtype_Memory = 3; +var strtype_Resource = 4; + +/* Extra update information -- autorestore only. */ +var gli_autorestore_glkstate = null; + +/* Beginning of linked list of windows. */ +var gli_windowlist = null; +var gli_rootwin = null; +/* Set when any window is created, destroyed, or resized. */ +var geometry_changed = true; +/* Received from GlkOte; describes the window size. */ +var content_metrics = null; + +/* Beginning of linked list of streams. */ +var gli_streamlist = null; +/* Beginning of linked list of filerefs. */ +var gli_filereflist = null; +/* Beginning of linked list of schannels. */ +var gli_schannellist = null; + +/* The current output stream. */ +var gli_currentstr = null; + +/* During a glk_select() block, this is the RefStruct which will contain + the result. */ +var gli_selectref = null; + +/* This is used to assigned disprock values to windows, when there is + no GiDispa layer to provide them. */ +var gli_api_display_rocks = 1; + +/* A positive number if the timer is set. */ +var gli_timer_interval = null; +var gli_timer_started = null; /* when the setTimeout began */ +var gli_timer_lastsent = null; /* last interval sent to GlkOte */ + +function gli_new_window(type, rock) { + var win = {}; + win.type = type; + win.rock = rock; + win.disprock = undefined; + + win.parent = null; + win.str = gli_stream_open_window(win); + win.echostr = null; + win.style = Const.style_Normal; + win.hyperlink = 0; + + win.input_generation = null; + win.linebuf = null; + win.char_request = false; + win.line_request = false; + win.char_request_uni = false; + win.line_request_uni = false; + win.hyperlink_request = false; + win.mouse_request = false; + win.echo_line_input = true; + win.line_input_terminators = []; + win.request_echo_line_input = null; /* only used during a request */ + + /* window-type-specific info is set up in glk_window_open */ + + win.prev = null; + win.next = gli_windowlist; + gli_windowlist = win; + if (win.next) + win.next.prev = win; + + if (GiDispa) + GiDispa.class_register('window', win); + else + win.disprock = gli_api_display_rocks++; + /* We need to assign a disprock even if there's no GiDispa layer, + because GlkOte differentiates windows by their disprock. */ + geometry_changed = true; + + return win; +} + +function gli_delete_window(win) { + var prev, next; + + if (GiDispa) + GiDispa.class_unregister('window', win); + geometry_changed = true; + + win.echostr = null; + if (win.str) { + gli_delete_stream(win.str); + win.str = null; + } + + prev = win.prev; + next = win.next; + win.prev = null; + win.next = null; + + if (prev) + prev.next = next; + else + gli_windowlist = next; + if (next) + next.prev = prev; + + win.parent = null; + win.rock = null; + win.disprock = null; +} + +function gli_windows_unechostream(str) { + var win; + + for (win=gli_windowlist; win; win=win.next) { + if (win.echostr === str) + win.echostr = null; + } +} + +/* Add a (Javascript) string to the given window's display. */ +function gli_window_put_string(win, val) { + var ix, ch; + + //### might be efficient to split the implementation up into + //### gli_window_buffer_put_string(), etc, since many functions + //### know the window type when they call this + switch (win.type) { + case Const.wintype_TextBuffer: + if (win.style != win.accumstyle + || win.hyperlink != win.accumhyperlink) + gli_window_buffer_deaccumulate(win); + win.accum.push(val); + break; + case Const.wintype_TextGrid: + for (ix=0; ix= win.gridwidth) { + win.cursorx = 0; + win.cursory++; + } + if (win.cursory < 0) + win.cursory = 0; + if (win.cursory >= win.gridheight) + break; /* outside the window */ + + if (ch == "\n") { + /* a newline just moves the cursor. */ + win.cursory++; + win.cursorx = 0; + continue; + } + + var lineobj = win.lines[win.cursory]; + lineobj.dirty = true; + lineobj.chars[win.cursorx] = ch; + lineobj.styles[win.cursorx] = win.style; + lineobj.hyperlinks[win.cursorx] = win.hyperlink; + + win.cursorx++; + /* We can leave the cursor outside the window, since it will be + canonicalized next time a character is printed. */ + } + break; + } +} + +/* Canonicalize the cursor position. That is, the cursor may have + been left outside the window area; wrap it if necessary. + + Returns true if the cursor winds up wrapped outside the window entirely; + false if the cursor winds up at a legal printing position. +*/ +function gli_window_grid_canonicalize(win) { + if (win.cursorx < 0) + win.cursorx = 0; + if (win.cursorx >= win.gridwidth) { + win.cursorx = 0; + win.cursory++; + } + if (win.cursory < 0) + win.cursory = 0; + if (win.cursory >= win.gridheight) + return true; /* outside the window */ + return false; +} + +/* Take the accumulation of strings (since the last style change) and + assemble them into a buffer window update. This must be called + after each style change; it must also be called right before + GlkOte.update(). (Actually we call it right before win.accum.push + if the style has changed -- there's no need to call for *every* style + change if no text is being pushed out in between.) +*/ +function gli_window_buffer_deaccumulate(win) { + var conta = win.content; + var stylename = StyleNameMap[win.accumstyle]; + var text, ls, ix, obj, arr; + + if (win.accum.length) { + text = win.accum.join(''); + ls = text.split('\n'); + for (ix=0; ix win.gridheight) { + win.lines.length = win.gridheight; + } + else if (oldheight < win.gridheight) { + for (ix=oldheight; ix win.gridwidth) { + lineobj.dirty = true; + lineobj.chars.length = win.gridwidth; + lineobj.styles.length = win.gridwidth; + lineobj.hyperlinks.length = win.gridwidth; + } + else if (oldwidth < win.gridwidth) { + lineobj.dirty = true; + for (cx=oldwidth; cx= max) { + split = min; + } + else { + split = Math.min(Math.max(split, min), max-splitwid); + } + + win.pair_splitpos = split; + win.pair_splitwidth = splitwid; + if (win.pair_vertical) { + box1 = { + left: win.bbox.left, + right: win.pair_splitpos, + top: win.bbox.top, + bottom: win.bbox.bottom + }; + box2 = { + left: box1.right + win.pair_splitwidth, + right: win.bbox.right, + top: win.bbox.top, + bottom: win.bbox.bottom + }; + } + else { + box1 = { + top: win.bbox.top, + bottom: win.pair_splitpos, + left: win.bbox.left, + right: win.bbox.right + }; + box2 = { + top: box1.bottom + win.pair_splitwidth, + bottom: win.bbox.bottom, + left: win.bbox.left, + right: win.bbox.right + }; + } + if (!win.pair_backward) { + ch1 = win.child1; + ch2 = win.child2; + } + else { + ch1 = win.child2; + ch2 = win.child1; + } + + gli_window_rearrange(ch1, box1); + gli_window_rearrange(ch2, box2); + break; + + } +} + +function gli_new_stream(type, readable, writable, rock) { + var str = {}; + str.type = type; + str.rock = rock; + str.disprock = undefined; + + str.unicode = false; + /* isbinary is only meaningful for Resource and streaming-File streams */ + str.isbinary = false; + str.streaming = false; + str.ref = null; + str.win = null; + str.file = null; + + /* for buffer mode */ + str.buf = null; + str.bufpos = 0; + str.buflen = 0; + str.bufeof = 0; + str.timer_id = null; + str.flush_func = null; + + /* for streaming mode */ + str.fstream = null; + + str.readcount = 0; + str.writecount = 0; + str.readable = readable; + str.writable = writable; + + str.prev = null; + str.next = gli_streamlist; + gli_streamlist = str; + if (str.next) + str.next.prev = str; + + if (GiDispa) + GiDispa.class_register('stream', str); + + return str; +} + +function gli_delete_stream(str) { + var prev, next; + + if (str === gli_currentstr) { + gli_currentstr = null; + } + + gli_windows_unechostream(str); + + if (str.type == strtype_Memory) { + if (GiDispa) + GiDispa.unretain_array(str.buf); + } + else if (str.type == strtype_File) { + if (str.fstream) { + str.fstream.fclose(); + str.fstream = null; + } + } + + if (GiDispa) + GiDispa.class_unregister('stream', str); + + prev = str.prev; + next = str.next; + str.prev = null; + str.next = null; + + if (prev) + prev.next = next; + else + gli_streamlist = next; + if (next) + next.prev = prev; + + str.fstream = null; + str.buf = null; + str.readable = false; + str.writable = false; + str.ref = null; + str.win = null; + str.file = null; + str.rock = null; + str.disprock = null; +} + +function gli_stream_open_window(win) { + var str; + str = gli_new_stream(strtype_Window, false, true, 0); + str.unicode = true; + str.win = win; + return str; +} + +/* This is called on every write to a file stream. If a file is being + written intermittently (a transcript file, for example) we'd like to + flush the output every few seconds, in case the user closes the + browser without closing the file ("script off"). + + We do this by setting a ten-second timer (if there isn't one set already). + The timer calls a flush method on the stream. + + (If autosave is on, we'll wind up flushing on most glk_select calls, + which isn't quite as nicely paced. But it's a minor problem.) +*/ +function gli_stream_dirty_file(str) { + if (str.streaming) + GlkOte.log('Bug: gli_stream_dirty_file called for streaming file!'); + if (str.timer_id === null) { + if (str.flush_func === null) { + /* Bodge together a closure to act as a stream method. */ + str.flush_func = function() { gli_stream_flush_file(str); }; + } + str.timer_id = setTimeout(str.flush_func, 10000); + } +} + +/* Write out the contents of a file stream to the "disk file". Because + localStorage doesn't support appending, we have to dump the entire + buffer out. +*/ +function gli_stream_flush_file(str) { + var Dialog = GlkOte.getlibrary('Dialog'); + + if (str.streaming) + GlkOte.log('Bug: gli_stream_flush_file called for streaming file!'); + if (!(str.timer_id === null)) { + clearTimeout(str.timer_id); + } + str.timer_id = null; + Dialog.file_write(str.ref, str.buf); +} + +function gli_new_fileref(filename, usage, rock, ref) { + var Dialog = GlkOte.getlibrary('Dialog'); + + var fref = {}; + fref.filename = filename; + fref.rock = rock; + fref.disprock = undefined; + + fref.textmode = ((usage & Const.fileusage_TextMode) != 0); + fref.filetype = (usage & Const.fileusage_TypeMask); + fref.filetypename = FileTypeMap[fref.filetype]; + if (!fref.filetypename) { + fref.filetypename = 'xxx'; + } + + if (!ref) { + var gameid = ''; + if (fref.filetype == Const.fileusage_SavedGame) + gameid = VM.get_signature(); + ref = Dialog.file_construct_ref(fref.filename, fref.filetypename, gameid); + } + fref.ref = ref; + + fref.prev = null; + fref.next = gli_filereflist; + gli_filereflist = fref; + if (fref.next) + fref.next.prev = fref; + + if (GiDispa) + GiDispa.class_register('fileref', fref); + + return fref; +} + +function gli_delete_fileref(fref) { + var prev, next; + + if (GiDispa) + GiDispa.class_unregister('fileref', fref); + + prev = fref.prev; + next = fref.next; + fref.prev = null; + fref.next = null; + + if (prev) + prev.next = next; + else + gli_filereflist = next; + if (next) + next.prev = prev; + + fref.filename = null; + fref.ref = null; + fref.rock = null; + fref.disprock = null; +} + +/* Write one character (given as a Unicode value) to a stream. + This is called by both the one-byte and four-byte character APIs. +*/ +function gli_put_char(str, ch) { + if (!str || !str.writable) + throw('gli_put_char: invalid stream'); + + if (!str.unicode) { + if (ch < 0 || ch >= 0x100) + ch = 63; // '?' + } + + str.writecount += 1; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + str.buffer4[0] = ch; + str.fstream.fwrite(str.buffer4, 1); + } + else { + if (!str.isbinary) { + /* cheap UTF-8 stream */ + var len; + if (ch < 0x10000) { + len = str.buffer4.write(String.fromCharCode(ch)); + str.fstream.fwrite(str.buffer4, len); // utf8 + } + else { + /* String.fromCharCode chokes on astral characters; + do it the hard way */ + var arr8 = UniArrayToUTF8([ch]); + var buf = str.fstream.BufferClass.from(arr8); + str.fstream.fwrite(buf); + } + } + else { + /* cheap big-endian stream */ + str.buffer4.writeUInt32BE(ch, 0, true); + str.fstream.fwrite(str.buffer4, 4); + } + } + } + else { + /* non-streaming... */ + gli_stream_dirty_file(str); + if (!str.unicode || (ch < 0x80 && !str.isbinary)) { + if (str.bufpos < str.buflen) { + str.buf[str.bufpos] = ch; + str.bufpos += 1; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + } + } + else { + var arr; + if (!str.isbinary) + arr = UniArrayToUTF8([ch]); + else + arr = UniArrayToBE32([ch]); + var len = arr.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (var ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + } + break; + case strtype_Memory: + if (str.bufpos < str.buflen) { + str.buf[str.bufpos] = ch; + str.bufpos += 1; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Window: + if (str.win.line_request) + throw('gli_put_char: window has pending line request'); + gli_window_put_string(str.win, CharToString(ch)); + if (str.win.echostr) + gli_put_char(str.win.echostr, ch); + break; + } +} + +/* Write characters (given as an array of Unicode values) to a stream. + This is called by both the one-byte and four-byte character APIs. + The "allbytes" argument is a hint that all the array values are + already in the range 0-255. +*/ +function gli_put_array(str, arr, allbytes) { + var ix, len, val; + + if (!str || !str.writable) + throw('gli_put_array: invalid stream'); + + if (!str.unicode && !allbytes) { + arr = TrimArrayToBytes(arr); + allbytes = true; + } + + str.writecount += arr.length; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + var buf = str.fstream.BufferClass.from(arr); + str.fstream.fwrite(buf); + } + else { + if (!str.isbinary) { + /* cheap UTF-8 stream */ + var arr8 = UniArrayToUTF8(arr); + var buf = str.fstream.BufferClass.from(arr8); + str.fstream.fwrite(buf); + } + else { + /* cheap big-endian stream */ + var buf = str.fstream.BufferClass.alloc(4*arr.length); + for (ix=0; ix str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Memory: + len = arr.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + break; + case strtype_Window: + if (str.win.line_request) + throw('gli_put_array: window has pending line request'); + if (allbytes) + val = String.fromCharCode.apply(this, arr); + else + val = UniArrayToString(arr); + gli_window_put_string(str.win, val); + if (str.win.echostr) + gli_put_array(str.win.echostr, arr, allbytes); + break; + } +} + +function gli_get_char(str, want_unicode) { + var ch; + + if (!str || !str.readable) + return -1; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (!str.unicode) { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + str.readcount++; + return str.buffer4[0]; + } + else { + if (!str.isbinary) { + /* slightly less cheap UTF8 stream */ + var val0, val1, val2, val3; + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val0 = str.buffer4[0]; + if (val0 < 0x80) { + ch = val0; + } + else { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val1 = str.buffer4[0]; + if ((val1 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xE0) == 0xC0) { + ch = (val0 & 0x1F) << 6; + ch |= (val1 & 0x3F); + } + else { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val2 = str.buffer4[0]; + if ((val2 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xF0) == 0xE0) { + ch = (((val0 & 0xF)<<12) & 0x0000F000); + ch |= (((val1 & 0x3F)<<6) & 0x00000FC0); + ch |= (((val2 & 0x3F)) & 0x0000003F); + } + else if ((val0 & 0xF0) == 0xF0) { + var len = str.fstream.fread(str.buffer4, 1); + if (!len) + return -1; + val3 = str.buffer4[0]; + if ((val3 & 0xC0) != 0x80) + return -1; + ch = (((val0 & 0x7)<<18) & 0x1C0000); + ch |= (((val1 & 0x3F)<<12) & 0x03F000); + ch |= (((val2 & 0x3F)<<6) & 0x000FC0); + ch |= (((val3 & 0x3F)) & 0x00003F); + } + else { + return -1; + } + } + } + } + else { + /* cheap big-endian stream */ + var len = str.fstream.fread(str.buffer4, 4); + if (len < 4) + return -1; + /*### or buf.readUInt32BE(0, true) */ + ch = (str.buffer4[0] << 24); + ch |= (str.buffer4[1] << 16); + ch |= (str.buffer4[2] << 8); + ch |= str.buffer4[3]; + } + str.readcount++; + ch >>>= 0; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + } + /* non-streaming, fall through to resource... */ + case strtype_Resource: + if (str.unicode) { + if (str.isbinary) { + /* cheap big-endian stream */ + if (str.bufpos >= str.bufeof) + return -1; + ch = str.buf[str.bufpos]; + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + if (str.bufpos >= str.bufeof) + return -1; + ch = (ch << 8) | (str.buf[str.bufpos] & 0xFF); + str.bufpos++; + } + else { + /* slightly less cheap UTF8 stream */ + var val0, val1, val2, val3; + if (str.bufpos >= str.bufeof) + return -1; + val0 = str.buf[str.bufpos]; + str.bufpos++; + if (val0 < 0x80) { + ch = val0; + } + else { + if (str.bufpos >= str.bufeof) + return -1; + val1 = str.buf[str.bufpos]; + str.bufpos++; + if ((val1 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xE0) == 0xC0) { + ch = (val0 & 0x1F) << 6; + ch |= (val1 & 0x3F); + } + else { + if (str.bufpos >= str.bufeof) + return -1; + val2 = str.buf[str.bufpos]; + str.bufpos++; + if ((val2 & 0xC0) != 0x80) + return -1; + if ((val0 & 0xF0) == 0xE0) { + ch = (((val0 & 0xF)<<12) & 0x0000F000); + ch |= (((val1 & 0x3F)<<6) & 0x00000FC0); + ch |= (((val2 & 0x3F)) & 0x0000003F); + } + else if ((val0 & 0xF0) == 0xF0) { + if (str.bufpos >= str.bufeof) + return -1; + val3 = str.buf[str.bufpos]; + str.bufpos++; + if ((val3 & 0xC0) != 0x80) + return -1; + ch = (((val0 & 0x7)<<18) & 0x1C0000); + ch |= (((val1 & 0x3F)<<12) & 0x03F000); + ch |= (((val2 & 0x3F)<<6) & 0x000FC0); + ch |= (((val3 & 0x3F)) & 0x00003F); + } + else { + return -1; + } + } + } + } + str.readcount++; + ch >>>= 0; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + /* non-unicode file/resource, fall through to memory... */ + case strtype_Memory: + if (str.bufpos < str.bufeof) { + ch = str.buf[str.bufpos]; + str.bufpos++; + str.readcount++; + if (!want_unicode && ch >= 0x100) + return 63; // return '?' + return ch; + } + else { + return -1; // end of stream + } + default: + return -1; + } +} + +function gli_get_line(str, buf, want_unicode) { + if (!str || !str.readable) + return 0; + + var len = buf.length; + var lx, ch; + var gotnewline; + + switch (str.type) { + case strtype_File: + if (str.streaming) { + if (len == 0) + return 0; + len -= 1; /* for the terminal null */ + gotnewline = false; + for (lx=0; lx= str.bufeof) { + len = 0; + } + else { + if (str.bufpos + len > str.bufeof) { + len = str.bufeof - str.bufpos; + } + } + gotnewline = false; + if (!want_unicode) { + for (lx=0; lx= 0x100) + ch = 63; // ch = '?' + buf[lx] = ch; + gotnewline = (ch == 10); + } + } + else { + for (lx=0; lx= str.bufeof) { + len = 0; + } + else { + if (str.bufpos + len > str.bufeof) { + len = str.bufeof - str.bufpos; + } + } + if (!want_unicode) { + for (lx=0; lx= 0x100) + ch = 63; // ch = '?' + buf[lx] = ch; + } + } + else { + for (lx=0; lx str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + for (ix=0; ix str.bufeof) + str.bufeof = str.bufpos; + } + break; + case strtype_Memory: + len = val.length; + if (len > str.buflen-str.bufpos) + len = str.buflen-str.bufpos; + if (str.unicode || allbytes) { + for (ix=0; ix= 0x100) + ch = 63; // '?' + str.buf[str.bufpos+ix] = ch; + } + } + str.bufpos += len; + if (str.bufpos > str.bufeof) + str.bufeof = str.bufpos; + break; + case strtype_Window: + if (str.win.line_request) + throw('glk_put_jstring: window has pending line request'); + gli_window_put_string(str.win, val); + if (str.win.echostr) + glk_put_jstring_stream(str.win.echostr, val, allbytes); + break; + } +} + +function gli_set_style(str, val) { + if (!str || !str.writable) + throw('gli_set_style: invalid stream'); + + if (val >= Const.style_NUMSTYLES) + val = 0; + + if (str.type == strtype_Window) { + str.win.style = val; + if (str.win.echostr) + gli_set_style(str.win.echostr, val); + } +} + +function gli_set_hyperlink(str, val) { + if (!str || !str.writable) + throw('gli_set_hyperlink: invalid stream'); + + if (str.type == strtype_Window) { + str.win.hyperlink = val; + if (str.win.echostr) + gli_set_hyperlink(str.win.echostr, val); + } +} + +/* The catalog of Glk API functions. */ + +function glk_exit() { + /* For safety, this is fast and idempotent. */ + has_exited = true; + ui_disabled = true; + gli_selectref = null; + if (option_exit_warning) + GlkOte.warning(option_exit_warning); + return DidNotReturn; +} + +function glk_tick() { + /* Do nothing. */ +} + +function glk_gestalt(sel, val) { + return glk_gestalt_ext(sel, val, null); +} + +function glk_gestalt_ext(sel, val, arr) { + switch (sel) { + + case 0: // gestalt_Version + /* This implements Glk spec version 0.7.6. */ + return 0x00000706; + + case 1: // gestalt_CharInput + /* This is not a terrific approximation. Return false for function + keys, control keys, and the high-bit non-printables. For + everything else in the Unicode range, return true. */ + if (val <= Const.keycode_Left && val >= Const.keycode_End) + return 1; + if (val >= 0x100000000-Const.keycode_MAXVAL) + return 0; + if (val > 0x10FFFF) + return 0; + if ((val >= 0 && val < 32) || (val >= 127 && val < 160)) + return 0; + return 1; + + case 2: // gestalt_LineInput + /* Same as the above, except no special keys. */ + if (val > 0x10FFFF) + return 0; + if ((val >= 0 && val < 32) || (val >= 127 && val < 160)) + return 0; + return 1; + + case 3: // gestalt_CharOutput + /* Same thing again. We assume that all printable characters, + as well as the placeholders for nonprintables, are one character + wide. */ + if ((val > 0x10FFFF) + || (val >= 0 && val < 32) + || (val >= 127 && val < 160)) { + if (arr) + arr[0] = 1; + return 0; // gestalt_CharOutput_CannotPrint + } + if (arr) + arr[0] = 1; + return 2; // gestalt_CharOutput_ExactPrint + + case 4: // gestalt_MouseInput + if (val == Const.wintype_TextBuffer) + return 1; + if (val == Const.wintype_Graphics && has_canvas) + return 1; + return 0; + + case 5: // gestalt_Timer + return 1; + + case 6: // gestalt_Graphics + return 1; + + case 7: // gestalt_DrawImage + case 24: // gestalt_DrawImageScale + if (val == Const.wintype_TextBuffer) + return 1; + if (val == Const.wintype_Graphics && has_canvas) + return 1; + return 0; + + case 8: // gestalt_Sound + return 0; + + case 9: // gestalt_SoundVolume + return 0; + + case 10: // gestalt_SoundNotify + return 0; + + case 11: // gestalt_Hyperlinks + return 1; + + case 12: // gestalt_HyperlinkInput + if (val == 3 || val == 4) // TextBuffer or TextGrid + return 1; + else + return 0; + + case 13: // gestalt_SoundMusic + return 0; + + case 14: // gestalt_GraphicsTransparency + return 1; + + case 15: // gestalt_Unicode + return 1; + + case 16: // gestalt_UnicodeNorm + return 1; + + case 17: // gestalt_LineInputEcho + return 1; + + case 18: // gestalt_LineTerminators + return 1; + + case 19: // gestalt_LineTerminatorKey + /* Really this result should be inspected from glkote.js. Since it + isn't, be sure to keep these values in sync with + terminator_key_names. */ + if (val == Const.keycode_Escape) + return 1; + if (val >= Const.keycode_Func12 && val <= Const.keycode_Func1) + return 1; + return 0; + + case 20: // gestalt_DateTime + return 1; + + case 21: // gestalt_Sound2 + return 0; + + case 22: // gestalt_ResourceStream + return 1; + + case 23: // gestalt_GraphicsCharInput + return 0; + + } + + if (option_glk_gestalt_hook) { + var res = option_glk_gestalt_hook(sel, val, arr); + if (res !== undefined) + return res; + } + + return 0; +} + +function glk_window_iterate(win, rockref) { + if (!win) + win = gli_windowlist; + else + win = win.next; + + if (win) { + if (rockref) + rockref.set_value(win.rock); + return win; + } + + if (rockref) + rockref.set_value(0); + return null; +} + +function glk_window_get_rock(win) { + if (!win) + throw('glk_window_get_rock: invalid window'); + return win.rock; +} + +function glk_window_get_root() { + return gli_rootwin; +} + +function glk_window_open(splitwin, method, size, wintype, rock) { + var oldparent, box, val; + var pairwin, newwin; + + if (!gli_rootwin) { + if (splitwin) + throw('glk_window_open: splitwin must be null for first window'); + + oldparent = null; + box = { + left: content_metrics.outspacingx, + top: content_metrics.outspacingy, + right: content_metrics.width-content_metrics.outspacingx, + bottom: content_metrics.height-content_metrics.outspacingy + }; + } + else { + if (!splitwin) + throw('glk_window_open: splitwin must not be null'); + + val = (method & Const.winmethod_DivisionMask); + if (val != Const.winmethod_Fixed && val != Const.winmethod_Proportional) + throw('glk_window_open: invalid method (not fixed or proportional)'); + + val = (method & Const.winmethod_DirMask); + if (val != Const.winmethod_Above && val != Const.winmethod_Below + && val != Const.winmethod_Left && val != Const.winmethod_Right) + throw('glk_window_open: invalid method (bad direction)'); + + box = splitwin.bbox; + + oldparent = splitwin.parent; + if (oldparent && oldparent.type != Const.wintype_Pair) + throw('glk_window_open: parent window is not Pair'); + } + + newwin = gli_new_window(wintype, rock); + + switch (newwin.type) { + case Const.wintype_TextBuffer: + /* accum is a list of strings of a given style; newly-printed text + is pushed onto the list. accumstyle is the style of that text. + Anything printed in a different style (or hyperlink value) + triggers a call to gli_window_buffer_deaccumulate, which cleans + out accum and adds the results to the content array. The content + is in GlkOte format. + */ + newwin.accum = []; + newwin.accumstyle = null; + newwin.accumhyperlink = 0; + newwin.content = []; + newwin.clearcontent = false; + newwin.reserve = []; /* autosave of recent content */ + break; + case Const.wintype_TextGrid: + /* lines is a list of line objects. A line looks like + { chars: [...], styles: [...], hyperlinks: [...], dirty: bool }. + */ + newwin.gridwidth = 0; + newwin.gridheight = 0; + newwin.lines = []; + newwin.cursorx = 0; + newwin.cursory = 0; + break; + case Const.wintype_Graphics: + if (!has_canvas) { + /* Graphics windows not supported; silently return null */ + gli_delete_window(newwin); + return null; + } + newwin.content = []; + newwin.reserve = []; /* autosave of recent content */ + break; + case Const.wintype_Blank: + break; + case Const.wintype_Pair: + throw('glk_window_open: cannot open pair window directly') + default: + /* Silently return null */ + gli_delete_window(newwin); + return null; + } + + if (!splitwin) { + gli_rootwin = newwin; + gli_window_rearrange(newwin, box); + } + else { + /* create pairwin, with newwin as the key */ + pairwin = gli_new_window(Const.wintype_Pair, 0); + pairwin.pair_dir = method & Const.winmethod_DirMask; + pairwin.pair_division = method & Const.winmethod_DivisionMask; + pairwin.pair_key = newwin; + pairwin.pair_keydamage = false; + pairwin.pair_size = size; + pairwin.pair_hasborder = ((method & Const.winmethod_BorderMask) == Const.winmethod_Border); + pairwin.pair_vertical = (pairwin.pair_dir == Const.winmethod_Left || pairwin.pair_dir == Const.winmethod_Right); + pairwin.pair_backward = (pairwin.pair_dir == Const.winmethod_Left || pairwin.pair_dir == Const.winmethod_Above); + + pairwin.child1 = splitwin; + pairwin.child2 = newwin; + splitwin.parent = pairwin; + newwin.parent = pairwin; + pairwin.parent = oldparent; + + if (oldparent) { + if (oldparent.child1 == splitwin) + oldparent.child1 = pairwin; + else + oldparent.child2 = pairwin; + } + else { + gli_rootwin = pairwin; + } + + gli_window_rearrange(pairwin, box); + } + + return newwin; +} + +function glk_window_close(win, statsref) { + if (!win) + throw('glk_window_close: invalid window'); + + if (win === gli_rootwin || !win.parent) { + /* close the root window, which means all windows. */ + + gli_rootwin = null; + + /* begin (simpler) closation */ + + gli_stream_fill_result(win.str, statsref); + gli_window_close(win, true); + } + else { + /* have to jigger parent */ + var pairwin, grandparwin, sibwin, box, wx, keydamage_flag; + + pairwin = win.parent; + if (win === pairwin.child1) + sibwin = pairwin.child2; + else if (win === pairwin.child2) + sibwin = pairwin.child1; + else + throw('glk_window_close: window tree is corrupted'); + + box = pairwin.bbox; + + grandparwin = pairwin.parent; + if (!grandparwin) { + gli_rootwin = sibwin; + sibwin.parent = null; + } + else { + if (grandparwin.child1 === pairwin) + grandparwin.child1 = sibwin; + else + grandparwin.child2 = sibwin; + sibwin.parent = grandparwin; + } + + /* Begin closation */ + + gli_stream_fill_result(win.str, statsref); + + /* Close the child window (and descendants), so that key-deletion can + crawl up the tree to the root window. */ + gli_window_close(win, true); + + /* This probably isn't necessary, but the child *is* gone, so just + in case. */ + if (win === pairwin.child1) { + pairwin.child1 = null; + } + else if (win === pairwin.child2) { + pairwin.child2 = null; + } + + /* Now we can delete the parent pair. */ + gli_window_close(pairwin, false); + + keydamage_flag = false; + for (wx=sibwin; wx; wx=wx.parent) { + if (wx.type == Const.wintype_Pair) { + if (wx.pair_keydamage) { + keydamage_flag = true; + wx.pair_keydamage = false; + } + } + } + + if (keydamage_flag) { + box = content_box; + gli_window_rearrange(gli_rootwin, box); + } + else { + gli_window_rearrange(sibwin, box); + } + } +} + +function glk_window_get_size(win, widthref, heightref) { + if (!win) + throw('glk_window_get_size: invalid window'); + + var wid = 0; + var hgt = 0; + var boxwidth, boxheight; + + switch (win.type) { + + case Const.wintype_TextGrid: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = Math.max(0, Math.floor((boxwidth-content_metrics.gridmarginx) / content_metrics.gridcharwidth)); + hgt = Math.max(0, Math.floor((boxheight-content_metrics.gridmarginy) / content_metrics.gridcharheight)); + break; + + case Const.wintype_TextBuffer: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = Math.max(0, Math.floor((boxwidth-content_metrics.buffermarginx) / content_metrics.buffercharwidth)); + hgt = Math.max(0, Math.floor((boxheight-content_metrics.buffermarginy) / content_metrics.buffercharheight)); + break; + + case Const.wintype_Graphics: + boxwidth = win.bbox.right - win.bbox.left; + boxheight = win.bbox.bottom - win.bbox.top; + wid = boxwidth - content_metrics.graphicsmarginx; + hgt = boxheight - content_metrics.graphicsmarginy; + break; + } + + if (widthref) + widthref.set_value(wid); + if (heightref) + heightref.set_value(hgt); +} + +function glk_window_set_arrangement(win, method, size, keywin) { + var wx, newdir, newvertical, newbackward; + + if (!win) + throw('glk_window_set_arrangement: invalid window'); + if (win.type != Const.wintype_Pair) + throw('glk_window_set_arrangement: not a pair window'); + + if (keywin) { + if (keywin.type == Const.wintype_Pair) + throw('glk_window_set_arrangement: keywin cannot be a pair window'); + for (wx=keywin; wx; wx=wx.parent) { + if (wx == win) + break; + } + if (!wx) + throw('glk_window_set_arrangement: keywin must be a descendant'); + } + + newdir = method & Const.winmethod_DirMask; + newvertical = (newdir == Const.winmethod_Left || newdir == Const.winmethod_Right); + newbackward = (newdir == Const.winmethod_Left || newdir == Const.winmethod_Above); + if (!keywin) + keywin = win.pair_key; + + if (newvertical && !win.pair_vertical) + throw('glk_window_set_arrangement: split must stay horizontal'); + if (!newvertical && win.pair_vertical) + throw('glk_window_set_arrangement: split must stay vertical'); + + if (keywin && keywin.type == Const.wintype_Blank + && (method & Const.winmethod_DivisionMask) == Const.winmethod_Fixed) + throw('glk_window_set_arrangement: a blank window cannot have a fixed size'); + + if ((newbackward && !win.pair_backward) || (!newbackward && win.pair_backward)) { + /* switch the children */ + wx = win.child1; + win.child1 = win.child2; + win.child2 = wx; + } + + /* set up everything else */ + win.pair_dir = newdir; + win.pair_division = (method & Const.winmethod_DivisionMask); + win.pair_key = keywin; + win.pair_size = size; + + win.pair_hasborder = ((method & Const.winmethod_BorderMask) == Const.winmethod_Border); + win.pair_vertical = (win.pair_dir == Const.winmethod_Left || win.pair_dir == Const.winmethod_Right); + win.pair_backward = (win.pair_dir == Const.winmethod_Left || win.pair_dir == Const.winmethod_Above); + + gli_window_rearrange(win, win.bbox); +} + +function glk_window_get_arrangement(win, methodref, sizeref, keywinref) { + if (!win) + throw('glk_window_get_arrangement: invalid window'); + if (win.type != Const.wintype_Pair) + throw('glk_window_get_arrangement: not a pair window'); + + if (sizeref) + sizeref.set_value(win.pair_size); + if (keywinref) + keywinref.set_value(win.pair_key); + if (methodref) + methodref.set_value(win.pair_dir | win.pair_division | (win.pair_hasborder ? Const.winmethod_Border : Const.winmethod_NoBorder)); +} + +function glk_window_get_type(win) { + if (!win) + throw('glk_window_get_type: invalid window'); + return win.type; +} + +function glk_window_get_parent(win) { + if (!win) + throw('glk_window_get_parent: invalid window'); + return win.parent; +} + +function glk_window_clear(win) { + var ix, cx, lineobj; + + if (!win) + throw('glk_window_clear: invalid window'); + + if (win.line_request) { + throw('glk_window_clear: window has pending line request'); + } + + switch (win.type) { + case Const.wintype_TextBuffer: + win.accum.length = 0; + win.accumstyle = null; + win.accumhyperlink = 0; + win.content.length = 0; + win.clearcontent = true; + break; + case Const.wintype_TextGrid: + win.cursorx = 0; + win.cursory = 0; + for (ix=0; ix str.bufeof) + pos = str.bufeof; + str.bufpos = pos; + } +} + +function glk_stream_get_position(str) { + if (!str) + throw('glk_stream_get_position: invalid stream'); + + switch (str.type) { + case strtype_File: + if (str.streaming) { + return str.fstream.ftell(); + } + /* fall through to memory... */ + case strtype_Resource: + /* fall through to memory... */ + case strtype_Memory: + return str.bufpos; + default: + return 0; + } +} + +function glk_stream_set_current(str) { + gli_currentstr = str; +} + +function glk_stream_get_current() { + return gli_currentstr; +} + +function glk_fileref_create_temp(usage, rock) { + var Dialog = GlkOte.getlibrary('Dialog'); + var filetype = (usage & Const.fileusage_TypeMask); + var filetypename = FileTypeMap[filetype]; + var ref = Dialog.file_construct_temp_ref(filetypename); + var fref = gli_new_fileref(ref.filename, usage, rock, ref); + return fref; +} + +function glk_fileref_create_by_name(usage, filename, rock) { + var Dialog = GlkOte.getlibrary('Dialog'); + + /* Filenames that do not come from the user must be cleaned up. */ + filename = Dialog.file_clean_fixed_name(filename, (usage & Const.fileusage_TypeMask)); + + var fref = gli_new_fileref(filename, usage, rock, null); + return fref; +} + +function glk_fileref_create_by_prompt(usage, fmode, rock) { + var modename; + + var filetype = (usage & Const.fileusage_TypeMask); + var filetypename = FileTypeMap[filetype]; + if (!filetypename) { + filetypename = 'xxx'; + } + + switch (fmode) { + case Const.filemode_Write: + modename = 'write'; + break; + case Const.filemode_ReadWrite: + modename = 'readwrite'; + break; + case Const.filemode_WriteAppend: + modename = 'writeappend'; + break; + case Const.filemode_Read: + default: + modename = 'read'; + break; + } + + var special = { + type: 'fileref_prompt', + filetype: filetypename, + filemode: modename + }; + var callback = { + usage: usage, + rock: rock + }; + + if (filetype == Const.fileusage_SavedGame) + special.gameid = VM.get_signature(); + + ui_specialinput = special; + ui_specialcallback = callback; + gli_selectref = null; + return DidNotReturn; +} + +function gli_fileref_create_by_prompt_callback(obj) { + var ref = obj.value; + /* This "value" field will be a dialog.js fileref object if we are + connected to GlkOte. However, if we are connected to RegTest, + it will be a plain string. We attempt to handle both cases. */ + + var usage = ui_specialcallback.usage; + var rock = ui_specialcallback.rock; + + var fref = null; + if (ref && typeof(ref) == 'object') { + fref = gli_new_fileref(ref.filename, usage, rock, ref); + } + else if (ref && typeof(ref) == 'string') { + fref = gli_new_fileref(ref, usage, rock, null); + } + + ui_specialinput = null; + ui_specialcallback = null; + + if (GiDispa) + GiDispa.prepare_resume(fref); + VM.resume(fref); +} + +function glk_fileref_destroy(fref) { + if (!fref) + throw('glk_fileref_destroy: invalid fileref'); + gli_delete_fileref(fref); +} + +function glk_fileref_iterate(fref, rockref) { + if (!fref) + fref = gli_filereflist; + else + fref = fref.next; + + if (fref) { + if (rockref) + rockref.set_value(fref.rock); + return fref; + } + + if (rockref) + rockref.set_value(0); + return null; +} + +function glk_fileref_get_rock(fref) { + if (!fref) + throw('glk_fileref_get_rock: invalid fileref'); + return fref.rock; +} + +function glk_fileref_delete_file(fref) { + var Dialog = GlkOte.getlibrary('Dialog'); + if (!fref) + throw('glk_fileref_delete_file: invalid fileref'); + Dialog.file_remove_ref(fref.ref); +} + +function glk_fileref_does_file_exist(fref) { + var Dialog = GlkOte.getlibrary('Dialog'); + if (!fref) + throw('glk_fileref_does_file_exist: invalid fileref'); + if (Dialog.file_ref_exists(fref.ref)) + return 1; + else + return 0; +} + +function glk_fileref_create_from_fileref(usage, oldfref, rock) { + if (!oldfref) + throw('glk_fileref_create_from_fileref: invalid fileref'); + + var fref = gli_new_fileref(oldfref.filename, usage, rock, null); + return fref; +} + +function glk_put_char(ch) { + gli_put_char(gli_currentstr, ch & 0xFF); +} + +function glk_put_char_stream(str, ch) { + gli_put_char(str, ch & 0xFF); +} + +function glk_put_string(val) { + glk_put_jstring_stream(gli_currentstr, val, true); +} + +function glk_put_string_stream(str, val) { + glk_put_jstring_stream(str, val, true); +} + +function glk_put_buffer(arr) { + arr = TrimArrayToBytes(arr); + gli_put_array(gli_currentstr, arr, true); +} + +function glk_put_buffer_stream(str, arr) { + arr = TrimArrayToBytes(arr); + gli_put_array(str, arr, true); +} + +function glk_set_style(val) { + gli_set_style(gli_currentstr, val); +} + +function glk_set_style_stream(str, val) { + gli_set_style(str, val); +} + +function glk_get_char_stream(str) { + if (!str) + throw('glk_get_char_stream: invalid stream'); + return gli_get_char(str, false); +} + +function glk_get_line_stream(str, buf) { + if (!str) + throw('glk_get_line_stream: invalid stream'); + return gli_get_line(str, buf, false); +} + +function glk_get_buffer_stream(str, buf) { + if (!str) + throw('glk_get_buffer_stream: invalid stream'); + return gli_get_buffer(str, buf, false); +} + +function glk_char_to_lower(val) { + if (val >= 0x41 && val <= 0x5A) + return val + 0x20; + if (val >= 0xC0 && val <= 0xDE && val != 0xD7) + return val + 0x20; + return val; +} + +function glk_char_to_upper(val) { + if (val >= 0x61 && val <= 0x7A) + return val - 0x20; + if (val >= 0xE0 && val <= 0xFE && val != 0xF7) + return val - 0x20; + return val; +} + +/* Style hints are not supported. We will use the new style system. */ +function glk_stylehint_set(wintype, styl, hint, value) { } +function glk_stylehint_clear(wintype, styl, hint) { } +function glk_style_distinguish(win, styl1, styl2) { + return 0; +} +function glk_style_measure(win, styl, hint, resultref) { + if (resultref) + resultref.set_value(0); + return 0; +} + +function glk_select(eventref) { + gli_selectref = eventref; + return DidNotReturn; +} + +function glk_select_poll(eventref) { + /* Because the Javascript interpreter is single-threaded, we cannot + have gotten a timer event since the last glk_select call. */ + + eventref.set_field(0, Const.evtype_None); + eventref.set_field(1, null); + eventref.set_field(2, 0); + eventref.set_field(3, 0); + + if (gli_timer_interval) { + var now = Date.now(); + if (now - gli_timer_started > gli_timer_interval) { + /* We're past the timer interval, even though we got no + event. Let's pretend we did, reset it, and return a + timer event. */ + gli_timer_started = Date.now(); + /* Resend timer request at next update. */ + gli_timer_lastsent = null; + + eventref.set_field(0, Const.evtype_Timer); + } + } +} + +function glk_request_line_event(win, buf, initlen) { + if (!win) + throw('glk_request_line_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_line_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + if (initlen) { + /* This will be copied into the next update. */ + var ls = buf.slice(0, initlen); + if (!current_partial_outputs) + current_partial_outputs = {}; + current_partial_outputs[win.disprock] = ByteArrayToString(ls); + } + win.line_request = true; + win.line_request_uni = false; + if (win.type == Const.wintype_TextBuffer) + win.request_echo_line_input = win.echo_line_input; + else + win.request_echo_line_input = true; + win.input_generation = event_generation; + win.linebuf = buf; + if (GiDispa) + GiDispa.retain_array(buf); + } + else { + throw('glk_request_line_event: window does not support keyboard input'); + } +} + +function glk_cancel_line_event(win, eventref) { + if (!win) + throw('glk_cancel_line_event: invalid window'); + + if (!win.line_request) { + if (eventref) { + eventref.set_field(0, Const.evtype_None); + eventref.set_field(1, null); + eventref.set_field(2, 0); + eventref.set_field(3, 0); + } + return; + } + + var input = ""; + var ix, val; + + if (current_partial_inputs) { + val = current_partial_inputs[win.disprock]; + if (val) + input = val; + } + + if (input.length > win.linebuf.length) + input = input.slice(0, win.linebuf.length); + + if (win.request_echo_line_input) { + ix = win.style; + gli_set_style(win.str, Const.style_Input); + gli_window_put_string(win, input); + if (win.echostr) + glk_put_jstring_stream(win.echostr, input); + gli_set_style(win.str, ix); + gli_window_put_string(win, "\n"); + if (win.echostr) + glk_put_jstring_stream(win.echostr, "\n"); + } + + for (ix=0; ix img.winmaxwidth) { + if (img.height !== undefined) + img.height = img.height * (img.winmaxwidth / img.widthratio); + img.widthratio = img.winmaxwidth; + } + img.winmaxwidth = null; + } + } + + switch (win.type) { + case Const.wintype_TextBuffer: + var alignment = 'inlineup'; + switch (val1) { + case Const.imagealign_InlineUp: + alignment = 'inlineup'; + break; + case Const.imagealign_InlineDown: + alignment = 'inlinedown'; + break; + case Const.imagealign_InlineCenter: + alignment = 'inlinecenter'; + break; + case Const.imagealign_MarginLeft: + alignment = 'marginleft'; + break; + case Const.imagealign_MarginRight: + alignment = 'marginright'; + break; + } + img.alignment = alignment; + if (win.hyperlink) + img.hyperlink = win.hyperlink; + gli_window_buffer_put_special(win, img); + return 1; + + case Const.wintype_Graphics: + img.x = val1; + img.y = val2; + /* width, height already set */ + win.content.push(img); + return 1; + } + + return 0; +} + +function glk_window_flow_break(win) { + if (!win) + throw('glk_window_flow_break: invalid window'); + + if (win.type == Const.wintype_TextBuffer) + gli_window_buffer_put_special(win, undefined, true); +} + +function glk_window_erase_rect(win, left, top, width, height) { + if (!win) + throw('glk_window_erase_rect: invalid window'); + if (win.type != Const.wintype_Graphics) + throw('glk_window_erase_rect: not a graphics window'); + + win.content.push({ special:'fill', x:left, y:top, width:width, height:height }); +} + +function glk_window_fill_rect(win, color, left, top, width, height) { + if (!win) + throw('glk_window_fill_rect: invalid window'); + if (win.type != Const.wintype_Graphics) + throw('glk_window_fill_rect: not a graphics window'); + + var colstr = gli_color_to_css(color); + win.content.push({ special:'fill', color:colstr, x:left, y:top, width:width, height:height }); +} + +function glk_window_set_background_color(win, color) { + if (!win) + throw('glk_window_set_background_color: invalid window'); + if (win.type != Const.wintype_Graphics) + throw('glk_window_set_background_color: not a graphics window'); + + var colstr = gli_color_to_css(color); + win.content.push({ special:'setcolor', color:colstr }); +} + +/* Convert a Glk numeric color value to a CSS-compatible string. +*/ +function gli_color_to_css(color) { + var res = (color & 0xFFFFFF).toString(16); + while (res.length < 6) { + res = '0' + res; + } + return '#' + res.toUpperCase(); +} + +function glk_schannel_iterate(schan, rockref) { + if (!schan) + schan = gli_schannellist; + else + schan = schan.next; + + if (schan) { + if (rockref) + rockref.set_value(schan.rock); + return schan; + } + + if (rockref) + rockref.set_value(0); + return null; +} + +function glk_schannel_get_rock(schan) { + if (!schan) + throw('glk_schannel_get_rock: invalid schannel'); + return schan.rock; +} + +function glk_schannel_create(rock) { + return null; +} + +function glk_schannel_destroy(schan) { + throw('glk_schannel_destroy: invalid schannel'); +} + +function glk_schannel_play(schan, sndid) { + throw('glk_schannel_play: invalid schannel'); +} + +function glk_schannel_play_ext(schan, sndid, repeats, notify) { + throw('glk_schannel_play_ext: invalid schannel'); +} + +function glk_schannel_stop(schan) { + throw('glk_schannel_stop: invalid schannel'); +} + +function glk_schannel_set_volume(schan, vol) { + throw('glk_schannel_set_volume: invalid schannel'); +} + +function glk_sound_load_hint(sndid, flag) { +} + +function glk_schannel_create_ext(rock, vol) { + return null; +} + +function glk_schannel_play_multi(schans, sndids, notify) { + throw('glk_schannel_play_multi: invalid schannel'); +} + +function glk_schannel_pause(schan) { + throw('glk_schannel_pause: invalid schannel'); +} + +function glk_schannel_unpause(schan) { + throw('glk_schannel_unpause: invalid schannel'); +} + +function glk_schannel_set_volume_ext(schan, vol, duration, notify) { + throw('glk_schannel_set_volume_ext: invalid schannel'); +} + +function glk_set_hyperlink(val) { + gli_set_hyperlink(gli_currentstr, val); +} + +function glk_set_hyperlink_stream(str, val) { + gli_set_hyperlink(str, val); +} + +function glk_request_hyperlink_event(win) { + if (!win) + throw('glk_request_hyperlink_event: invalid window'); + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + win.hyperlink_request = true; + } +} + +function glk_cancel_hyperlink_event(win) { + if (!win) + throw('glk_cancel_hyperlink_event: invalid window'); + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + win.hyperlink_request = false; + } +} + +function glk_buffer_to_lower_case_uni(arr, numchars) { + var ix, jx, pos, val, origval; + var arrlen = arr.length; + var src = arr.slice(0, numchars); + + if (arrlen < numchars) + throw('buffer_to_lower_case_uni: numchars exceeds array length'); + + pos = 0; + for (ix=0; ix= pos) + break; + grpstart = ix; + while (ix < pos && unicode_combin_table[arr[ix]]) + ix++; + grpend = ix; + if (grpend - grpstart >= 2) { + /* Sort this group. */ + for (jx = grpend-1; jx > grpstart; jx--) { + for (kx = grpstart; kx < jx; kx++) { + if (unicode_combin_table[arr[kx]] > unicode_combin_table[arr[kx+1]]) { + tmp = arr[kx]; + arr[kx] = arr[kx+1]; + arr[kx+1] = tmp; + } + } + } + } + } + + return pos; +} + +function gli_buffer_canon_compose_uni(arr, numchars) { + /* The algorithm for canonically composing characters in a string: + for each base character, compare it to all the following + combining characters (up to the next base character). If they're + composable, compose them. Repeat until no more pairs are found. */ + + var ix, jx, curch, newch, curclass, newclass, map, pos; + + if (numchars == 0) + return 0; + + pos = 0; + curch = arr[0]; + curclass = unicode_combin_table[curch]; + if (curclass) + curclass = 999; // just in case the first character is a combiner + ix = 1; + jx = ix; + while (true) { + if (jx >= numchars) { + arr[pos] = curch; + pos = ix; + break; + } + newch = arr[jx]; + newclass = unicode_combin_table[newch]; + map = unicode_compo_table[curch]; + if (map !== undefined && map[newch] !== undefined + && (!curclass || (newclass && curclass < newclass))) { + curch = map[newch]; + arr[pos] = curch; + } + else { + if (!newclass) { + pos = ix; + curch = newch; + } + curclass = newclass; + arr[ix] = newch; + ix++; + } + jx++; + } + + return pos; +} + +function glk_buffer_canon_decompose_uni(arr, numchars) { + var arrlen = arr.length; + var len; + + len = gli_buffer_canon_decompose_uni(arr, numchars); + + /* in case we stretched the array */ + arr.length = arrlen; + + return len; +} + +function glk_buffer_canon_normalize_uni(arr, numchars) { + var arrlen = arr.length; + var len; + + len = gli_buffer_canon_decompose_uni(arr, numchars); + len = gli_buffer_canon_compose_uni(arr, len); + + /* in case we stretched the array */ + arr.length = arrlen; + + return len; +} + +function glk_put_char_uni(ch) { + gli_put_char(gli_currentstr, ch); +} + +function glk_put_string_uni(val) { + glk_put_jstring_stream(gli_currentstr, val, false); +} + +function glk_put_buffer_uni(arr) { + gli_put_array(gli_currentstr, arr, false); +} + +function glk_put_char_stream_uni(str, ch) { + gli_put_char(str, ch); +} + +function glk_put_string_stream_uni(str, val) { + glk_put_jstring_stream(str, val, false); +} + +function glk_put_buffer_stream_uni(str, arr) { + gli_put_array(str, arr, false); +} + +function glk_get_char_stream_uni(str) { + if (!str) + throw('glk_get_char_stream_uni: invalid stream'); + return gli_get_char(str, true); +} + +function glk_get_buffer_stream_uni(str, buf) { + if (!str) + throw('glk_get_buffer_stream_uni: invalid stream'); + return gli_get_buffer(str, buf, true); +} + +function glk_get_line_stream_uni(str, buf) { + if (!str) + throw('glk_get_line_stream_uni: invalid stream'); + return gli_get_line(str, buf, true); +} + +function glk_stream_open_file_uni(fref, fmode, rock) { + var Dialog = GlkOte.getlibrary('Dialog'); + + if (!fref) + throw('glk_stream_open_file_uni: invalid fileref'); + + var str; + var fstream; + + if (fmode != Const.filemode_Read + && fmode != Const.filemode_Write + && fmode != Const.filemode_ReadWrite + && fmode != Const.filemode_WriteAppend) + throw('glk_stream_open_file_uni: illegal filemode'); + + if (fmode == Const.filemode_Read && !Dialog.file_ref_exists(fref.ref)) + return null; + + if (!Dialog.streaming) { + var content = null; + if (fmode != Const.filemode_Write) { + content = Dialog.file_read(fref.ref); + } + if (content == null) { + content = []; + if (fmode != Const.filemode_Read) { + /* We just created this file. (Or perhaps we're in Write mode + and we're truncating.) Write immediately, to create it and + get the creation date right. */ + Dialog.file_write(fref.ref, '', true); + } + } + if (content.length == null) + throw('glk_stream_open_file_uni: data read had no length'); + } + else { + fstream = Dialog.file_fopen(fmode, fref.ref); + if (!fstream) + return null; + } + + str = gli_new_stream(strtype_File, + (fmode != Const.filemode_Write), + (fmode != Const.filemode_Read), + rock); + str.unicode = true; + str.isbinary = !fref.textmode; + str.ref = fref.ref; + str.origfmode = fmode; + + if (!Dialog.streaming) { + str.streaming = false; + str.buf = content; + str.buflen = 0xFFFFFFFF; /* enormous */ + if (fmode == Const.filemode_Write) + str.bufeof = 0; + else + str.bufeof = content.length; + if (fmode == Const.filemode_WriteAppend) + str.bufpos = str.bufeof; + else + str.bufpos = 0; + } + else { + str.streaming = true; + str.fstream = fstream; + /* We'll want a Buffer object around for short and writes. */ + str.buffer4 = fstream.BufferClass.alloc(4); + } + + return str; +} + +function glk_stream_open_memory_uni(buf, fmode, rock) { + var str; + + if (fmode != Const.filemode_Read + && fmode != Const.filemode_Write + && fmode != Const.filemode_ReadWrite) + throw('glk_stream_open_memory: illegal filemode'); + + str = gli_new_stream(strtype_Memory, + (fmode != Const.filemode_Write), + (fmode != Const.filemode_Read), + rock); + str.unicode = true; + + if (buf) { + str.buf = buf; + str.buflen = buf.length; + str.bufpos = 0; + if (fmode == Const.filemode_Write) + str.bufeof = 0; + else + str.bufeof = str.buflen; + if (GiDispa) + GiDispa.retain_array(buf); + } + + return str; +} + +function glk_request_char_event_uni(win) { + if (!win) + throw('glk_request_char_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_char_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + win.char_request = true; + win.char_request_uni = true; + win.input_generation = event_generation; + } + else { + /* ### wintype_Graphics could accept char input if we set up the focus to allow it. See gestalt_GraphicsCharInput. */ + throw('glk_request_char_event: window does not support keyboard input'); + } +} + +function glk_request_line_event_uni(win, buf, initlen) { + if (!win) + throw('glk_request_line_event: invalid window'); + if (win.char_request || win.line_request) + throw('glk_request_line_event: window already has keyboard request'); + + if (win.type == Const.wintype_TextBuffer + || win.type == Const.wintype_TextGrid) { + if (initlen) { + /* This will be copied into the next update. */ + var ls = buf.slice(0, initlen); + if (!current_partial_outputs) + current_partial_outputs = {}; + current_partial_outputs[win.disprock] = UniArrayToString(ls); + } + win.line_request = true; + win.line_request_uni = true; + if (win.type == Const.wintype_TextBuffer) + win.request_echo_line_input = win.echo_line_input; + else + win.request_echo_line_input = true; + win.input_generation = event_generation; + win.linebuf = buf; + if (GiDispa) + GiDispa.retain_array(buf); + } + else { + throw('glk_request_line_event: window does not support keyboard input'); + } +} + +function glk_current_time(timevalref) { + var now = new Date().getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_current_simple_time(factor) { + var now = new Date().getTime(); + return Math.floor(now / (factor * 1000)); +} + +function glk_time_to_date_utc(timevalref, dateref) { + var now = timevalref.get_field(0) * 4294967296000 + timevalref.get_field(1) * 1000 + timevalref.get_field(2) / 1000; + var obj = new Date(now); + + dateref.set_field(0, obj.getUTCFullYear()) + dateref.set_field(1, 1+obj.getUTCMonth()) + dateref.set_field(2, obj.getUTCDate()) + dateref.set_field(3, obj.getUTCDay()) + dateref.set_field(4, obj.getUTCHours()) + dateref.set_field(5, obj.getUTCMinutes()) + dateref.set_field(6, obj.getUTCSeconds()) + dateref.set_field(7, 1000*obj.getUTCMilliseconds()) +} + +function glk_time_to_date_local(timevalref, dateref) { + var now = timevalref.get_field(0) * 4294967296000 + timevalref.get_field(1) * 1000 + timevalref.get_field(2) / 1000; + var obj = new Date(now); + + dateref.set_field(0, obj.getFullYear()) + dateref.set_field(1, 1+obj.getMonth()) + dateref.set_field(2, obj.getDate()) + dateref.set_field(3, obj.getDay()) + dateref.set_field(4, obj.getHours()) + dateref.set_field(5, obj.getMinutes()) + dateref.set_field(6, obj.getSeconds()) + dateref.set_field(7, 1000*obj.getMilliseconds()) +} + +function glk_simple_time_to_date_utc(time, factor, dateref) { + var now = time*(1000*factor); + var obj = new Date(now); + + dateref.set_field(0, obj.getUTCFullYear()) + dateref.set_field(1, 1+obj.getUTCMonth()) + dateref.set_field(2, obj.getUTCDate()) + dateref.set_field(3, obj.getUTCDay()) + dateref.set_field(4, obj.getUTCHours()) + dateref.set_field(5, obj.getUTCMinutes()) + dateref.set_field(6, obj.getUTCSeconds()) + dateref.set_field(7, 1000*obj.getUTCMilliseconds()) +} + +function glk_simple_time_to_date_local(time, factor, dateref) { + var now = time*(1000*factor); + var obj = new Date(now); + + dateref.set_field(0, obj.getFullYear()) + dateref.set_field(1, 1+obj.getMonth()) + dateref.set_field(2, obj.getDate()) + dateref.set_field(3, obj.getDay()) + dateref.set_field(4, obj.getHours()) + dateref.set_field(5, obj.getMinutes()) + dateref.set_field(6, obj.getSeconds()) + dateref.set_field(7, 1000*obj.getMilliseconds()) +} + +function glk_date_to_time_utc(dateref, timevalref) { + var obj = new Date(0); + + obj.setUTCFullYear(dateref.get_field(0)); + obj.setUTCMonth(dateref.get_field(1)-1); + obj.setUTCDate(dateref.get_field(2)); + obj.setUTCHours(dateref.get_field(4)); + obj.setUTCMinutes(dateref.get_field(5)); + obj.setUTCSeconds(dateref.get_field(6)); + obj.setUTCMilliseconds(dateref.get_field(7)/1000); + + var now = obj.getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_date_to_time_local(dateref, timevalref) { + var obj = new Date( + dateref.get_field(0), dateref.get_field(1)-1, dateref.get_field(2), + dateref.get_field(4), dateref.get_field(5), dateref.get_field(6), + dateref.get_field(7)/1000); + + var now = obj.getTime(); + var usec; + + timevalref.set_field(0, Math.floor(now / 4294967296000)); + timevalref.set_field(1, Math.floor(now / 1000) >>>0); + usec = Math.floor((now % 1000) * 1000); + if (usec < 0) + usec = 1000000 + usec; + timevalref.set_field(2, usec); +} + +function glk_date_to_simple_time_utc(dateref, factor) { + var obj = new Date(0); + + obj.setUTCFullYear(dateref.get_field(0)); + obj.setUTCMonth(dateref.get_field(1)-1); + obj.setUTCDate(dateref.get_field(2)); + obj.setUTCHours(dateref.get_field(4)); + obj.setUTCMinutes(dateref.get_field(5)); + obj.setUTCSeconds(dateref.get_field(6)); + obj.setUTCMilliseconds(dateref.get_field(7)/1000); + + var now = obj.getTime(); + return Math.floor(now / (factor * 1000)); +} + +function glk_date_to_simple_time_local(dateref, factor) { + var obj = new Date( + dateref.get_field(0), dateref.get_field(1)-1, dateref.get_field(2), + dateref.get_field(4), dateref.get_field(5), dateref.get_field(6), + dateref.get_field(7)/1000); + + var now = obj.getTime(); + return Math.floor(now / (factor * 1000)); +} + +/* End of Glk namespace function. Return the object which will + become the Glk global. */ +return { + classname: 'Glk', + version: '2.3.7', /* GlkOte/GlkApi version */ + init : init, + inited : is_inited, + update : update, + getlibrary : get_library, + save_allstate : save_allstate, + restore_allstate : restore_allstate, + fatal_error : fatal_error, + + byte_array_to_string : ByteArrayToString, + uni_array_to_string : UniArrayToString, + Const : Const, + RefBox : RefBox, + RefStruct : RefStruct, + DidNotReturn : DidNotReturn, + call_may_not_return : call_may_not_return, + + glk_put_jstring : glk_put_jstring, + glk_put_jstring_stream : glk_put_jstring_stream, + + glk_exit : glk_exit, + glk_tick : glk_tick, + glk_gestalt : glk_gestalt, + glk_gestalt_ext : glk_gestalt_ext, + glk_window_iterate : glk_window_iterate, + glk_window_get_rock : glk_window_get_rock, + glk_window_get_root : glk_window_get_root, + glk_window_open : glk_window_open, + glk_window_close : glk_window_close, + glk_window_get_size : glk_window_get_size, + glk_window_set_arrangement : glk_window_set_arrangement, + glk_window_get_arrangement : glk_window_get_arrangement, + glk_window_get_type : glk_window_get_type, + glk_window_get_parent : glk_window_get_parent, + glk_window_clear : glk_window_clear, + glk_window_move_cursor : glk_window_move_cursor, + glk_window_get_stream : glk_window_get_stream, + glk_window_set_echo_stream : glk_window_set_echo_stream, + glk_window_get_echo_stream : glk_window_get_echo_stream, + glk_set_window : glk_set_window, + glk_window_get_sibling : glk_window_get_sibling, + glk_stream_iterate : glk_stream_iterate, + glk_stream_get_rock : glk_stream_get_rock, + glk_stream_open_file : glk_stream_open_file, + glk_stream_open_memory : glk_stream_open_memory, + glk_stream_close : glk_stream_close, + glk_stream_set_position : glk_stream_set_position, + glk_stream_get_position : glk_stream_get_position, + glk_stream_set_current : glk_stream_set_current, + glk_stream_get_current : glk_stream_get_current, + glk_fileref_create_temp : glk_fileref_create_temp, + glk_fileref_create_by_name : glk_fileref_create_by_name, + glk_fileref_create_by_prompt : glk_fileref_create_by_prompt, + glk_fileref_destroy : glk_fileref_destroy, + glk_fileref_iterate : glk_fileref_iterate, + glk_fileref_get_rock : glk_fileref_get_rock, + glk_fileref_delete_file : glk_fileref_delete_file, + glk_fileref_does_file_exist : glk_fileref_does_file_exist, + glk_fileref_create_from_fileref : glk_fileref_create_from_fileref, + glk_put_char : glk_put_char, + glk_put_char_stream : glk_put_char_stream, + glk_put_string : glk_put_string, + glk_put_string_stream : glk_put_string_stream, + glk_put_buffer : glk_put_buffer, + glk_put_buffer_stream : glk_put_buffer_stream, + glk_set_style : glk_set_style, + glk_set_style_stream : glk_set_style_stream, + glk_get_char_stream : glk_get_char_stream, + glk_get_line_stream : glk_get_line_stream, + glk_get_buffer_stream : glk_get_buffer_stream, + glk_char_to_lower : glk_char_to_lower, + glk_char_to_upper : glk_char_to_upper, + glk_stylehint_set : glk_stylehint_set, + glk_stylehint_clear : glk_stylehint_clear, + glk_style_distinguish : glk_style_distinguish, + glk_style_measure : glk_style_measure, + glk_select : glk_select, + glk_select_poll : glk_select_poll, + glk_request_line_event : glk_request_line_event, + glk_cancel_line_event : glk_cancel_line_event, + glk_request_char_event : glk_request_char_event, + glk_cancel_char_event : glk_cancel_char_event, + glk_request_mouse_event : glk_request_mouse_event, + glk_cancel_mouse_event : glk_cancel_mouse_event, + glk_request_timer_events : glk_request_timer_events, + glk_image_get_info : glk_image_get_info, + glk_image_draw : glk_image_draw, + glk_image_draw_scaled : glk_image_draw_scaled, + glk_image_draw_scaled_ext : glk_image_draw_scaled_ext, + glk_window_flow_break : glk_window_flow_break, + glk_window_erase_rect : glk_window_erase_rect, + glk_window_fill_rect : glk_window_fill_rect, + glk_window_set_background_color : glk_window_set_background_color, + glk_schannel_iterate : glk_schannel_iterate, + glk_schannel_get_rock : glk_schannel_get_rock, + glk_schannel_create : glk_schannel_create, + glk_schannel_destroy : glk_schannel_destroy, + glk_schannel_play : glk_schannel_play, + glk_schannel_play_ext : glk_schannel_play_ext, + glk_schannel_stop : glk_schannel_stop, + glk_schannel_set_volume : glk_schannel_set_volume, + glk_schannel_create_ext : glk_schannel_create_ext, + glk_schannel_play_multi : glk_schannel_play_multi, + glk_schannel_pause : glk_schannel_pause, + glk_schannel_unpause : glk_schannel_unpause, + glk_schannel_set_volume_ext : glk_schannel_set_volume_ext, + glk_sound_load_hint : glk_sound_load_hint, + glk_set_hyperlink : glk_set_hyperlink, + glk_set_hyperlink_stream : glk_set_hyperlink_stream, + glk_request_hyperlink_event : glk_request_hyperlink_event, + glk_cancel_hyperlink_event : glk_cancel_hyperlink_event, + glk_buffer_to_lower_case_uni : glk_buffer_to_lower_case_uni, + glk_buffer_to_upper_case_uni : glk_buffer_to_upper_case_uni, + glk_buffer_to_title_case_uni : glk_buffer_to_title_case_uni, + glk_buffer_canon_decompose_uni : glk_buffer_canon_decompose_uni, + glk_buffer_canon_normalize_uni : glk_buffer_canon_normalize_uni, + glk_put_char_uni : glk_put_char_uni, + glk_put_string_uni : glk_put_string_uni, + glk_put_buffer_uni : glk_put_buffer_uni, + glk_put_char_stream_uni : glk_put_char_stream_uni, + glk_put_string_stream_uni : glk_put_string_stream_uni, + glk_put_buffer_stream_uni : glk_put_buffer_stream_uni, + glk_get_char_stream_uni : glk_get_char_stream_uni, + glk_get_buffer_stream_uni : glk_get_buffer_stream_uni, + glk_get_line_stream_uni : glk_get_line_stream_uni, + glk_stream_open_file_uni : glk_stream_open_file_uni, + glk_stream_open_memory_uni : glk_stream_open_memory_uni, + glk_request_char_event_uni : glk_request_char_event_uni, + glk_request_line_event_uni : glk_request_line_event_uni, + glk_set_echo_line_event : glk_set_echo_line_event, + glk_set_terminators_line_event : glk_set_terminators_line_event, + glk_current_time : glk_current_time, + glk_current_simple_time : glk_current_simple_time, + glk_time_to_date_utc : glk_time_to_date_utc, + glk_time_to_date_local : glk_time_to_date_local, + glk_simple_time_to_date_utc : glk_simple_time_to_date_utc, + glk_simple_time_to_date_local : glk_simple_time_to_date_local, + glk_date_to_time_utc : glk_date_to_time_utc, + glk_date_to_time_local : glk_date_to_time_local, + glk_date_to_simple_time_utc : glk_date_to_simple_time_utc, + glk_date_to_simple_time_local : glk_date_to_simple_time_local, + glk_stream_open_resource : glk_stream_open_resource, + glk_stream_open_resource_uni : glk_stream_open_resource_uni +}; + +}; + +/* Glk is an instance of GlkClass, ready to init. */ +var Glk = new GlkClass(); + +// Node-compatible behavior +try { exports.Glk = Glk; exports.GlkClass = GlkClass; } catch (ex) {}; + +/* End of Glk library. */ diff --git a/src/Arcade/Zork/vendor/vendor.d.ts b/src/Arcade/Zork/vendor/vendor.d.ts new file mode 100644 index 0000000000..f01f0e8da1 --- /dev/null +++ b/src/Arcade/Zork/vendor/vendor.d.ts @@ -0,0 +1,15 @@ +// Type declarations for the verbatim vendored MIT-licensed interpreter files. +// See src/Arcade/Zork/LICENSE.md and README.md. + +declare module "*/vendor/zvm.js" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ZVM: new () => any; + export default ZVM; +} + +declare module "*/vendor/glkapi.js" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const Glk: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const GlkClass: new () => any; +} diff --git a/src/Arcade/Zork/vendor/zvm.js b/src/Arcade/Zork/vendor/zvm.js new file mode 100644 index 0000000000..74dd2187a5 --- /dev/null +++ b/src/Arcade/Zork/vendor/zvm.js @@ -0,0 +1,4312 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ZVM = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o retain immediate returns if optimisations are disabled +Variable = Operand.subClass({ + // Get a value + toString: function() + { + var variable = this.v; + + // Indirect + if ( this.indirect ) + { + return 'e.indirect(' + variable + ')'; + } + + // Stack + if ( variable === 0 ) + { + // If we've been passed a value we're setting a variable + return 's[--e.sp]'; + } + // Locals + if ( --variable < 15 ) + { + return 'l[' + variable + ']'; + } + // Globals + return 'e.m.getUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ')'; + }, + + // Store a value + store: function( value ) + { + var variable = this.v; + + // Indirect variable + if ( this.indirect ) + { + return 'e.indirect(' + variable + ',' + value + ')'; + } + + // BrancherStorers need the value + if ( this.returnval ) + { + return 'e.variable(' + variable + ',' + value + ')'; + } + + // Stack + if ( variable === 0 ) + { + // If we've been passed a value we're setting a variable + return 't=' + value + ';s[e.sp++]=t'; + } + // Locals + if ( --variable < 15 ) + { + return 'l[' + variable + ']=' + value; + } + // Globals + return 'e.ram.setUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ',' + value + ')'; + }, + + // Convert an Operand into a signed operand + U2S: function() + { + return 'e.U2S(' + this + ')'; + }, +}), + +// Generic opcode +// .func() must be set, which returns what .write() will actually return; it is passed the operands as its arguments +Opcode = Class.subClass({ + init: function( engine, context, code, pc, next, operands ) + { + this.e = engine; + this.context = context; + this.code = code; + this.pc = pc; + this.labels = [ this.pc + '/' + this.code ]; + this.next = next; + this.operands = operands; + + // Post-init function (so that they don't all have to call _super) + if ( this.post ) + { + this.post(); + } + }, + + // Write out the opcode, passing .operands to .func(), with a JS comment of the pc/opcode + toString: function() + { + return this.label() + ( this.func ? this.func.apply( this, this.operands ) : '' ); + }, + + // Return a string of the operands separated by commas + args: function( joiner ) + { + return this.operands.join( joiner ); + }, + + // Generate a comment of the pc and code, possibly for more than one opcode + label: function() + { + return '/* ' + this.labels.join() + ' */ '; + }, +}), + +// Stopping opcodes +Stopper = Opcode.subClass({ + stopper: 1, +}), + +// Pausing opcodes (ie, set the pc at the end of the context) +Pauser = Stopper.subClass({ + post: function() + { + this.origfunc = this.func; + this.func = this.newfunc; + }, + + newfunc: function() + { + return 'e.stop=1;e.pc=' + this.next + ';' + this.origfunc.apply( this, arguments ); + }, +}), + +PauserStorer = Pauser.subClass({ + storer: 1, + + post: function() + { + this.storer = this.operands.pop(); + this.origfunc = this.func; + this.func = this.newfunc; + }, +}), + +// Join multiple branchers together with varying logic conditions +BrancherLogic = Class.subClass({ + init: function( ops, code ) + { + this.ops = ops || []; + this.code = code || '||'; + }, + + toString: function() + { + var i = 0, + ops = [], + op; + while ( i < this.ops.length ) + { + op = this.ops[i++]; + // Accept either Opcodes or further BrancherLogics + ops.push( + op.func ? + ( op.iftrue ? '' : '!(' ) + op.func.apply( op, op.operands ) + ( op.iftrue ? '' : ')' ) : + op + ); + } + return ( this.invert ? '(!(' : '(' ) + ops.join( this.code ) + ( this.invert ? '))' : ')' ); + }, +}), + +// Branching opcodes +Brancher = Opcode.subClass({ + // Flag for the disassembler + brancher: 1, + + keyword: 'if', + + // Process the branch result now + post: function() + { + var result, + prev, + + // Calculate the offset + brancher = this.operands.pop(), + offset = brancher[1]; + this.iftrue = brancher[0]; + + // Process the offset + if ( offset === 0 || offset === 1 ) + { + result = 'e.ret(' + offset + ')'; + } + else + { + offset += this.next - 2; + + // Add this target to this context's list + this.context.targets.push( offset ); + result = 'e.pc=' + offset; + } + + this.result = result + ';return'; + this.offset = offset; + this.cond = new BrancherLogic( [this] ); + + // TODO: re-enable + /*if ( this.e.options.debug ) + { + // Stop if we must + if ( debugflags.noidioms ) + { + return; + } + }*/ + + // Compare with previous statement + if ( this.context.ops.length ) + { + prev = this.context.ops.pop(); + // As long as no other opcodes have an offset property we can skip the instanceof check + if ( /* prev instanceof Brancher && */ prev.offset === offset ) + { + // Goes to same offset so reuse the Brancher arrays + this.cond.ops.unshift( prev.cond ); + this.labels = prev.labels; + this.labels.push( this.pc + '/' + this.code ); + } + else + { + this.context.ops.push( prev ); + } + } + }, + + // Write out the brancher + toString: function() + { + var result = this.result; + + // Account for Contexts + if ( result instanceof Context ) + { + // Update the context to be a child of this context + if ( this.e.options.debug ) + { + result.context = this.context; + } + + result = result + ( result.stopper ? '; return' : '' ); + + // Extra line breaks for multi-op results + if ( this.result.ops.length > 1 ) + { + result = '\n' + result + '\n'; + if ( this.e.options.debug ) + { + result += this.context.spacer; + } + } + } + + // Print out a label for all included branches and the branch itself + return this.label() + this.keyword + this.cond + ' {' + result + '}'; + }, +}), + +// Brancher + Storer +BrancherStorer = Brancher.subClass({ + storer: 1, + + // Set aside the storer operand + post: function() + { + BrancherStorer.super.post.call( this ); + this.storer = this.operands.pop(); + this.storer.returnval = 1; + + // Replace the func + this.origfunc = this.func; + this.func = this.newfunc; + }, + + newfunc: function() + { + return this.storer.store( this.origfunc.apply( this, arguments ) ); + }, +}), + +// Storing opcodes +Storer = Opcode.subClass({ + // Flag for the disassembler + storer: 1, + + // Set aside the storer operand + post: function() + { + this.storer = this.operands.pop(); + }, + + // Write out the opcode, passing it to the storer (if there still is one) + toString: function() + { + var data = Storer.super.toString.call( this ); + + // If we still have a storer operand, use it + // Otherwise (if it's been removed due to optimisations) just return func() + return this.storer ? this.storer.store( data ) : data; + }, +}), + +// Routine calling opcodes +Caller = Stopper.subClass({ + // Fake a result variable + result: { v: -1 }, + + // Write out the opcode + toString: function() + { + // TODO: Debug: include label if possible + return this.label() + 'e.call(' + this.operands.shift() + ',' + this.result.v + ',' + this.next + ',[' + this.args() + '])'; + }, +}), + +// Routine calling opcodes, storing the result +CallerStorer = Caller.subClass({ + // Flag for the disassembler + storer: 1, + + post: function() + { + // We can't let the storer be optimised away here + this.result = this.operands.pop(); + }, +}), + +// A generic context (a routine, loop body etc) +Context = Class.subClass({ + init: function( engine, pc ) + { + this.e = engine; + this.pc = pc; + this.pre = []; + this.ops = []; + this.post = []; + this.targets = []; // Branch targets + if ( engine.options.debug ) + { + this.spacer = ''; + } + }, + + toString: function() + { + if ( this.e.options.debug ) + { + // Indent the spacer further if needed + if ( this.context ) + { + this.spacer = this.context.spacer + ' '; + } + // DEBUG: Pretty print! + return this.pre.join( '' ) + ( this.ops.length > 1 ? this.spacer : '' ) + this.ops.join( ';\n' + this.spacer ) + this.post.join( '' ); + + } + else + { + // Return the code + return this.pre.join( '' ) + this.ops.join( ';' ) + this.post.join( '' ); + } + }, +}), + +// A routine body +RoutineContext = Context.subClass({ + toString: function() + { + // TODO: Debug: If we have routine names, find this one's name + + // Add in some extra vars and return + this.pre.unshift( 'var l=e.l,s=e.s,t=0;\n' ); + return RoutineContext.super.toString.call( this ); + }, +}); + +// Opcode builder +// Easily build a new opcode from a class +function opcode_builder( Class, func, flags ) +{ + flags = flags || {}; + if ( func ) + { + /*if ( func.pop ) + { + flags.str = func; + flags.func = common_func; + } + else + {*/ + flags.func = func; + //} + } + return Class.subClass( flags ); +} + +module.exports = { + Operand: Operand, + Variable: Variable, + Opcode: Opcode, + Stopper: Stopper, + Pauser: Pauser, + PauserStorer: PauserStorer, + BrancherLogic: BrancherLogic, + Brancher: Brancher, + BrancherStorer: BrancherStorer, + Storer: Storer, + Caller: Caller, + CallerStorer: CallerStorer, + Context: Context, + RoutineContext: RoutineContext, + opcode_builder: opcode_builder, +}; + +},{"../common/utils.js":3}],2:[function(require,module,exports){ +/* + +File classes +============ + +Copyright (c) 2016 The ifvms.js team +BSD licenced +http://github.com/curiousdannii/ifvms.js + +*/ + +'use strict'; + +var utils = require( './utils.js' ), +MemoryView = utils.MemoryView, + +// A basic IFF file, to be extended later +// Currently supports buffer data +IFF = utils.Class.subClass({ + init: function( data ) + { + this.type = ''; + this.chunks = []; + + if ( data ) + { + var view = MemoryView( data ), + i = 12, length, chunk_length; + + // Check that it is actually an IFF file + if ( view.getFourCC( 0 ) !== 'FORM' ) + { + throw new Error( 'Not an IFF file' ); + } + + // Parse the file + this.type = view.getFourCC( 8 ); + length = view.getUint32( 4 ) + 8; + + while ( i < length ) + { + chunk_length = view.getUint32( i + 4 ); + + if ( chunk_length < 0 || ( chunk_length + i ) > length ) + { + throw new Error( 'IFF chunk out of range' ); + } + + this.chunks.push({ + type: view.getFourCC( i ), + offset: i, + data: view.getUint8Array( i + 8, chunk_length ), + }); + + i += 8 + chunk_length; + if ( chunk_length % 2 ) + { + i++; + } + } + } + }, + + write: function() + { + // Start with the IFF type + var buffer_len = 12, i = 0, index = 12, + out, chunk; + + // First calculate the required buffer length + while ( i < this.chunks.length ) + { + // Replace typed arrays or dataviews with their buffers + if ( this.chunks[i].data.buffer ) + { + this.chunks[i].data = this.chunks[i].data.buffer; + } + this.chunks[i].length = this.chunks[i].data.byteLength || this.chunks[i].data.length; + buffer_len += 8 + this.chunks[i++].length; + if ( buffer_len % 2 ) + { + buffer_len++; + } + } + + out = MemoryView( buffer_len ); + out.setFourCC( 0, 'FORM' ); + out.setUint32( 4, buffer_len - 8 ); + out.setFourCC( 8, this.type ); + + // Go through the chunks and write them out + i = 0; + while ( i < this.chunks.length ) + { + chunk = this.chunks[i++]; + out.setFourCC( index, chunk.type ); + out.setUint32( index + 4, chunk.length ); + out.setUint8Array( index + 8, chunk.data ); + index += 8 + chunk.length; + if ( index % 2 ) + { + index++; + } + } + + return out.buffer; + }, +}), + +Blorb = IFF.subClass({ + init: function( data ) + { + this.super.init.call( this, data ); + if ( data ) + { + if ( this.type !== 'IFRS' ) + { + throw new Error( 'Not a Blorb file' ); + } + + // Process the RIdx chunk to find the main exec chunk + if ( this.chunks[0].type !== 'RIdx' ) + { + throw new Error( 'Malformed Blorb: chunk 1 is not RIdx' ); + } + var view = MemoryView( this.chunks[0].data ), + i = 4; + while ( i < this.chunks[0].data.length ) + { + if ( view.getFourCC( i ) === 'Exec' && view.getUint32( i + 4 ) === 0 ) + { + this.exec = this.chunks.filter( function( chunk ) + { + return chunk.offset === view.getUint32( i + 8 ); + })[0]; + return; + } + i += 12; + } + } + }, +}), + +Quetzal = IFF.subClass({ + // Parse a Quetzal savefile, or make a blank one + init: function( data ) + { + this.super.init.call( this, data ); + if ( data ) + { + // Check this is a Quetzal savefile + if ( this.type !== 'IFZS' ) + { + throw new Error( 'Not a Quetzal savefile' ); + } + + // Go through the chunks and extract the useful ones + var i = 0, + type, chunk_data, view; + + while ( i < this.chunks.length ) + { + type = this.chunks[i].type; + chunk_data = this.chunks[i++].data; + + // Memory and stack chunks + if ( type === 'CMem' || type === 'UMem' ) + { + this.memory = chunk_data; + this.compressed = ( type === 'CMem' ); + } + else if ( type === 'Stks' ) + { + this.stacks = chunk_data; + } + + // Story file data + else if ( type === 'IFhd' ) + { + view = MemoryView( chunk_data.buffer ); + this.release = view.getUint16( 0 ); + this.serial = view.getUint8Array( 2, 6 ); + // The checksum isn't used, but if we throw it away we can't round-trip + this.checksum = view.getUint16( 8 ); + // The pc is only a Uint24, but there's no function for that, so grab an extra byte and then discard it + this.pc = view.getUint32( 9 ) & 0xFFFFFF; + } + } + } + }, + + // Write out a savefile + write: function() + { + // Reset the IFF type + this.type = 'IFZS'; + + // Format the IFhd chunk correctly + var ifhd = MemoryView( 13 ); + ifhd.setUint16( 0, this.release ); + ifhd.setUint8Array( 2, this.serial ); + ifhd.setUint32( 9, this.pc ); + ifhd.setUint16( 8, this.checksum ); + + // Add the chunks + this.chunks = [ + { type: 'IFhd', data: ifhd }, + { type: ( this.compressed ? 'CMem' : 'UMem' ), data: this.memory }, + { type: 'Stks', data: this.stacks }, + ]; + + // Return the byte array + return this.super.write.call( this ); + }, +}); + +// Inspect a file and identify its format and version number +function identify( buffer ) +{ + var view = MemoryView( buffer ), + blorb, + format, + version; + + // Blorb + if ( view.getFourCC( 0 ) === 'FORM' && view.getFourCC( 8 ) === 'IFRS' ) + { + blorb = new Blorb( buffer ); + if ( blorb.exec ) + { + format = blorb.exec.type; + buffer = blorb.exec.data; + if ( format === 'GLUL' ) + { + view = MemoryView( buffer ); + version = view.getUint32( 4 ); + } + if ( format === 'ZCOD' ) + { + version = buffer[0]; + } + } + } + // Glulx + else if ( view.getFourCC( 0 ) === 'Glul' ) + { + format = 'GLUL'; + version = view.getUint32( 4 ); + } + // Z-Code + else + { + version = view.getUint8( 0 ); + if ( version > 0 && version < 9 ) + { + format = 'ZCOD'; + } + } + + if ( format && version ) + { + return { + format: format, + version: version, + data: buffer, + }; + } +} + +module.exports = { + IFF: IFF, + Blorb: Blorb, + Quetzal: Quetzal, + identify: identify, +}; + +},{"./utils.js":3}],3:[function(require,module,exports){ +/* + +Common untility functions +========================= + +Copyright (c) 2016 The ifvms.js team +BSD licenced +http://github.com/curiousdannii/ifvms.js + +*/ + +'use strict'; + +// Utility to extend objects +function extend() +{ + var old = arguments[0], i = 1, add, name; + while ( i < arguments.length ) + { + add = arguments[i++]; + for ( name in add ) + { + old[name] = add[name]; + } + } + return old; +} + +// Simple classes +// Inspired by John Resig's class implementation +// http://ejohn.org/blog/simple-javascript-inheritance/ + +function Class() +{} + +Class.subClass = function( props ) +{ + function newClass() + { + if ( this.init ) + { + this.init.apply( this, arguments ); + } + } + newClass.prototype = extend( Object.create( this.prototype ), props ); + newClass.subClass = this.subClass; + newClass.super = newClass.prototype.super = this.prototype; + return newClass; +}; + +// An enhanced DataView +// Accepts an ArrayBuffer, another view (MemoryView / DataView / TypedArray), or a length number +function MemoryView( buffer, byteOffset, byteLength ) +{ + // Length number + if ( typeof buffer === 'number' ) + { + buffer = new ArrayBuffer( buffer ); + } + + // MemoryView / DataView / TypedArray + else if ( buffer.buffer ) + { + // If unspecified, byteOffset defaults at the beginning of the given view. Note + // that We will adjust 'byteOffset' after using the initial value to calculate + // the default byteLength below. + byteOffset |= 0; + + // A view may be a subset of a potentially larger array buffer. Before extracting + // the underlying buffer, map the given 'byteLength' and byteOffset' to the underlying + // array buffer from which we will construct the DataView. + + // A specified 'byteLength' does not need to be adjusted, but if no byteLength was + // given, we need to ensure that the resulting MemoryView does not extend past the + // end of the typed array (see above). + if ( typeof byteLength === 'undefined' ) + { + byteLength = buffer.byteLength - byteOffset; + } + + // Map the 'byteOffset', which is currently relative to the typed array, to the same + // location in the underlying array buffer. + byteOffset += buffer.byteOffset; + + // Finally, extract the underlying array buffer. + buffer = buffer.buffer; + } + // Else already an ArrayBuffer. No adjustments to 'byteOffset'/'byteLength' necessary. + + return extend( new DataView( buffer, byteOffset, byteLength ), { + getUint8Array: function( start, length ) + { + // Note that start/length are non-optional, so we only need to adjust the start to + // the byteOffset of the view. (See MemoryView ctor comments.) + start += this.byteOffset; + + return new Uint8Array( this.buffer.slice( start, start + length ) ); + }, + getUint16Array: function( start, length ) + { + // Note that start/length are non-optional, so we only need to adjust the start to + // the byteOffset of the view. (See MemoryView ctor comments.) + start += this.byteOffset; + + // We cannot simply return a Uint16Array as most systems are little-endian + return Uint8toUint16Array( new Uint8Array( this.buffer, start, length * 2 ) ); + }, + setUint8Array: function( start, data ) + { + if ( data instanceof ArrayBuffer ) + { + data = new Uint8Array( data ); + } + ( new Uint8Array( this.buffer, this.byteOffset, this.byteLength ) ).set( data, start ); + }, + //setBuffer16 NOTE: if we implement this we cannot simply set a Uint16Array as most systems are little-endian + + // For use with IFF files + getFourCC: function( index ) + { + return String.fromCharCode( this.getUint8( index ), this.getUint8( index + 1 ), this.getUint8( index + 2 ), this.getUint8( index + 3 ) ); + }, + setFourCC: function( index, text ) + { + this.setUint8( index, text.charCodeAt( 0 ) ); + this.setUint8( index + 1, text.charCodeAt( 1 ) ); + this.setUint8( index + 2, text.charCodeAt( 2 ) ); + this.setUint8( index + 3, text.charCodeAt( 3 ) ); + }, + } ); +} + +// Utilities for 16-bit signed arithmetic +function U2S16( value ) +{ + return value << 16 >> 16; +} +function S2U16 ( value ) +{ + return value & 0xFFFF; +} + +// Utility to convert from byte arrays to word arrays +function Uint8toUint16Array( array ) +{ + var i = 0, l = array.length, + result = new Uint16Array( l / 2 ); + while ( i < l ) + { + result[i / 2] = array[i++] << 8 | array[i++]; + } + return result; +} + +module.exports = { + extend: extend, + Class: Class, + MemoryView: MemoryView, + U2S16: U2S16, + S2U16: S2U16, + Uint8toUint16Array: Uint8toUint16Array, +}; +},{}],4:[function(require,module,exports){ +/* + +ZVM - the ifvms.js Z-Machine (versions 3-5, 8) +============================================== + +Copyright (c) 2017 The ifvms.js team +MIT licenced +https://github.com/curiousdannii/ifvms.js + +*/ + +/* + +This file is the public API of ZVM, which is based on the API of Quixe: +https://github.com/erkyrath/quixe/wiki/Quixe-Without-GlkOte#quixes-api + +ZVM willfully ignores the standard in these ways: + Non-buffered output is not supported + Saving tables is not supported (yet?) + No interpreter number or version is set + +Any other non-standard behaviour should be considered a bug + +*/ + +'use strict'; + +var utils = require( './common/utils.js' ), +file = require( './common/file.js' ), + +default_options = { + stack_len: 100 * 1000, + undo_len: 1000 * 1000, +}, + +api = { + + init: function() + { + // Create this here so that it won't be cleared on restart + this.jit = {}; + + // The Quixe API expects the start function to be named init + this.init = this.start; + }, + + prepare: function( storydata, options ) + { + // If we are not given a glk option then we cannot continue + if ( !options.Glk ) + { + throw new Error( 'A reference to Glk is required' ); + } + this.Glk = options.Glk; + this.data = storydata; + this.options = utils.extend( {}, default_options, options ); + }, + + start: function() + { + var Glk = this.Glk, + data; + try + { + // Identify the format and version number of the data file we were given + data = file.identify( this.data ); + delete this.data; + if ( !data || data.format !== 'ZCOD' ) + { + throw new Error( 'This is not a Z-Code file' ); + } + if ( [ 3, 4, 5, 8 ].indexOf( data.version ) < 0 ) + { + throw new Error( 'Unsupported Z-Machine version: ' + data.version ); + } + + // Load the storyfile we are given into our MemoryView (an enhanced DataView) + this.m = utils.MemoryView( data.data ); + + // Make a seperate MemoryView for the ram, and store the original ram + this.staticmem = this.m.getUint16( 0x0E ); + this.ram = utils.MemoryView( this.m, 0, this.staticmem ); + this.origram = this.m.getUint8Array( 0, this.staticmem ); + + // Cache the game signature + let signature = '' + let i = 0 + while ( i < 0x1E ) + { + signature += ( this.origram[i] < 0x10 ? '0' : '' ) + this.origram[i++].toString( 16 ) + } + this.signature = signature + + // Handle loading and clearing autosaves + let autorestored + const Dialog = this.options.Dialog + if ( Dialog ) + { + if ( this.options.clear_vm_autosave ) + { + Dialog.autosave_write( signature, null ) + } + else if ( this.options.do_vm_autosave ) + { + try + { + const snapshot = Dialog.autosave_read( signature ) + if ( snapshot ) + { + this.do_autorestore( snapshot ) + autorestored = 1 + } + } + catch (ex) + { + this.log('Autorestore failed, deleting it: ' + ex) + Dialog.autosave_write( signature, null ) + } + } + } + + // Initiate the engine, run, and wait for our first Glk event + if ( !autorestored ) + { + this.restart(); + this.run(); + } + if ( !this.quit ) + { + this.glk_event = new Glk.RefStruct(); + if ( !this.glk_blocking_call ) + { + Glk.glk_select( this.glk_event ); + } + else + { + this.glk_event.push_field( this.glk_blocking_call ); + } + } + Glk.update() + } + catch ( e ) + { + Glk.fatal_error( e ); + console.log( e ); + } + }, + + resume: function( resumearg ) + { + var Glk = this.Glk, + glk_event = this.glk_event, + event_type, + run; + + try + { + event_type = glk_event.get_field( 0 ); + + // Process the event + if ( event_type === 2 ) + { + this.handle_char_input( glk_event.get_field( 2 ) ); + run = 1; + } + if ( event_type === 3 ) + { + this.handle_line_input( glk_event.get_field( 2 ), glk_event.get_field( 3 ) ); + run = 1; + } + + // Arrange events + if ( event_type === 5 ) + { + this.update_screen_size() + } + + // glk_fileref_create_by_prompt handler + if ( event_type === 'fileref_create_by_prompt' ) + { + run = this.handle_create_fileref( resumearg ); + } + + this.glk_blocking_call = null; + if ( run ) + { + this.run(); + } + + // Wait for another event + if ( !this.quit ) + { + this.glk_event = new Glk.RefStruct(); + if ( !this.glk_blocking_call ) + { + Glk.glk_select( this.glk_event ); + } + else + { + this.glk_event.push_field( this.glk_blocking_call ); + } + } + Glk.update() + } + catch ( e ) + { + Glk.fatal_error( e ); + console.log( e ); + } + }, + + get_signature: function() + { + return this.signature + }, + + // Run + run: function() + { + var pc, + result; + + // Stop when ordered to + this.stop = 0; + while ( !this.stop ) + { + pc = this.pc; + if ( !this.jit[pc] ) + { + this.compile(); + } + result = this.jit[pc]( this ); + + // Return from a VM func if the JIT function returned a result + if ( !isNaN( result ) ) + { + this.ret( result ); + } + } + }, + + // Compile a JIT routine + compile: function() + { + var context = this.disassemble(); + + // Compile the routine with new Function() + this.jit[context.pc] = new Function( 'e', '' + context ); + + if ( context.pc < this.staticmem ) + { + this.log( 'Caching a JIT function in dynamic memory: ' + context.pc ); + } + }, + +}, + +VM = utils.Class.subClass( utils.extend( + api, + require( './zvm/runtime.js' ), + require( './zvm/text.js' ), + require( './zvm/io.js' ), + require( './zvm/disassembler.js' ) +) ); + +module.exports = VM; + +},{"./common/file.js":2,"./common/utils.js":3,"./zvm/disassembler.js":5,"./zvm/io.js":6,"./zvm/runtime.js":8,"./zvm/text.js":9}],5:[function(require,module,exports){ +/* + +Z-Machine disassembler - disassembles zcode into an AST +======================================================= + +Copyright (c) 2011 The ifvms.js team +BSD licenced +http://github.com/curiousdannii/ifvms.js + +*/ + +/* + +Note: + Nothing is done to check whether an instruction actually has a valid number of operands. Extras will usually be ignored while missing operands may throw errors at either the code building stage or when the JIT code is called. + +TODO: + If we diassessemble part of what we already have before, can we just copy/slice the context? + +*/ + +var AST = require( '../common/ast.js' ); + +module.exports.disassemble = function() +{ + var pc, offset, // Set in the loop below + memory = this.m, + opcodes = this.opcodes, + temp, + code, + opcode_class, + operands_type, // The types of the operands, or -1 for var instructions + operands, + + // Create the context for this code fragment + context = new AST.RoutineContext( this, this.pc ); + + // Utility function to unpack the variable form operand types byte + function get_var_operand_types( operands_byte, operands_type ) + { + for ( var i = 0; i < 4; i++ ) + { + operands_type.push( (operands_byte & 0xC0) >> 6 ); + operands_byte <<= 2; + } + } + + // Set the context's root context to be itself, and add it to the list of subcontexts + //context.root = context; + //context.contexts[0] = context; + + // Run through until we can no more + while ( 1 ) + { + // This instruction + offset = pc = this.pc; + code = memory.getUint8( pc++ ); + + // Extended instructions + if ( code === 190 ) + { + operands_type = -1; + code = memory.getUint8( pc++ ) + 1000; + } + + else if ( code & 0x80 ) + { + // Variable form instructions + if ( code & 0x40 ) + { + operands_type = -1; + // 2OP instruction with VAR parameters + if ( !(code & 0x20) ) + { + code &= 0x1F; + } + } + + // Short form instructions + else + { + operands_type = [ (code & 0x30) >> 4 ]; + // Clear the operand type if 1OP, keep for 0OPs + if ( operands_type[0] < 3 ) + { + code &= 0xCF; + } + } + } + + // Long form instructions + else + { + operands_type = [ code & 0x40 ? 2 : 1, code & 0x20 ? 2 : 1 ]; + code &= 0x1F; + } + + // Check for missing opcodes + if ( !opcodes[code] ) + { + this.log( '' + context ); + this.stop = 1; + throw new Error( 'Unknown opcode #' + code + ' at pc=' + offset ); + } + + // Variable for quicker access to the opcode flags + opcode_class = opcodes[code].prototype; + + // Variable form operand types + if ( operands_type === -1 ) + { + operands_type = []; + get_var_operand_types( memory.getUint8(pc++), operands_type ); + + // VAR_LONG opcodes have two operand type bytes + if ( code === 236 || code === 250 ) + { + get_var_operand_types( memory.getUint8(pc++), operands_type ); + } + } + + // Load the operands + operands = []; + temp = 0; + while ( temp < operands_type.length ) + { + // Large constant + if ( operands_type[temp] === 0 ) + { + operands.push( new AST.Operand( this, memory.getUint16(pc) ) ); + pc += 2; + } + + // Small constant + if ( operands_type[temp] === 1 ) + { + operands.push( new AST.Operand( this, memory.getUint8(pc++) ) ); + } + + // Variable operand + if ( operands_type[temp++] === 2 ) + { + operands.push( new AST.Variable( this, memory.getUint8(pc++) ) ); + } + } + + // Check for a store variable + if ( opcode_class.storer ) + { + operands.push( new AST.Variable( this, memory.getUint8(pc++) ) ); + } + + // Check for a branch address + // If we don't calculate the offset now we won't be able to tell the difference between 0x40 and 0x0040 + if ( opcode_class.brancher ) + { + temp = memory.getUint8( pc++ ); + operands.push( [ + temp & 0x80, // iftrue + temp & 0x40 ? + // single byte address + temp & 0x3F : + // word address, but first get the second byte of it + ( temp << 8 | memory.getUint8( pc++ ) ) << 18 >> 18, + ] ); + } + + // Check for a text literal + if ( opcode_class.printer ) + { + // Just use the address as an operand, the text will be decoded at run time + operands.push( pc ); + + // Continue until we reach the stop bit + // (or the end of the file, which will stop memory access errors, even though it must be a malformed storyfile) + while ( pc < this.eof ) + { + temp = memory.getUint8( pc ); + pc += 2; + + // Stop bit + if ( temp & 0x80 ) + { + break; + } + } + } + + // Update the engine's pc + this.pc = pc; + + // Create the instruction + context.ops.push( new opcodes[code]( this, context, code, offset, pc, operands ) ); + + // Check for the end of a large if block + temp = 0; + /*if ( context.targets.indexOf( pc ) >= 0 ) + { + if ( DEBUG ) + { + // Skip if we must + if ( !debugflags.noidioms ) + { + temp = idiom_if_block( context, pc ); + } + } + else + { + temp = idiom_if_block( context, pc ); + } + }*/ + + // We can't go any further if we have a final stopper :( + if ( opcode_class.stopper && !temp ) + { + break; + } + } + + return context; +}; + +},{"../common/ast.js":1}],6:[function(require,module,exports){ +/* + +Z-Machine IO +============ + +Copyright (c) 2020 The ifvms.js team +MIT licenced +https://github.com/curiousdannii/ifvms.js + +*/ + +'use strict'; + +/* + +TODO: + + - pre-existing line input + - timed input + - mouse input + - write colours into header + +*/ + +const utils = require('../common/utils.js') +const U2S = utils.U2S16 +//S2U = utils.S2U16 + +// Glulx key codes accepted by the Z-Machine +const ZSCII_keyCodes = (function() +{ + var codes = { + 0xfffffff9: 8, // delete/backspace + 0xfffffffa: 13, // enter + 0xfffffff8: 27, // escape + 0xfffffffc: 129, // up + 0xfffffffb: 130, // down + 0xfffffffe: 131, // left + 0xfffffffd: 132, // right + 0xfffffff3: 146, // End / key pad 1 + 0xfffffff5: 148, // PgDn / key pad 3 + 0xfffffff4: 152, // Home / key pad 7 + 0xfffffff6: 154, // PgUp / key pad 9 + }, + i = 0; + while ( i < 12 ) + { + codes[ 0xffffffef - i ] = 133 + i++; // function keys + } + return codes; +})() + +// Style mappings +// The index bits are (lowest to highest): mono, italic, bold +const style_mappings = [0, 2, 1, 10, 4, 9, 5, 6] + +// Convert a 15 bit colour to RGB +function convert_true_colour(colour) +{ + const from5to8 = [0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, + 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255] + + // Stretch the five bits per colour out to 8 bits + return (from5to8[colour & 0x1F] << 16) | (from5to8[(colour & 0x03E0) >> 5] << 8) | (from5to8[(colour & 0x7C00) >> 10]) +} + +// The standard 15 bit colour values +const zcolours = [ + 0xFFFE, // Current + 0xFFFF, // Default + 0x0000, // Black + 0x001D, // Red + 0x0340, // Green + 0x03BD, // Yellow + 0x59A0, // Blue + 0x7C1F, // Magenta + 0x77A0, // Cyan + 0x7FFF, // White + 0x5AD6, // Light grey + 0x4631, // Medium grey + 0x2D6B, // Dark grey +] + +module.exports = { + + init_io: function() + { + this.io = { + reverse: 0, + bold: 0, + italic: 0, + bg: -1, + fg: -1, + + // A variable for whether we are outputing in a monospaced font. If non-zero then we are + // Bit 0 is for @set_style, bit 1 for the header, and bit 2 for @set_font + mono: this.m.getUint8( 0x11 ) & 0x02, + + // A variable for checking whether the transcript bit has been changed + transcript: this.m.getUint8( 0x11 ) & 0x01, + + // Index 0 is input stream 1, the output streams follow + streams: [ 0, 1, {}, [], {} ], + + currentwin: 0, + + // Use Zarf's algorithm for the upper window + // http://eblong.com/zarf/glk/quote-box.html + // Implemented in fix_upper_window() and split_window() + height: 0, // What the VM thinks the height is + glkheight: 0, // Actual height of the Glk window + maxheight: 0, // Height including quote boxes etc + seenheight: 0, // Last height the player saw + width: 0, + row: 0, + col: 0, + }; + + //this.process_colours(); + + // Construct the windows if they do not already exist + this.open_windows() + }, + + erase_line: function( value ) + { + if ( value === 1 ) + { + var io = this.io, + row = io.row, + col = io.col; + this._print( Array( io.width - io.col + 1 ).join( ' ' ) ); + this.set_cursor( row, col ); + } + }, + + erase_window: function(window) + { + if (window < 1) + { + this.Glk.glk_window_clear(this.mainwin) + if (this.io.bg >= 0) + { + this.Glk.glk_stylehint_set(3, 0, 8, this.io.bg) + } + else if (this.io.bg === -1) + { + this.Glk.glk_stylehint_clear(3, 0, 8) + } + } + if (window !== 0) + { + if (window === -1) + { + this.split_window(0) + } + if (this.upperwin) + { + this.Glk.glk_window_clear(this.upperwin) + this.set_cursor(0, 0) + } + } + }, + + fileref_create_by_prompt: function( data ) + { + if ( typeof data.run === 'undefined' ) + { + data.run = 1; + } + this.fileref_data = data; + this.glk_blocking_call = 'fileref_create_by_prompt'; + this.Glk.glk_fileref_create_by_prompt( data.usage, data.mode, data.rock || 0 ); + }, + + // Fix the upper window height before an input event + fix_upper_window: function() + { + var Glk = this.Glk, + io = this.io; + + // If we have seen the entire window, shrink it to what it should be + if (io.seenheight >= io.maxheight) + { + io.maxheight = io.height; + } + if ( this.upperwin ) + { + if ( io.maxheight === 0 ) + { + Glk.glk_window_close( this.upperwin ); + this.upperwin = null; + } + else if (io.maxheight !== io.glkheight) + { + Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); + } + io.glkheight = io.maxheight + } + io.seenheight = io.maxheight; + io.maxheight = io.height; + }, + + format: function() + { + this.Glk.glk_set_style(style_mappings[!!this.io.mono | this.io.italic | this.io.bold]) + if (this.Glk.glk_gestalt(0x1100, 0)) + { + this.Glk.garglk_set_reversevideo(this.io.reverse) + } + }, + + get_cursor: function( array ) + { + this.ram.setUint16( array, this.io.row + 1 ); + this.ram.setUint16( array + 2, this.io.col + 1 ); + }, + + // Handle char input + handle_char_input: function( charcode ) + { + var stream4 = this.io.streams[4], + code = ZSCII_keyCodes[ charcode ] || this.reverse_unicode_table[ charcode ] || 63; + this.variable( this.read_data.storer, code ); + + // Echo to the commands log + if ( stream4.mode === 1 ) + { + stream4.cache += code; + } + if ( stream4.mode === 2 ) + { + this.Glk.glk_put_char_stream_uni( stream4.str, code ); + } + }, + + // Handle the result of glk_fileref_create_by_prompt() + handle_create_fileref: function( fref ) + { + var Glk = this.Glk, + data = this.fileref_data, + str; + + if ( fref ) + { + if ( data.unicode ) + { + str = Glk.glk_stream_open_file_uni( fref, data.mode, data.rock || 0 ); + } + else + { + str = Glk.glk_stream_open_file( fref, data.mode, data.rock || 0 ); + } + Glk.glk_fileref_destroy( fref ); + } + if ( data.func === 'restore' || data.func === 'save' ) + { + this.save_restore_handler( str ); + } + if ( data.func === 'input_stream' ) + { + this.io.streams[0] = str; + } + if ( data.func === 'output_stream' ) + { + this.output_stream_handler( str ); + } + + // Signal to resume() to call run() if required + return data.run; + }, + + // Handle line input + handle_line_input: function( len, terminator ) + { + var ram = this.ram, + options = this.read_data, + streams = this.io.streams, + + // Cut the response to len, convert to a lower case string, and then to a ZSCII array + command = String.fromCharCode.apply( null, options.buffer.slice( 0, len ) ) + '\n', + response = this.text_to_zscii( command.slice( 0, -1 ).toLowerCase() ); + + // 7.1.1.1: The response must be echoed, Glk will handle this + + // But we do have to echo to the transcripts + if ( streams[2].mode === 1 ) + { + streams[2].cache += command; + } + if ( streams[2].mode === 2 ) + { + this.Glk.glk_put_jstring_stream( streams[2].str, command ); + } + + if ( streams[4].mode === 1 ) + { + streams[4].cache += command; + } + if ( streams[4].mode === 2 ) + { + this.Glk.glk_put_jstring_stream( streams[4].str, command ); + } + + // Store the response + if ( this.version < 5 ) + { + // Append zero terminator + response.push( 0 ); + + // Store the response in the buffer + ram.setUint8Array( options.bufaddr + 1, response ); + } + else + { + // Store the response length + ram.setUint8( options.bufaddr + 1, len ); + + // Store the response in the buffer + ram.setUint8Array( options.bufaddr + 2, response ); + + // Store the terminator + this.variable( options.storer, isNaN( terminator ) ? 13 : terminator ); + } + + if ( options.parseaddr ) + { + // Tokenise the response + this.tokenise( options.bufaddr, options.parseaddr ); + } + }, + + input_stream: function( stream ) + { + var io = this.io; + if ( stream && !io.streams[0] ) + { + this.fileref_create_by_prompt({ + func: 'input_stream', + mode: 0x02, + rock: 212, + unicode: 1, + usage: 0x103, + }); + } + if ( !stream && io.streams[0] ) + { + this.Glk.glk_stream_close( io.streams[0] ); + io.streams[0] = 0; + } + }, + + // Open windows + open_windows: function() + { + const Glk = this.Glk + + if (!this.mainwin) + { + // We will borrow the general approach of Bocfel to implement the Z-Machine's formatting model in Glk + // https://github.com/garglk/garglk/blob/master/terps/bocfel/screen.c + + // Reset some Glk stylehints just in case + const styles_to_reset = [1, 2, 4, 5, 6, 9, 10] + for (let i = 0; i < 7; i++) + { + // Reset the size, weight, and obliqueness + Glk.glk_stylehint_set(0, styles_to_reset[i], 3, 0) + Glk.glk_stylehint_set(0, styles_to_reset[i], 4, 0) + Glk.glk_stylehint_set(0, styles_to_reset[i], 5, 0) + // And force proportional font + Glk.glk_stylehint_set(0, styles_to_reset[i], 6, 1) + } + + // Now set the style hints we will use + + // Bold will use subheader + Glk.glk_stylehint_set(0, 4, 4, 1) + // Italic will use emphasised + Glk.glk_stylehint_set(0, 1, 5, 1) + // Bold+italic will use alert + Glk.glk_stylehint_set(0, 5, 4, 1) + Glk.glk_stylehint_set(0, 5, 5, 1) + // Fixed will use preformated + Glk.glk_stylehint_set(0, 2, 6, 0) + // Bold+fixed will use user1 + Glk.glk_stylehint_set(0, 9, 4, 1) + Glk.glk_stylehint_set(0, 9, 6, 0) + // Italic+fixed will use user2 + Glk.glk_stylehint_set(0, 10, 5, 1) + Glk.glk_stylehint_set(0, 10, 6, 0) + // Bold+italic+fixed will use note + Glk.glk_stylehint_set(0, 6, 4, 1) + Glk.glk_stylehint_set(0, 6, 5, 1) + Glk.glk_stylehint_set(0, 6, 6, 0) + + this.mainwin = Glk.glk_window_open(0, 0, 0, 3, 201) + Glk.glk_set_window(this.mainwin) + if (this.version3) + { + this.statuswin = Glk.glk_window_open(this.mainwin, 0x12, 1, 4, 202) + if (this.statuswin && this.Glk.glk_gestalt(0x1100, 0)) + { + Glk.garglk_set_reversevideo_stream(Glk.glk_window_get_stream(this.statuswin), 1) + } + } + } + else + { + // Clean up after restarting + Glk.glk_stylehint_clear(0, 0, 8) + if (this.Glk.glk_gestalt(0x1100, 0)) + { + Glk.garglk_set_zcolors_stream(this.mainwin.str, this.io.fg, this.io.bg) + } + Glk.glk_window_clear(this.mainwin) + if (this.upperwin) + { + Glk.glk_window_close(this.upperwin) + this.upperwin = null + } + } + }, + + // Manage output streams + output_stream: function( stream, addr, called_from_print ) + { + var ram = this.ram, + streams = this.io.streams, + data, text; + stream = U2S( stream ); + + // The screen + if ( stream === 1 ) + { + streams[1] = 1; + } + if ( stream === -1 ) + { + streams[1] = 0; + } + + // Transcript + if ( stream === 2 && !streams[2].mode ) + { + this.fileref_create_by_prompt({ + func: 'output_stream', + mode: 0x05, + rock: 210, + run: !called_from_print, + str: 2, + unicode: 1, + usage: 0x102, + }); + streams[2].cache = ''; + streams[2].mode = 1; + if ( !called_from_print ) + { + this.stop = 1; + } + } + if ( stream === -2 ) + { + ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) ); + if ( streams[2].mode === 2 ) + { + this.Glk.glk_stream_close( streams[2].str ); + } + streams[2].mode = this.io.transcript = 0; + } + + // Memory + if ( stream === 3 ) + { + streams[3].unshift( [ addr, '' ] ); + } + if ( stream === -3 ) + { + data = streams[3].shift(); + text = this.text_to_zscii( data[1] ); + ram.setUint16( data[0], text.length ); + ram.setUint8Array( data[0] + 2, text ); + } + + // Command list + if ( stream === 4 && !streams[4].mode ) + { + this.fileref_create_by_prompt({ + func: 'output_stream', + mode: 0x05, + rock: 211, + str: 4, + unicode: 1, + usage: 0x103, + }); + streams[4].cache = ''; + streams[4].mode = 1; + this.stop = 1; + } + if ( stream === -4 ) + { + if ( streams[4].mode === 2 ) + { + this.Glk.glk_stream_close( streams[4].str ); + } + streams[4].mode = 0; + } + }, + + output_stream_handler: function( str ) + { + var ram = this.ram, + streams = this.io.streams, + data = this.fileref_data; + + if ( data.str === 2 ) + { + ram.setUint8( 0x11, ( ram.getUint8( 0x11 ) & 0xFE ) | ( str ? 1 : 0 ) ); + if ( str ) + { + streams[2].mode = 2; + streams[2].str = str; + this.io.transcript = 1; + if ( streams[2].cache ) + { + this.Glk.glk_put_jstring_stream( streams[2].str, streams[2].cache ); + } + } + else + { + streams[2].mode = this.io.transcript = 0; + } + } + + if ( data.str === 4 ) + { + if ( str ) + { + streams[4].mode = 2; + streams[4].str = str; + if ( streams[4].cache ) + { + this.Glk.glk_put_jstring_stream( streams[4].str, streams[4].cache ); + } + } + else + { + streams[4].mode = 0; + } + } + }, + + // Print text! + _print: function( text ) + { + var Glk = this.Glk, + io = this.io, + i = 0; + + // Stream 3 gets the text first + if ( io.streams[3].length ) + { + io.streams[3][0][1] += text; + } + else + { + // Convert CR into LF + text = text.replace( /\r/g, '\n' ); + + // Check the transcript bit + // Because it might need to prompt for a file name, we return here, and will print again in the handler + if ( ( this.m.getUint8( 0x11 ) & 0x01 ) !== io.transcript ) + { + this.output_stream( io.transcript ? -2 : 2, 0, 1 ); + } + + // Check if the monospace font bit has changed + // Unfortunately, even now Inform changes this bit for the font statement, even though the 1.1 standard depreciated it :( + if ( ( this.m.getUint8( 0x11 ) & 0x02 ) !== ( io.mono & 0x02 ) ) + { + io.mono ^= 0x02; + this.format(); + } + + // For the upper window we print each character individually so that we can track the cursor position + if ( io.currentwin && this.upperwin ) + { + // Don't automatically increase the size of the window + // If we confirm that games do need this then we can implement it later + while ( i < text.length && io.row < io.height ) + { + Glk.glk_put_jstring( text[i++] ); + io.col++; + if ( io.col === io.width ) + { + io.col = 0; + io.row++; + } + } + } + else if ( !io.currentwin ) + { + if ( io.streams[1] ) + { + Glk.glk_put_jstring( text ); + } + // Transcript + if ( io.streams[2].mode === 1 ) + { + io.streams[2].cache += text; + } + if ( io.streams[2].mode === 2 ) + { + Glk.glk_put_jstring_stream( io.streams[2].str, text ); + } + } + } + }, + + // Print many things + print: function( type, val ) + { + var proptable, result; + + // Number + if ( type === 0 ) + { + result = val; + } + // Unicode + if ( type === 1 ) + { + result = String.fromCharCode( val ); + } + // Text from address + if ( type === 2 ) + { + result = this.jit[ val ] || this.decode( val ); + } + // Object + if ( type === 3 ) + { + proptable = this.m.getUint16( this.objects + ( this.version3 ? 9 : 14 ) * val + ( this.version3 ? 7 : 12 ) ); + result = this.decode( proptable + 1, this.m.getUint8( proptable ) * 2 ); + } + // ZSCII + if ( type === 4 ) + { + if ( !this.unicode_table[ val ] ) + { + return; + } + result = this.unicode_table[ val ]; + } + this._print( '' + result ); + }, + + print_table: function( zscii, width, height, skip ) + { + height = height || 1; + skip = skip || 0; + var i = 0; + while ( i++ < height ) + { + this._print( this.zscii_to_text( this.m.getUint8Array( zscii, width ) ) + ( i < height ? '\r' : '' ) ); + zscii += width + skip; + } + }, + + // Process CSS default colours + /*process_colours: function() + { + // Convert RGB to a Z-Machine true colour + // RGB is a css colour code. rgb(), #000000 and #000 formats are supported. + function convert_RGB( code ) + { + var round = Math.round, + data = /(\d+),\s*(\d+),\s*(\d+)|#(\w{1,2})(\w{1,2})(\w{1,2})/.exec( code ), + result; + + // Nice rgb() code + if ( data[1] ) + { + result = [ data[1], data[2], data[3] ]; + } + else + { + // Messy CSS colour code + result = [ parseInt( data[4], 16 ), parseInt( data[5], 16 ), parseInt( data[6], 16 ) ]; + // Stretch out compact #000 codes to their full size + if ( code.length === 4 ) + { + result = [ result[0] << 4 | result[0], result[1] << 4 | result[1], result[2] << 4 | result[2] ]; + } + } + + // Convert to a 15bit colour + return round( result[2] / 8.226 ) << 10 | round( result[1] / 8.226 ) << 5 | round( result[0] / 8.226 ); + } + + // Standard colours + var colours = [ + 0xFFFE, // Current + 0xFFFF, // Default + 0x0000, // Black + 0x001D, // Red + 0x0340, // Green + 0x03BD, // Yellow + 0x59A0, // Blue + 0x7C1F, // Magenta + 0x77A0, // Cyan + 0x7FFF, // White + 0x5AD6, // Light grey + 0x4631, // Medium grey + 0x2D6B, // Dark grey + ], + + // Start with CSS colours provided by the runner + fg_css = this.options.fgcolour, + bg_css = this.options.bgcolour, + // Convert to true colour for storing in the header + fg_true = fg_css ? convert_RGB( fg_css ) : 0xFFFF, + bg_true = bg_css ? convert_RGB( bg_css ) : 0xFFFF, + // Search the list of standard colours + fg = colours.indexOf( fg_true ), + bg = colours.indexOf( bg_true ); + // ZVMUI must have colours for reversing text, even if we don't write them to the header + // So use the given colours or assume black on white + if ( fg < 2 ) + { + fg = fg_css || 2; + } + if ( bg < 2 ) + { + bg = bg_css || 9; + } + + utils.extend( this.options, { + fg: fg, + bg: bg, + fg_true: fg_true, + bg_true: bg_true, + }); + },*/ + + // Request line input + read: function( storer, text, parse, time, routine ) + { + var len = this.m.getUint8( text ), + initiallen = 0, + buffer, + input_stream1_len; + + if ( this.version3 ) + { + this.v3_status(); + } + // The spec is badly phrased; the buffer capacity includes the zero terminator + // See https://github.com/DFillmore/Z-Machine-Standard/issues/76 + if (this.version < 5) + { + len-- + } + //else + //{ + //initiallen = this.m.getUint8( text + 1 ); + //} + + buffer = Array( len ); + buffer.fill( 0 ) + this.read_data = { + buffer: buffer, + bufaddr: text, // text-buffer + parseaddr: parse, // parse-buffer + routine: routine, + storer: storer, + time: time, + }; + + // Input stream 1 + if ( this.io.streams[0] ) + { + input_stream1_len = this.Glk.glk_get_line_stream_uni( this.io.streams[0], buffer ); + + // Check for a newline character + if ( buffer[input_stream1_len - 1] === 0x0A ) + { + input_stream1_len--; + } + if ( input_stream1_len ) + { + this._print( String.fromCharCode.apply( null, buffer.slice( 0, input_stream1_len ) ) + '\n' ); + this.handle_line_input( input_stream1_len ); + return this.stop = 0; + } + else + { + this.input_stream( 0 ); + } + } + + // TODO: pre-existing input + this.Glk.glk_request_line_event_uni( this.io.currentwin ? this.upperwin : this.mainwin, buffer, initiallen ); + this.fix_upper_window(); + }, + + // Request character input + read_char: function( storer, one, time, routine ) + { + // Input stream 1 + if ( this.io.streams[0] ) + { + var code = this.Glk.glk_get_char_stream_uni( this.io.streams[0] ); + // Check for EOF + if ( code === -1 ) + { + this.input_stream( 0 ); + } + else + { + this.variable( storer, code ); + return this.stop = 0; + } + } + + this.read_data = { + routine: routine, + storer: storer, + time: time, + }; + this.Glk.glk_request_char_event_uni( this.io.currentwin ? this.upperwin : this.mainwin ); + this.fix_upper_window(); + }, + + set_colour: function(foreground, background) + { + this.set_true_colour(zcolours[foreground], zcolours[background]) + }, + + // Note that row and col must be decremented in JIT code + set_cursor: function( row, col ) + { + var io = this.io; + + // 8.7.2.3: do nothing if the lower window is selected + if ( !io.currentwin ) + { + return + } + + if ( row >= io.height ) + { + // Moving the cursor to a row forces the upper window + // to open enough for that line to exist + this.split_window( row + 1 ); + } + if ( this.upperwin && row >= 0 && col >= 0 && col < io.width ) + { + this.Glk.glk_window_move_cursor( this.upperwin, col, row ); + io.row = row; + io.col = col; + } + }, + + set_font: function( font ) + { + var returnval = this.io.mono & 0x04 ? 4 : 1; + if ( font === 0 ) + { + return returnval; + } + // We only support fonts 1 and 4 + if ( font !== 1 && font !== 4 ) + { + return 0; + } + if ( font !== returnval ) + { + this.io.mono ^= 0x04; + this.format(); + } + return returnval; + }, + + // Set styles + set_style: function( stylebyte ) + { + var io = this.io; + + // Setting the style to Roman will clear the others + if ( stylebyte === 0 ) + { + io.reverse = io.bold = io.italic = 0; + io.mono &= 0xFE; + } + if ( stylebyte & 0x01 ) + { + io.reverse = 1; + } + if ( stylebyte & 0x02 ) + { + io.bold = 0x04; + } + if ( stylebyte & 0x04 ) + { + io.italic = 0x02; + } + if ( stylebyte & 0x08 ) + { + io.mono |= 0x01; + } + this.format(); + }, + + // Set true colours + set_true_colour: function(foreground, background) + { + const Glk = this.Glk + if (Glk.glk_gestalt(0x1100, 0)) + { + let fg, bg + if (foreground === 0xFFFE) + { + fg = -2 + } + else + { + if (foreground === 0xFFFF) + { + fg = -1 + } + else + { + fg = convert_true_colour(foreground) + } + this.io.fg = fg + } + + if (background === 0xFFFE) + { + bg = -2 + } + else + { + if (background === 0xFFFF) + { + bg = -1 + } + else + { + bg = convert_true_colour(background) + } + this.io.bg = bg + } + + // Set the colours for each open window + Glk.garglk_set_zcolors_stream(this.mainwin.str, fg, bg) + if (this.upperwin) + { + Glk.garglk_set_zcolors_stream(this.upperwin.str, fg, bg) + } + } + }, + + set_window: function( window ) + { + this.io.currentwin = window; + + // Focusing the upper window resets the cursor to the top left; + // it also opens the upper window if it's not open + if ( window ) + { + this.set_cursor( 0, 0 ); + } + + this.Glk.glk_set_window( this.upperwin && window ? this.upperwin : this.mainwin ); + this.format(); + }, + + split_window: function( lines ) + { + var Glk = this.Glk, + io = this.io, + row = io.row, col = io.col, + oldheight = io.height, + str; + io.height = lines; + + // Erase existing lines if we are expanding into existing rows + if ( this.upperwin && lines > oldheight ) + { + str = Glk.glk_window_get_stream( this.upperwin ); + while ( oldheight < lines ) + { + Glk.glk_window_move_cursor( this.upperwin, 0, oldheight++ ); + Glk.glk_put_jstring_stream( str, Array( io.width + 1 ).join( ' ' ) ); + } + Glk.glk_window_move_cursor( this.upperwin, col, row ); + } + + // Don't decrease the height of the window yet, only increase + if ( lines > io.maxheight ) + { + io.maxheight = lines; + + // Set the height of the window + // Create the window if it doesn't exist + if ( !this.upperwin ) + { + if (this.io.bg >= 0) + { + Glk.glk_stylehint_set(4, 0, 8, this.io.bg) + } + this.upperwin = Glk.glk_window_open( this.mainwin, 0x12, io.maxheight, 4, 203 ); + if (this.Glk.glk_gestalt(0x1100, 0)) + { + Glk.garglk_set_zcolors_stream(this.upperwin.str, this.io.fg, this.io.bg) + } + Glk.glk_stylehint_clear(4, 0, 8) + } + else + { + Glk.glk_window_set_arrangement( Glk.glk_window_get_parent( this.upperwin ), 0x12, io.maxheight, null ); + } + io.glkheight = io.maxheight + } + + if ( lines ) + { + // Reset the cursor if it is now outside the window + if ( io.row >= lines ) + { + this.set_cursor( 0, 0 ); + } + // 8.6.1.1.2: In version three the upper window is always cleared + if ( this.version3 ) + { + Glk.glk_window_clear( this.upperwin ); + } + } + }, + + // Update the header after restarting or restoring + update_header: function() + { + var ram = this.ram; + + // Reset the Xorshift seed + this.xorshift_seed = 0; + + // Update the screen size variables - in version 3 does not actually set the header variables + this.update_screen_size() + + // For version 3 we only set Flags 1 + if ( this.version3 ) + { + return ram.setUint8( 0x01, + ( ram.getUint8( 0x01 ) & 0x8F ) // Keep all except bits 4-6 + | ( this.statuswin ? 0x20 : 0x10 ) // If status win is available then set 0x20 for the upper win also being available, otherwise 0x10 for the status win itself + | 0x40 // Variable pitch font is default - Or can we tell from options if the font is fixed pitch? + ); + } + + // Flags 1 + ram.setUint8( 0x01, + (this.Glk.glk_gestalt(0x1100, 0) ? 1 : 0) // Check if colour is supported + | 0x1C // Bold, italic and mono are supported + | 0x00 // Timed input not supported yet + ); + + // Flags 2: Clear bits 3, 5, 7: no character graphics, mouse or sound effects + // This is really a word, but we only care about the lower byte + ram.setUint8( 0x11, ram.getUint8( 0x11 ) & 0x57 ); + + // Font height/width in "units" + if ( this.version > 4 ) + { + ram.setUint16( 0x26, 0x0101 ) + } + + // Colours + //ram.setUint8( 0x2C, isNaN( this.options.bg ) ? 1 : this.options.bg ); + //ram.setUint8( 0x2D, isNaN( this.options.fg ) ? 1 : this.options.fg ); + //this.extension_table( 5, this.options.fg_true ); + //this.extension_table( 6, this.options.bg_true ); + + // Z Machine Spec revision + ram.setUint16( 0x32, 0x0102 ); + + // Clear flags three, we don't support any of that stuff + this.extension_table( 4, 0 ); + }, + + update_screen_size: function() + { + const Glk = this.Glk + const height_box = new Glk.RefBox() + const width_box = new Glk.RefBox() + const tempwin = Glk.glk_window_open( this.mainwin, 0x12, 0, 4, 0 ) + let height = 0 + let width = 0 + + // The main window is proportional, so its width may not be accurate + // If the upper or status window is present, use its width, or else try to make a temp window + // The height is the total of all windows + + Glk.glk_window_get_size( this.mainwin, width_box, height_box ) + height = height_box.get_value() + + if ( this.upperwin ) + { + Glk.glk_window_get_size( this.upperwin, width_box, height_box ) + height += height_box.get_value() + } + if ( this.statuswin ) + { + Glk.glk_window_get_size( this.statuswin, width_box, height_box ) + height += height_box.get_value() + } + if ( tempwin ) + { + Glk.glk_window_get_size( tempwin, width_box, 0 ) + Glk.glk_window_close( tempwin ) + } + + // Use whichever width was available + width = width_box.get_value() + + // Cap the dimensions + // Height is capped to 254 as 255 means infinite, which breaks some games + height = Math.min( height, 254 ) + width = this.io.width = Math.min( width, 255 ) + + // Update the header + if ( this.version > 3 ) + { + this.ram.setUint8( 0x20, height ) + this.ram.setUint8( 0x21, width ) + } + if ( this.version > 4 ) + { + this.ram.setUint16( 0x22, width ) + this.ram.setUint16( 0x24, height ) + } + + // Fix the cursor if it is outside the window + if ( this.io.col >= width ) + { + this.io.col = width - 1 + } + }, + + // Output the version 3 status line + v3_status: function() + { + if ( !this.statuswin ) + { + return; + } + + var Glk = this.Glk, + str = Glk.glk_window_get_stream( this.statuswin ), + memory = this.m, + width = this.io.width, + hours_score = memory.getUint16( this.globals + 2 ), + mins_turns = memory.getUint16( this.globals + 4 ), + proptable = memory.getUint16( this.objects + 9 * memory.getUint16( this.globals ) + 7 ), + shortname = '' + this.decode( proptable + 1, memory.getUint8( proptable ) * 2 ), + rhs; + + // Handle the turns/score or time + if ( memory.getUint8( 0x01 ) & 0x02 ) + { + rhs = 'Time: ' + ( hours_score % 12 === 0 ? 12 : hours_score % 12 ) + ':' + ( mins_turns < 10 ? '0' : '' ) + mins_turns + ' ' + ( hours_score > 11 ? 'PM' : 'AM' ); + } + else + { + rhs = 'Score: ' + hours_score + ' Turns: ' + mins_turns; + } + + // Print a blank line in reverse + Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); + Glk.glk_put_jstring_stream( str, Array( width + 1 ).join( ' ' ) ); + + // Trim the shortname if necessary + Glk.glk_window_move_cursor( this.statuswin, 0, 0 ); + Glk.glk_put_jstring_stream( str, ' ' + shortname.slice( 0, width - rhs.length - 4 ) ); + + // Print the right hand side + Glk.glk_window_move_cursor( this.statuswin, width - rhs.length - 1, 0 ); + Glk.glk_put_jstring_stream( str, rhs ); + }, + +}; + +},{"../common/utils.js":3}],7:[function(require,module,exports){ +/* + +Z-Machine opcodes +================= + +Copyright (c) 2017 The ifvms.js team +MIT licenced +https://github.com/curiousdannii/ifvms.js + +*/ + +'use strict'; + +/* + +TODO: + Abstract out the signed conversions such that they can be eliminated if possible + don't access memory directly + +*/ + +var AST = require( '../common/ast.js' ), +Variable = AST.Variable, +Opcode = AST.Opcode, +Stopper = AST.Stopper, +Pauser = AST.Pauser, +PauserStorer = AST.PauserStorer, +Brancher = AST.Brancher, +BrancherStorer = AST.BrancherStorer, +Storer = AST.Storer, +Caller = AST.Caller, +CallerStorer = AST.CallerStorer, +opcode_builder = AST.opcode_builder, + +// Common functions, variables and opcodes +simple_func = function( a ) { return '' + a; }, +stack_var = new Variable( this.e, 0 ), +alwaysbranch = opcode_builder( Brancher, function() { return 1; } ), +not = opcode_builder( Storer, function( a ) { return 'e.S2U(~' + a + ')'; } ), + +// Indirect storer opcodes - rather non-generic I'm afraid +// Not used for inc/dec +// @load (variable) -> (result) +// @pull (variable) +// @store (variable) value +Indirect = Storer.subClass({ + storer: 0, + + post: function() + { + var operands = this.operands, + op0 = operands[0], + op0isVar = op0 instanceof Variable; + + // Replace the indirect operand with a Variable, and set .indirect if needed + operands[0] = new Variable( this.e, op0isVar ? op0 : op0.v ); + if ( op0isVar || op0.v === 0 ) + { + operands[0].indirect = 1; + } + + // Get the storer + this.storer = this.code === 142 ? operands.pop() : operands.shift(); + + // @pull needs an added stack. If for some reason it was compiled with two operands this will break! + if ( operands.length === 0 ) + { + operands.push( stack_var ); + } + }, + + func: simple_func, +}), + +Incdec = Opcode.subClass({ + func: function( variable ) + { + var varnum = variable.v - 1, + operator = this.code % 2 ? 1 : -1; + + // Fallback to the runtime function if our variable is a variable operand itself + // Or, if it's a global + if ( variable instanceof Variable || varnum > 14 ) + { + return 'e.incdec(' + variable + ',' + operator + ')'; + } + + return ( varnum < 0 ? 'e.s[e.sp-1]' : 'e.l[' + varnum + ']' ) + ( operator === 1 ? '++' : '--' ); + }, +}), + +// Version 3 @save/restore branch instead of store +V3SaveRestore = Stopper.subClass({ + brancher: 1, + + toString: function() + { + return 'e.stop=1;e.' + ( this.code === 181 ? 'save' : 'restore' ) + '(' + ( this.pc + 1 ) + ')'; + }, +}), + +V45Restore = opcode_builder( PauserStorer, function() { return 'e.restore(' + ( this.next - 1 ) + ')'; } ), +V45Save = opcode_builder( PauserStorer, function() { return 'e.save(' + ( this.next - 1 ) + ')'; } ); + +/*eslint brace-style: "off" */ +/*eslint indent: "off" */ + +module.exports = function( version ) +{ + +return { + +/* je */ 1: opcode_builder( Brancher, function() { return arguments.length === 2 ? this.args( '===' ) : 'e.jeq(' + this.args() + ')'; } ), +/* jl */ 2: opcode_builder( Brancher, function( a, b ) { return a.U2S() + '<' + b.U2S(); } ), +/* jg */ 3: opcode_builder( Brancher, function( a, b ) { return a.U2S() + '>' + b.U2S(); } ), +// Too many U2S/S2U for these... +/* dec_chk */ 4: opcode_builder( Brancher, function( variable, value ) { return 'e.U2S(e.incdec(' + variable + ',-1))<' + value.U2S(); } ), +/* inc_chk */ 5: opcode_builder( Brancher, function( variable, value ) { return 'e.U2S(e.incdec(' + variable + ',1))>' + value.U2S(); } ), +/* jin */ 6: opcode_builder( Brancher, function() { return 'e.jin(' + this.args() + ')'; } ), +/* test */ 7: opcode_builder( Brancher, function() { return 'e.test(' + this.args() + ')'; } ), +/* or */ 8: opcode_builder( Storer, function() { return this.args( '|' ); } ), +/* and */ 9: opcode_builder( Storer, function() { return this.args( '&' ); } ), +/* test_attr */ 10: opcode_builder( Brancher, function() { return 'e.test_attr(' + this.args() + ')'; } ), +/* set_attr */ 11: opcode_builder( Opcode, function() { return 'e.set_attr(' + this.args() + ')'; } ), +/* clear_attr */ 12: opcode_builder( Opcode, function() { return 'e.clear_attr(' + this.args() + ')'; } ), +/* store */ 13: Indirect, +/* insert_obj */ 14: opcode_builder( Opcode, function() { return 'e.insert_obj(' + this.args() + ')'; } ), +/* loadw */ 15: opcode_builder( Storer, function( array, index ) { return 'e.m.getUint16(e.S2U(' + array + '+2*' + index.U2S() + '))'; } ), +/* loadb */ 16: opcode_builder( Storer, function( array, index ) { return 'e.m.getUint8(e.S2U(' + array + '+' + index.U2S() + '))'; } ), +/* get_prop */ 17: opcode_builder( Storer, function() { return 'e.get_prop(' + this.args() + ')'; } ), +/* get_prop_addr */ 18: opcode_builder( Storer, function() { return 'e.find_prop(' + this.args() + ')'; } ), +/* get_next_prop */ 19: opcode_builder( Storer, function() { return 'e.find_prop(' + this.args( ',0,' ) + ')'; } ), +/* add */ 20: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '+' ) + ')'; } ), +/* sub */ 21: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '-' ) + ')'; } ), +/* mul */ 22: opcode_builder( Storer, function() { return 'e.S2U(' + this.args( '*' ) + ')'; } ), +/* div */ 23: opcode_builder( Storer, function( a, b ) { return 'e.S2U(parseInt(' + a.U2S() + '/' + b.U2S() + '))'; } ), +/* mod */ 24: opcode_builder( Storer, function( a, b ) { return 'e.S2U(' + a.U2S() + '%' + b.U2S() + ')'; } ), +/* call_2s */ 25: CallerStorer, +/* call_2n */ 26: Caller, +/* set_colour */ 27: opcode_builder( Opcode, function() { return 'e.set_colour(' + this.args() + ')'; } ), +/* throw */ 28: opcode_builder( Stopper, function( value, cookie ) { return 'while(e.frames.length+1>' + cookie + '){e.frameptr=e.frames.pop()}return ' + value; } ), +/* jz */ 128: opcode_builder( Brancher, function( a ) { return a + '===0'; } ), +/* get_sibling */ 129: opcode_builder( BrancherStorer, function( obj ) { return 'e.get_sibling(' + obj + ')'; } ), +/* get_child */ 130: opcode_builder( BrancherStorer, function( obj ) { return 'e.get_child(' + obj + ')'; } ), +/* get_parent */ 131: opcode_builder( Storer, function( obj ) { return 'e.get_parent(' + obj + ')'; } ), +/* get_prop_length */ 132: opcode_builder( Storer, function( a ) { return 'e.get_prop_len(' + a + ')'; } ), +/* inc */ 133: Incdec, +/* dec */ 134: Incdec, +/* print_addr */ 135: opcode_builder( Opcode, function( addr ) { return 'e.print(2,' + addr + ')'; } ), +/* call_1s */ 136: CallerStorer, +/* remove_obj */ 137: opcode_builder( Opcode, function( obj ) { return 'e.remove_obj(' + obj + ')'; } ), +/* print_obj */ 138: opcode_builder( Opcode, function( obj ) { return 'e.print(3,' + obj + ')'; } ), +/* ret */ 139: opcode_builder( Stopper, function( a ) { return 'return ' + a; } ), +/* jump */ 140: opcode_builder( Stopper, function( a ) { return 'e.pc=' + a.U2S() + '+' + ( this.next - 2 ); } ), +/* print_paddr */ 141: opcode_builder( Opcode, function( addr ) { return 'e.print(2,' + addr + '*' + this.e.addr_multipler + ')'; } ), +/* load */ 142: Indirect.subClass( { storer: 1 } ), +143: version < 5 ? + /* not (v3/4) */ not : + /* call_1n (v5/8) */ Caller, +/* rtrue */ 176: opcode_builder( Stopper, function() { return 'return 1'; } ), +/* rfalse */ 177: opcode_builder( Stopper, function() { return 'return 0'; } ), +// Reconsider a generalised class for @print/@print_ret? +/* print */ 178: opcode_builder( Opcode, function( text ) { return 'e.print(2,' + text + ')'; }, { printer: 1 } ), +/* print_ret */ 179: opcode_builder( Stopper, function( text ) { return 'e.print(2,' + text + ');e.print(1,13);return 1'; }, { printer: 1 } ), +/* nop */ 180: Opcode, +/* save (v3/4) */ 181: version < 4 ? + V3SaveRestore : + V45Save, +/* restore(v3/4) */ 182: version < 4 ? + V3SaveRestore : + V45Restore, +/* restart */ 183: opcode_builder( Stopper, function() { return 'e.restart()'; } ), +/* ret_popped */ 184: opcode_builder( Stopper, function( a ) { return 'return ' + a; }, { post: function() { this.operands.push( stack_var ); } } ), +185: version < 5 ? + /* pop (v3/4) */ opcode_builder( Opcode, function() { return 's[--e.sp]'; } ) : + /* catch (v5/8) */ opcode_builder( Storer, function() { return 'e.frames.length+1'; } ), +/* quit */ 186: opcode_builder( Pauser, function() { return 'e.quit=1;e.Glk.glk_exit()'; } ), +/* new_line */ 187: opcode_builder( Opcode, function() { return 'e.print(1,13)'; } ), +188: version < 4 ? + /* show_status (v3) */ opcode_builder( Stopper, function() { return 'e.pc=' + this.next + ';e.v3_status()'; } ) : + /* act as a nop in later versions */ Opcode, +/* verify */ 189: alwaysbranch, // Actually check?? +/* piracy */ 191: alwaysbranch, +/* call_vs */ 224: CallerStorer, +/* storew */ 225: opcode_builder( Opcode, function( array, index, value ) { return 'e.ram.setUint16(e.S2U(' + array + '+2*' + index.U2S() + '),' + value + ')'; } ), +/* storeb */ 226: opcode_builder( Opcode, function( array, index, value ) { return 'e.ram.setUint8(e.S2U(' + array + '+' + index.U2S() + '),' + value + ')'; } ), +/* put_prop */ 227: opcode_builder( Opcode, function() { return 'e.put_prop(' + this.args() + ')'; } ), +/* read */ 228: version < 5 ? + opcode_builder( Pauser, function() { return 'e.read(0,' + this.args() + ')'; } ) : + opcode_builder( PauserStorer, function() { return 'e.read(' + this.storer.v + ',' + this.args() + ')'; } ), +/* print_char */ 229: opcode_builder( Opcode, function( a ) { return 'e.print(4,' + a + ')'; } ), +/* print_num */ 230: opcode_builder( Opcode, function( a ) { return 'e.print(0,' + a.U2S() + ')'; } ), +/* random */ 231: opcode_builder( Storer, function( a ) { return 'e.random(' + a.U2S() + ')'; } ), +/* push */ 232: opcode_builder( Storer, simple_func, { post: function() { this.storer = stack_var; }, storer: 0 } ), +/* pull */ 233: Indirect, +/* split_window */ 234: opcode_builder( Opcode, function( lines ) { return 'e.split_window(' + lines + ')'; } ), +/* set_window */ 235: opcode_builder( Opcode, function( wind ) { return 'e.set_window(' + wind + ')'; } ), +/* call_vs2 */ 236: CallerStorer, +/* erase_window */ 237: opcode_builder( Opcode, function( win ) { return 'e.erase_window(' + win.U2S() + ')'; } ), +/* erase_line */ 238: opcode_builder( Opcode, function( a ) { return 'e.erase_line(' + a + ')'; } ), +/* set_cursor */ 239: opcode_builder( Opcode, function( row, col ) { return 'e.set_cursor(' + row + '-1,' + col + '-1)'; } ), +/* get_cursor */ 240: opcode_builder( Opcode, function( addr ) { return 'e.get_cursor(' + addr + ')'; } ), +/* set_text_style */ 241: opcode_builder( Opcode, function( stylebyte ) { return 'e.set_style(' + stylebyte + ')'; } ), +/* buffer_mode */ 242: Opcode, // We don't support non-buffered output +/* output_stream */ 243: opcode_builder( Stopper, function() { return 'e.pc=' + this.next + ';e.output_stream(' + this.args() + ')'; } ), +/* input_stream */ 244: opcode_builder( Pauser, function() { return 'e.input_stream(' + this.args() + ')'; } ), +/* sound_effect */ 245: Opcode, // We don't support sounds +/* read_char */ 246: opcode_builder( PauserStorer, function() { return 'e.read_char(' + this.storer.v + ',' + ( this.args() || '1' ) + ')'; } ), +/* scan_table */ 247: opcode_builder( BrancherStorer, function() { return 'e.scan_table(' + this.args() + ')'; } ), +/* not (v5/8) */ 248: not, +/* call_vn */ 249: Caller, +/* call_vn2 */ 250: Caller, +/* tokenise */ 251: opcode_builder( Opcode, function() { return 'e.tokenise(' + this.args() + ')'; } ), +/* encode_text */ 252: opcode_builder( Opcode, function() { return 'e.encode_text(' + this.args() + ')'; } ), +/* copy_table */ 253: opcode_builder( Opcode, function() { return 'e.copy_table(' + this.args() + ')'; } ), +/* print_table */ 254: opcode_builder( Opcode, function() { return 'e.print_table(' + this.args() + ')'; } ), +/* check_arg_count */ 255: opcode_builder( Brancher, function( arg ) { return 'e.stack.getUint8(e.frameptr+5)&(1<<(' + arg + '-1))'; } ), +/* save */ 1000: V45Save, +/* restore */ 1001: V45Restore, +/* log_shift */ 1002: opcode_builder( Storer, function( a, b ) { return 'e.S2U(e.log_shift(' + a + ',' + b.U2S() + '))'; } ), +/* art_shift */ 1003: opcode_builder( Storer, function( a, b ) { return 'e.S2U(e.art_shift(' + a.U2S() + ',' + b.U2S() + '))'; } ), +/* set_font */ 1004: opcode_builder( Storer, function( font ) { return 'e.set_font(' + font + ')'; } ), +/* save_undo */ 1009: opcode_builder( Storer, function() { return 'e.save_undo(' + this.next + ',' + this.storer.v + ')'; } ), +// As the standard says calling this without a save point is illegal, we don't need to actually store anything (but it must still be disassembled) +/* restore_undo */ 1010: opcode_builder( Opcode, function() { return 'if(e.restore_undo())return'; }, { storer: 1 } ), +/* print_unicode */ 1011: opcode_builder( Opcode, function( a ) { return 'e.print(1,' + a + ')'; } ), +// Assume we can print and read all unicode characters rather than actually testing +/* check_unicode */ 1012: opcode_builder( Storer, function() { return 3; } ), +/* set_true_colour */ 1013: opcode_builder( Opcode, function() { return 'e.set_true_colour(' + this.args() + ')'; } ), +/* sound_data */ 1014: Opcode.subClass( { brancher: 1 } ), // We don't support sounds (but disassemble the branch address) +/* gestalt */ 1030: opcode_builder( Storer, function() { return 'e.gestalt(' + this.args() + ')'; } ), +/* parchment */ //1031: opcode_builder( Storer, function() { return 'e.op_parchment(' + this.args() + ')'; } ), + +}; + +}; + +},{"../common/ast.js":1}],8:[function(require,module,exports){ +/* + +Z-Machine runtime functions +=========================== + +Copyright (c) 2020 The ifvms.js team +MIT licenced +https://github.com/curiousdannii/ifvms.js + +*/ + +'use strict'; + +/* + +TODO: + Save/restore: table, name, prompt support + +*/ + +const file = require( '../common/file.js' ) +const utils = require( '../common/utils.js' ) +const extend = utils.extend +const U2S = utils.U2S16 +const S2U = utils.S2U16 + +// A clone function which ignores the properties we don't want to serialise +function clone( obj ) +{ + const recurse = obj => typeof obj === 'object' ? clone( obj ) : obj + const newobj = {} + + if ( Array.isArray( obj ) ) + { + return obj.map( recurse ) + } + + for ( let prop in obj ) + { + if ( prop !== 'buffer' && prop !== 'str' ) + { + newobj[prop] = recurse( obj[prop] ) + } + } + return newobj +} + +// Test whether we are running on a littleEndian system +const littleEndian = (function() +{ + var testUint8Array = new Uint8Array( 2 ), + testUint16Array = new Uint16Array( testUint8Array.buffer ); + testUint16Array[0] = 1; + return testUint8Array[0] === 1; +})() + +function fix_stack_endianness( view, start, end, auto ) +{ + if ( littleEndian && !auto ) + { + while ( start < end ) + { + view.setUint16( start, view.getUint16( start, 1 ) ); + start += 2; + } + } +} + +module.exports = { + + art_shift: function( number, places ) + { + return places > 0 ? number << places : number >> -places; + }, + + // Call a routine + call: function( addr, storer, next, args ) + { + // 6.4.3: Calls to 0 instead just store 0 + if ( addr === 0 ) + { + if ( storer >= 0 ) + { + this.variable( storer, 0 ); + } + return this.pc = next; + } + + // Get the number of locals and advance the pc + this.pc = addr * this.addr_multipler; + var locals_count = this.m.getUint8( this.pc++ ), + + stack = this.stack, + i = 0, + + // Write the current stack use + frameptr = this.frameptr; + stack.setUint16( frameptr + 6, this.sp ); + this.frames.push( frameptr ); + + // Create a new frame + frameptr = this.frameptr = this.s.byteOffset + this.sp * 2; + // Return address + stack.setUint32( frameptr, next << 8 ); + // Flags + stack.setUint8( frameptr + 3, ( storer >= 0 ? 0 : 0x10 ) | locals_count ); + // Storer + stack.setUint8( frameptr + 4, storer >= 0 ? storer : 0 ); + // Supplied arguments + stack.setUint8( frameptr + 5, ( 1 << args.length ) - 1 ); + + // Create the locals and stack + this.make_stacks(); + this.sp = 0; + while ( i < locals_count ) + { + this.l[i] = i < args.length ? args[i] : ( this.version < 5 ? this.m.getUint16( this.pc + i * 2 ) : 0 ); + i++; + } + if ( this.version < 5 ) + { + this.pc += locals_count * 2; + } + }, + + clear_attr: function( object, attribute ) + { + var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0; + this.ram.setUint8( addr, this.m.getUint8( addr ) & ~( 0x80 >> attribute % 8 ) ); + }, + + copy_table: function( first, second, size ) + { + size = U2S( size ); + var ram = this.ram, + i = 0, + allowcorrupt = size < 0; + size = Math.abs( size ); + + // Simple case, zeroes + if ( second === 0 ) + { + while ( i < size ) + { + ram.setUint8( first + i++, 0 ); + } + return; + } + + if ( allowcorrupt ) + { + while ( i < size ) + { + ram.setUint8( second + i, this.m.getUint8( first + i++ ) ); + } + } + else + { + ram.setUint8Array( second, this.m.getUint8Array( first, size ) ); + } + }, + + do_autorestore: function( snapshot ) + { + const Glk = this.Glk + + // Restore Glk + Glk.restore_allstate( snapshot.glk ) + + // Get references to our Glk objects + this.io = snapshot.io + const RockBox = new Glk.RefBox() + let obj + while ( obj = Glk.glk_window_iterate( obj, RockBox ) ) + { + if ( RockBox.value === 201 ) + { + this.mainwin = obj + if ( obj.linebuf ) + { + snapshot.read_data.buffer = obj.linebuf + } + } + if ( RockBox.value === 202 ) + { + this.statuswin = obj + } + if ( RockBox.value === 203 ) + { + this.upperwin = obj + } + } + obj = null + while ( obj = Glk.glk_stream_iterate( obj, RockBox ) ) + { + if ( RockBox.value === 210 ) + { + this.io.streams[2].str = obj + } + if ( RockBox.value === 211 ) + { + this.io.streams[4].str = obj + } + } + + // Restart and restore the RAM and stacks + this.restart(1) + this.restore_file(this.options.Dialog.streaming ? new Uint8Array(snapshot.ram) : Uint8Array.from(snapshot.ram), 1) + + // Set remaining data from the snapshot + this.read_data = snapshot.read_data + this.xorshift_seed = snapshot.xorshift_seed + }, + + do_autosave: function( save ) + { + if ( !this.options.Dialog ) + { + throw new Error( 'A reference to Dialog is required' ) + } + + let snapshot = null + if ( ( save || 0 ) >= 0 ) + { + const ram = this.save_file(this.pc, 1) + snapshot = { + glk: this.Glk.save_allstate(), + io: clone( this.io ), + ram: this.options.Dialog.streaming ? ram : Array.from(new Uint8Array(ram)), + read_data: clone( this.read_data ), + xorshift_seed: this.xorshift_seed, + } + } + + this.options.Dialog.autosave_write( this.signature, snapshot ) + }, + + encode_text: function( zscii, length, from, target ) + { + this.ram.setUint8Array( target, this.encode( this.m.getUint8Array( zscii + from, length ) ) ); + }, + + // Access the extension table + extension_table: function( word, value ) + { + var addr = this.extension; + if ( !addr || word > this.extension_count ) + { + return 0; + } + addr += 2 * word; + if ( value === undefined ) + { + return this.m.getUint16( addr ); + } + this.ram.setUint16( addr, value ); + }, + + // Find the address of a property, or given the previous property, the number of the next + find_prop: function( object, property, prev ) + { + var memory = this.m, + version3 = this.version3, + + this_property_byte, this_property, + last_property = 0, + + // Get this property table + properties = memory.getUint16( this.objects + ( version3 ? 9 : 14 ) * object + ( version3 ? 7 : 12 ) ); + + // Skip over the object's short name + properties += memory.getUint8( properties ) * 2 + 1; + + // Run through the properties + while ( 1 ) + { + this_property_byte = memory.getUint8( properties ); + this_property = this_property_byte & ( version3 ? 0x1F : 0x3F ); + + // Found the previous property, so return this one's number + if ( last_property === prev ) + { + return this_property; + } + // Found the property! Return its address + if ( this_property === property ) + { + // Must include the offset + return properties + ( !version3 && this_property_byte & 0x80 ? 2 : 1 ); + } + // Gone past the property + if ( this_property < property ) + { + return 0; + } + + // Go to next property + last_property = this_property; + + // Calculate the size of this property and skip to the next + if ( version3 ) + { + properties += ( this_property_byte >> 5 ) + 2; + } + else + { + if ( this_property_byte & 0x80 ) + { + this_property = memory.getUint8( properties + 1 ) & 0x3F; + properties += this_property ? this_property + 2 : 66; + } + else + { + properties += this_property_byte & 0x40 ? 3 : 2; + } + } + } + }, + + // 1.2 spec @gestalt + gestalt: function( id /*, arg*/ ) + { + switch ( id ) + { + case 1: + return 0x0102; + } + return 0; + }, + + // Get the first child of an object + get_child: function( obj ) + { + if ( this.version3 ) + { + return this.m.getUint8( this.objects + 9 * obj + 6 ); + } + else + { + return this.m.getUint16( this.objects + 14 * obj + 10 ); + } + }, + + get_sibling: function( obj ) + { + if ( this.version3 ) + { + return this.m.getUint8( this.objects + 9 * obj + 5 ); + } + else + { + return this.m.getUint16( this.objects + 14 * obj + 8 ); + } + }, + + get_parent: function( obj ) + { + if ( this.version3 ) + { + return this.m.getUint8( this.objects + 9 * obj + 4 ); + } + else + { + return this.m.getUint16( this.objects + 14 * obj + 6 ); + } + }, + + get_prop: function( object, property ) + { + var memory = this.m, + + // Try to find the property + addr = this.find_prop( object, property ), + len; + + // If we have the property + if ( addr ) + { + len = memory.getUint8( addr - 1 ); + // Assume we're being called for a valid short property + return memory[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'getUint16' : 'getUint8' ]( addr ); + } + + // Use the default properties table + // Remember that properties are 1-indexed + return memory.getUint16( this.properties + 2 * ( property - 1 ) ); + }, + + // Get the length of a property + // This opcode expects the address of the property data, not a property block + get_prop_len: function( addr ) + { + // Spec 1.1 + if ( addr === 0 ) + { + return 0; + } + + var value = this.m.getUint8( addr - 1 ); + + // Version 3 + if ( this.version3 ) + { + return ( value >> 5 ) + 1; + } + + // Two size/number bytes + if ( value & 0x80 ) + { + value &= 0x3F; + return value === 0 ? 64 : value; + } + // One byte size/number + return value & 0x40 ? 2 : 1; + }, + + // Quick hack for @inc/@dec/@inc_chk/@dec_chk + incdec: function( varnum, change ) + { + if ( varnum === 0 ) + { + this.s[this.sp - 1] += change; + return this.s[this.sp - 1]; + } + if ( --varnum < 15 ) + { + this.l[varnum] += change; + return this.l[varnum]; + } + else + { + var offset = this.globals + ( varnum - 15 ) * 2; + this.ram.setUint16( offset, this.m.getUint16( offset ) + change ); + return this.ram.getUint16( offset ); + } + }, + + // Indirect variables + indirect: function( variable, value ) + { + if ( variable === 0 ) + { + if ( arguments.length > 1 ) + { + return this.s[this.sp - 1] = value; + } + else + { + return this.s[this.sp - 1]; + } + } + return this.variable( variable, value ); + }, + + insert_obj: function( obj, dest ) + { + // First remove the obj from wherever it was + this.remove_obj( obj ); + // Now add it to the destination + this.set_family( obj, dest, dest, obj, obj, this.get_child( dest ) ); + }, + + // @jeq + jeq: function() + { + var i = 1; + + // Account for many arguments + while ( i < arguments.length ) + { + if ( arguments[i++] === arguments[0] ) + { + return 1; + } + } + }, + + jin: function( child, parent ) + { + return this.get_parent( child ) === parent; + }, + + log: function( message ) + { + if ( this.options.GlkOte ) + { + this.options.GlkOte.log( message ); + } + }, + + log_shift: function( number, places ) + { + return places > 0 ? number << places : number >>> -places; + }, + + make_stacks: function() + { + var locals_count = this.stack.getUint8( this.frameptr + 3 ) & 0x0F; + this.l = new Uint16Array( this.stack.buffer, this.frameptr + 8, locals_count ); + this.s = new Uint16Array( this.stack.buffer, this.frameptr + 8 + locals_count * 2 ); + }, + + put_prop: function( object, property, value ) + { + // Try to find the property + var addr = this.find_prop( object, property ), + len; + + if ( addr ) + { + len = this.m.getUint8( addr - 1 ); + + // Assume we're being called for a valid short property + this.ram[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'setUint16' : 'setUint8' ]( addr, value ); + } + }, + + random: function( range ) + { + var seed = this.xorshift_seed; + + // Switch to the Xorshift RNG (or switch off if range == 0) + if ( range < 1 ) + { + this.xorshift_seed = range; + return 0; + } + + // Pure randomness + if ( seed === 0 ) + { + return 1 + ( Math.random() * range ) | 0; + } + + // Based on the discussions in this forum topic, we will not implement the sequential mode recommended in the standard + // http://www.intfiction.org/forum/viewtopic.php?f=38&t=16023 + + // Instead implement a 32 bit Xorshift generator + seed ^= ( seed << 13 ); + seed ^= ( seed >> 17 ); + this.xorshift_seed = ( seed ^= ( seed << 5 ) ); + return 1 + ( ( seed & 0x7FFF ) % range ); + }, + + remove_obj: function( obj ) + { + var parent = this.get_parent( obj ), + older_sibling, + younger_sibling, + temp_younger; + + // No parent, do nothing + if ( parent === 0 ) + { + return; + } + + older_sibling = this.get_child( parent ); + younger_sibling = this.get_sibling( obj ); + + // obj is first child + if ( older_sibling === obj ) + { + this.set_family( obj, 0, parent, younger_sibling ); + } + // obj isn't first child, so fix the older sibling + else + { + // Go through the tree until we find the older sibling + while ( 1 ) + { + temp_younger = this.get_sibling( older_sibling ); + if ( temp_younger === obj ) + { + break; + } + older_sibling = temp_younger; + } + this.set_family( obj, 0, 0, 0, older_sibling, younger_sibling ); + } + }, + + // (Re)start the VM + restart: function(autorestoring) + { + var ram = this.ram, + version = ram.getUint8( 0x00 ), + version3 = version === 3, + addr_multipler = version3 ? 2 : ( version === 8 ? 8 : 4 ), + flags2 = ram.getUint8( 0x11 ), + property_defaults = ram.getUint16( 0x0A ), + extension = ( version > 4 ) ? ram.getUint16( 0x36 ) : 0, + stack = utils.MemoryView( this.options.stack_len ); + + // Reset the RAM, but preserve flags 2 + ram.setUint8Array( 0, this.origram ); + ram.setUint8( 0x11, flags2 ); + + extend( this, { + + // Locals and stacks of various kinds + stack: stack, + frameptr: 0, + frames: [], + s: new Uint16Array( stack.buffer, 8 ), + sp: 0, + l: [], + undo: [], + undo_len: 0, + + glk_blocking_call: null, + + // Get some header variables + version: version, + version3: version3, + pc: ram.getUint16( 0x06 ), + properties: property_defaults, + objects: property_defaults + ( version3 ? 53 : 112 ), // 62-9 or 126-14 - if we take this now then we won't need to always decrement the object number + globals: ram.getUint16( 0x0C ), + // staticmem: set in prepare() + eof: ( ram.getUint16( 0x1A ) || 65536 ) * addr_multipler, + extension: extension, + extension_count: extension ? ram.getUint16( extension ) : 0, + + // Routine and string multiplier + addr_multipler: addr_multipler, + + // Opcodes for this version of the Z-Machine + opcodes: require( './opcodes.js' )( version ), + + }); + + this.init_text(); + if (!autorestoring) + { + this.init_io() + } + + // Update the header + this.update_header(); + }, + + // Request a restore + restore: function( pc ) + { + this.pc = pc; + this.fileref_create_by_prompt({ + func: 'restore', + mode: 0x02, + usage: 0x01, + }); + }, + + restore_file: function( data, autorestoring ) + { + var ram = this.ram, + quetzal = new file.Quetzal( data ), + qmem = quetzal.memory, + stack = this.stack, + flags2 = ram.getUint8( 0x11 ), + temp, + i = 0, j = 0; + + // Check this is a savefile for this story + if ( ram.getUint16( 0x02 ) !== quetzal.release || ram.getUint16( 0x1C ) !== quetzal.checksum ) + { + return 0; + } + while ( i < 6 ) + { + if ( ram.getUint8( 0x12 + i ) !== quetzal.serial[i++] ) + { + return 0; + } + } + i = 0; + + // Memory chunk + // Reset the RAM + ram.setUint8Array( 0, this.origram ); + if ( quetzal.compressed ) + { + while ( i < qmem.length ) + { + temp = qmem[i++]; + // Same memory + if ( temp === 0 ) + { + j += 1 + qmem[i++]; + } + else + { + ram.setUint8( j, temp ^ this.origram[j++] ); + } + } + } + else + { + ram.setUint8Array( 0, qmem ); + } + // Preserve flags 2 + ram.setUint8( 0x11, flags2 ); + + // Stacks + stack.setUint8Array( 0, quetzal.stacks ); + this.frames = []; + i = 0; + while ( i < quetzal.stacks.byteLength ) + { + this.frameptr = i; + this.frames.push( i ); + // Swap the bytes of the locals and stacks + fix_stack_endianness( stack, j = i + 8, j += ( stack.getUint8( i + 3 ) & 0x0F ) * 2, autorestoring ) + fix_stack_endianness( stack, j, j += stack.getUint16( i + 6 ) * 2, autorestoring ) + i = j; + } + this.frames.pop(); + this.sp = stack.getUint16( this.frameptr + 6 ); + this.make_stacks(); + + this.pc = quetzal.pc; + this.update_header(); + + // Collapse the upper window (8.6.1.3) + if ( this.version3 ) + { + this.split_window( 0 ); + } + + return 2; + }, + + restore_undo: function() + { + if ( this.undo.length === 0 ) + { + return 0; + } + + var state = this.undo.pop(); + this.frameptr = state.frameptr; + this.pc = state.pc; + this.undo_len -= ( state.ram.byteLength + state.stack.byteLength ); + + // Replace the ram, preserving flags 2 + state.ram[0x11] = this.m.getUint8( 0x11 ); + this.ram.setUint8Array( 0, state.ram ); + + // Fix up the stack + this.frames = state.frames; + this.sp = state.sp; + this.stack.setUint8Array( 0, state.stack ); + this.make_stacks(); + + this.variable( state.var, 2 ); + return 1; + }, + + // Return from a routine + ret: function( result ) + { + var stack = this.stack, + + // Get the storer and return pc from this frame + frameptr = this.frameptr, + storer = stack.getUint8( frameptr + 3 ) & 0x10 ? -1 : stack.getUint8( frameptr + 4 ); + this.pc = stack.getUint32( frameptr ) >> 8; + + // Recreate the locals and stacks from the previous frame + frameptr = this.frameptr = this.frames.pop(); + this.make_stacks(); + this.sp = stack.getUint16( frameptr + 6 ); + + // Store the result if there is one + if ( storer >= 0 ) + { + this.variable( storer, result || 0 ); + } + }, + + // pc is the address of the storer operand (or branch in v3) + save: function( pc ) + { + this.pc = pc; + this.fileref_create_by_prompt({ + func: 'save', + mode: 0x01, + usage: 0x01, + }); + }, + + save_file: function( pc, autosaving ) + { + var memory = this.m, + quetzal = new file.Quetzal(), + stack = utils.MemoryView( this.stack.buffer.slice() ), + zeroes = 0, + i, j, + frameptr = this.frameptr, + abyte; + + // IFhd chunk + quetzal.release = memory.getUint16( 0x02 ); + quetzal.serial = memory.getUint8Array( 0x12, 6 ); + quetzal.checksum = memory.getUint16( 0x1C ); + quetzal.pc = pc; + + // Memory chunk + if ( autosaving ) + { + quetzal.memory = this.m.getUint8Array( 0, this.staticmem ) + } + else + { + const compressed_mem = [] + quetzal.compressed = 1; + for ( i = 0; i < this.staticmem; i++ ) + { + abyte = memory.getUint8( i ) ^ this.origram[i]; + if ( abyte === 0 ) + { + if ( ++zeroes === 256 ) + { + compressed_mem.push( 0, 255 ); + zeroes = 0; + } + } + else + { + if ( zeroes ) + { + compressed_mem.push( 0, zeroes - 1 ); + zeroes = 0; + } + compressed_mem.push( abyte ); + } + } + quetzal.memory = compressed_mem; + } + + // Stacks + // Set the current sp + stack.setUint16( frameptr + 6, this.sp ); + + // Swap the bytes of the locals and stacks + if ( littleEndian && !autosaving ) + { + const frames = this.frames.slice() + frames.push( frameptr ) + for ( i = 0; i < frames.length; i++ ) + { + frameptr = frames[i]; + fix_stack_endianness( stack, j = frameptr + 8, j += ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 ); + fix_stack_endianness( stack, j, j += stack.getUint16( frameptr + 6 ) * 2 ); + } + } + quetzal.stacks = stack.getUint8Array( 0, this.frameptr + 8 + ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 + this.sp * 2 ); + + return quetzal.write(); + }, + + save_restore_handler: function( str ) + { + var memory = this.m, + Glk = this.Glk, + result = 0, + buffer = [], + temp, iftrue, offset; + + if ( str ) + { + // Save + if ( this.fileref_data.func === 'save' ) + { + Glk.glk_put_buffer_stream( str, new Uint8Array( this.save_file( this.pc ) ) ); + result = 1; + } + // Restore + else + { + buffer = new Uint8Array( 128 * 1024 ); + Glk.glk_get_buffer_stream( str, buffer ); + result = this.restore_file( buffer.buffer ); + } + Glk.glk_stream_close( str ); + } + + // Store the result / branch in z3 + if ( this.version3 ) + { + // Calculate the branch + temp = memory.getUint8( this.pc++ ); + iftrue = temp & 0x80; + offset = temp & 0x40 ? + // single byte address + temp & 0x3F : + // word address, but first get the second byte of it + ( temp << 8 | memory.getUint8( this.pc++ ) ) << 18 >> 18; + + if ( !result === !iftrue ) + { + if ( offset === 0 || offset === 1 ) + { + this.ret( offset ); + } + else + { + this.pc += offset - 2; + } + } + } + else + { + this.variable( memory.getUint8( this.pc++ ), result ); + } + }, + + save_undo: function( pc, variable ) + { + // Drop an old undo state if we've reached the limit, but always save at least one state + var state + if ( this.undo_len > this.options.undo_len ) + { + state = this.undo.shift() + this.undo_len -= ( state.ram.byteLength + state.stack.byteLength ) + } + state = { + frameptr: this.frameptr, + frames: this.frames.slice(), + pc: pc, + ram: this.m.getUint8Array( 0, this.staticmem ), + sp: this.sp, + stack: this.stack.getUint8Array( 0, this.s.byteOffset + this.sp * 2 ), + var: variable, + } + this.undo_len += ( state.ram.byteLength + state.stack.byteLength ) + this.undo.push( state ) + return 1 + }, + + scan_table: function( key, addr, length, form ) + { + form = form || 0x82; + var memoryfunc = form & 0x80 ? 'getUint16' : 'getUint8'; + form &= 0x7F; + length = addr + length * form; + + while ( addr < length ) + { + if ( this.m[memoryfunc]( addr ) === key ) + { + return addr; + } + addr += form; + } + return 0; + }, + + set_attr: function( object, attribute ) + { + var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0; + this.ram.setUint8( addr, this.m.getUint8( addr ) | 0x80 >> attribute % 8 ); + }, + + set_family: function( obj, newparent, parent, child, bigsis, lilsis ) + { + var ram = this.ram, + objects = this.objects; + + if ( this.version3 ) + { + // Set the new parent of the obj + ram.setUint8( objects + 9 * obj + 4, newparent ); + // Update the parent's first child if needed + if ( parent ) + { + ram.setUint8( objects + 9 * parent + 6, child ); + } + // Update the little sister of a big sister + if ( bigsis ) + { + ram.setUint8( objects + 9 * bigsis + 5, lilsis ); + } + } + else + { + // Set the new parent of the obj + ram.setUint16( objects + 14 * obj + 6, newparent ); + // Update the parent's first child if needed + if ( parent ) + { + ram.setUint16( objects + 14 * parent + 10, child ); + } + // Update the little sister of a big sister + if ( bigsis ) + { + ram.setUint16( objects + 14 * bigsis + 8, lilsis ); + } + } + }, + + test: function( bitmap, flag ) + { + return ( bitmap & flag ) === flag; + }, + + test_attr: function( object, attribute ) + { + return ( this.m.getUint8( this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0 ) << attribute % 8 ) & 0x80; + }, + + // Read or write a variable + variable: function( variable, value ) + { + var havevalue = value !== undefined, + offset; + if ( variable === 0 ) + { + if ( havevalue ) + { + this.s[this.sp++] = value; + } + else + { + return this.s[--this.sp]; + } + } + else if ( --variable < 15 ) + { + if ( havevalue ) + { + this.l[variable] = value; + } + else + { + return this.l[variable]; + } + } + else + { + offset = this.globals + ( variable - 15 ) * 2; + if ( havevalue ) + { + this.ram.setUint16( offset, value ); + } + else + { + return this.m.getUint16( offset ); + } + } + return value; + }, + + // Utilities for signed arithmetic + U2S: U2S, + S2U: S2U, + +}; + +},{"../common/file.js":2,"../common/utils.js":3,"./opcodes.js":7}],9:[function(require,module,exports){ +/* + +Z-Machine text functions +======================== + +Copyright (c) 2017 The ifvms.js team +MIT licenced +https://github.com/curiousdannii/ifvms.js + +*/ + +/* + +TODO: + Consider quote suggestions from 1.1 spec + +*/ + +module.exports = { + + init_text: function() + { + var self = this, + memory = this.m, + + alphabet_addr = ( this.version > 4 ) && memory.getUint16( 0x34 ), + unicode_addr = this.extension_table( 3 ), + unicode_len = unicode_addr && memory.getUint8( unicode_addr++ ); + + this.abbr_addr = memory.getUint16( 0x18 ); + + // Generate alphabets + function make_alphabet( data ) + { + var alphabets = [[], [], []], + i = 0; + while ( i < 78 ) + { + alphabets[( i / 26 ) | 0][i % 26] = data[ i++ ]; + } + // A2->7 is always a newline + alphabets[2][1] = 13; + self.alphabets = alphabets; + } + + // Make the unicode tables + function make_unicode( data ) + { + var table = { 13: '\r' }, // New line conversion + reverse = { 13: 13 }, + i = 0; + while ( i < data.length ) + { + table[155 + i] = String.fromCharCode( data[i] ); + reverse[data[i]] = 155 + i++; + } + i = 32; + while ( i < 127 ) + { + table[i] = String.fromCharCode( i ); + reverse[i] = i++; + } + self.unicode_table = table; + self.reverse_unicode_table = reverse; + } + + // Check for custom alphabets + make_alphabet( alphabet_addr ? memory.getUint8Array( alphabet_addr, 78 ) + // Or use the standard alphabet + : this.text_to_zscii( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \r0123456789.,!?_#\'"/\\-:()', 1 ) ); + + // Check for a custom unicode table + make_unicode( unicode_addr ? memory.getUint16Array( unicode_addr, unicode_len ) + // Or use the default + : this.text_to_zscii( unescape( '%E4%F6%FC%C4%D6%DC%DF%BB%AB%EB%EF%FF%CB%CF%E1%E9%ED%F3%FA%FD%C1%C9%CD%D3%DA%DD%E0%E8%EC%F2%F9%C0%C8%CC%D2%D9%E2%EA%EE%F4%FB%C2%CA%CE%D4%DB%E5%C5%F8%D8%E3%F1%F5%C3%D1%D5%E6%C6%E7%C7%FE%F0%DE%D0%A3%u0153%u0152%A1%BF' ), 1 ) ); + + // Parse the standard dictionary + this.dictionaries = {}; + this.dict = memory.getUint16( 0x08 ); + this.parse_dict( this.dict ); + + // Optimise our own functions + /*if ( DEBUG ) + { + if ( !debugflags.nooptimise ) + optimise_obj( this, 'TEXT' ); + }*/ + }, + + // Decode Z-chars into ZSCII and then Unicode + decode: function( addr, length ) + { + var memory = this.m, + + start_addr = addr, + temp, + buffer = [], + i = 0, + zchar, + alphabet = 0, + result = [], + resulttexts = [], + usesabbr, + unicodecount = 0; + + // Check if this one's been cached already + if ( this.jit[addr] ) + { + return this.jit[addr]; + } + + // If we've been given a length, then use it as the finaladdr, + // Otherwise don't go past the end of the file + length = length ? length + addr : this.eof; + + // Go through until we've reached the end of the text or a stop bit + while ( addr < length ) + { + temp = memory.getUint16( addr ); + addr += 2; + + buffer.push( temp >> 10 & 0x1F, temp >> 5 & 0x1F, temp & 0x1F ); + + // Stop bit + if ( temp & 0x8000 ) + { + break; + } + } + + // Process the Z-chars + while ( i < buffer.length ) + { + zchar = buffer[i++]; + + // Special chars + // Space + if ( zchar === 0 ) + { + result.push( 32 ); + } + // Abbreviations + else if ( zchar < 4 ) + { + usesabbr = 1; + result.push( -1 ); + resulttexts.push( '\uE000+this.abbr(' + ( 32 * ( zchar - 1 ) + buffer[i++] ) + ')+\uE000' ); + } + // Shift characters + else if ( zchar < 6 ) + { + alphabet = zchar; + } + // Check for a 10 bit ZSCII character + else if ( alphabet === 2 && zchar === 6 ) + { + // Check we have enough Z-chars left. + if ( i + 1 < buffer.length ) + { + result.push( buffer[i++] << 5 | buffer[i++] ) + } + } + // Regular characters + else if ( zchar < 0x20 ) + { + result.push( this.alphabets[alphabet][ zchar - 6 ] ); + } + + // Reset the alphabet + alphabet = alphabet < 4 ? 0 : alphabet - 3; + + // Add to the index if we've had raw unicode + if ( ( i % 3 ) === 0 ) + { + i += unicodecount; + unicodecount = 0; + } + } + + result = this.zscii_to_text( result, resulttexts ); + // Abbreviations must be extracted at run time, so return a function instead + if ( usesabbr ) + { + result = { + toString: ( Function( 'return"' + result.replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ).replace( /\r/g, '\\r' ).replace( /\uE000/g, '"' ) + '"' ) ).bind( this ), + }; + } + // Cache and return + if ( start_addr >= this.staticmem ) + { + this.jit[start_addr] = result; + } + return result; + }, + + // Encode ZSCII into Z-chars + encode: function( zscii ) + { + var alphabets = this.alphabets, + zchars = [], + word_len = this.version3 ? 6 : 9, + i = 0, + achar, + temp, + result = []; + + // Encode the Z-chars + while ( zchars.length < word_len ) + { + achar = zscii[i++]; + // Space + if ( achar === 32 ) + { + zchars.push( 0 ); + } + // Alphabets + else if ( ( temp = alphabets[0].indexOf( achar ) ) >= 0 ) + { + zchars.push( temp + 6 ); + } + else if ( ( temp = alphabets[1].indexOf( achar ) ) >= 0 ) + { + zchars.push( 4, temp + 6 ); + } + else if ( ( temp = alphabets[2].indexOf( achar ) ) >= 0 ) + { + zchars.push( 5, temp + 6 ); + } + // Pad character + else if ( achar === undefined ) + { + zchars.push( 5 ); + } + // 10-bit ZSCII + else + { + zchars.push( 5, 6, achar >> 5, achar & 0x1F ) + } + } + zchars.length = word_len; + + // Encode to bytes + i = 0; + while ( i < word_len ) + { + result.push( zchars[i++] << 2 | zchars[i] >> 3, ( zchars[i++] & 0x07 ) << 5 | zchars[i++] ); + } + result[ result.length - 2 ] |= 0x80; + return result; + }, + + // In these two functions zscii means an array of ZSCII codes and text means a regular Javascript unicode string + zscii_to_text: function( zscii, texts ) + { + var i = 0, l = zscii.length, + charr, + j = 0, + result = ''; + + while ( i < l ) + { + charr = zscii[i++]; + // Text substitution from abbreviations or 1.1 unicode + if ( charr === -1 ) + { + result += texts[j++]; + } + // Regular characters + if ( ( charr = this.unicode_table[charr] ) ) + { + result += charr; + } + } + return result; + }, + + // If the second argument is set then don't use the unicode table + text_to_zscii: function( text, notable ) + { + var array = [], i = 0, l = text.length, charr; + while ( i < l ) + { + charr = text.charCodeAt( i++ ); + // Check the unicode table + if ( !notable ) + { + charr = this.reverse_unicode_table[charr] || 63; + } + array.push( charr ); + } + return array; + }, + + // Parse and cache a dictionary + parse_dict: function( addr ) + { + var memory = this.m, + + addr_start = addr, + dict = {}, + entry_len, + endaddr, + + // Get the word separators + seperators_len = memory.getUint8( addr++ ); + + // Support: IE, Safari, Firefox<38, Chrome<45, Opera<32, Node<4 + // These browsers don't support Uint8Array.indexOf() so convert to a normal array + dict.separators = Array.prototype.slice.call( memory.getUint8Array( addr, seperators_len ) ); + + addr += seperators_len; + + // Go through the dictionary and cache its entries + entry_len = memory.getUint8( addr++ ); + endaddr = addr + 2 + entry_len * memory.getUint16( addr ); + addr += 2; + while ( addr < endaddr ) + { + dict[ Array.prototype.toString.call( memory.getUint8Array( addr, this.version3 ? 4 : 6 ) ) ] = addr; + addr += entry_len; + } + this.dictionaries[addr_start] = dict; + + return dict; + }, + + // Print an abbreviation + abbr: function( abbrnum ) + { + return this.decode( this.m.getUint16( this.abbr_addr + 2 * abbrnum ) * 2 ); + }, + + // Tokenise a text + tokenise: function( bufaddr, parseaddr, dictionary, flag ) + { + // Use the default dictionary if one wasn't provided + dictionary = dictionary || this.dict; + + // Parse the dictionary if needed + dictionary = this.dictionaries[dictionary] || this.parse_dict( dictionary ); + + var memory = this.m, + ram = this.ram, + bufferlength = 1e3, + i = 1, + letter, + separators = dictionary.separators, + word, + words = [], + max_words, + dictword, + wordcount = 0; + + // In versions 5 and 8 we can get the actual buffer length + if ( this.version > 4 ) + { + bufferlength = memory.getUint8( bufaddr + i++ ) + 2; + } + + // Find the words, separated by the separators, but as well as the separators themselves + while ( i < bufferlength ) + { + letter = memory.getUint8( bufaddr + i ); + if ( letter === 0 ) + { + break; + } + else if ( letter === 32 || separators.indexOf( letter ) >= 0 ) + { + if ( letter !== 32 ) + { + words.push( [ [letter], i ] ); + } + word = null; + } + else + { + if ( !word ) + { + words.push( [ [], i ] ); + word = words[ words.length - 1 ][0]; + } + word.push( letter ); + } + i++; + } + + // Go through the text until we either have reached the max number of words, or we're out of words + max_words = Math.min( words.length, memory.getUint8( parseaddr ) ); + while ( wordcount < max_words ) + { + dictword = dictionary['' + this.encode( words[wordcount][0] )]; + + // If the flag is set then don't overwrite words which weren't found + if ( !flag || dictword ) + { + // Fill out the buffer + ram.setUint16( parseaddr + 2 + wordcount * 4, dictword || 0 ); + ram.setUint8( parseaddr + 4 + wordcount * 4, words[wordcount][0].length ); + ram.setUint8( parseaddr + 5 + wordcount * 4, words[wordcount][1] ); + } + wordcount++; + } + + // Update the number of found words + ram.setUint8( parseaddr + 1, wordcount ); + }, + +}; + +},{}]},{},[4])(4) +}); diff --git a/webpack.config.js b/webpack.config.js index e69044d401..4dee264d61 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -145,7 +145,7 @@ module.exports = (env, argv) => { }, }, }, - { test: /\.(ttf|woff2|png|jpe?g|gif|jp2|webp|svg)$/, type: "asset/resource" }, + { test: /\.(ttf|woff2|png|jpe?g|gif|jp2|webp|svg|z3)$/, type: "asset/resource" }, { test: /\.s?css$/, use: ["style-loader", "css-loader"], From 9d054df2aaefe12317d3c53270f30abebb2c301d Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:21:13 -0700 Subject: [PATCH 04/12] Arcade: Zork save-file Dialog layer backed by localStorage Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitattributes | 1 + src/Arcade/Zork/ZorkDialog.ts | 80 +++++++++++++++++++++++++++++ test/Arcade/Zork/ZorkDialog.test.ts | 34 ++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/Arcade/Zork/ZorkDialog.ts create mode 100644 test/Arcade/Zork/ZorkDialog.test.ts diff --git a/.gitattributes b/.gitattributes index 6313b56c57..7db2bc9212 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto eol=lf +*.z3 binary diff --git a/src/Arcade/Zork/ZorkDialog.ts b/src/Arcade/Zork/ZorkDialog.ts new file mode 100644 index 0000000000..3e281f3973 --- /dev/null +++ b/src/Arcade/Zork/ZorkDialog.ts @@ -0,0 +1,80 @@ +/** + * Minimal non-streaming Glk Dialog implementation backed by localStorage. + * Implements the subset of the GlkOte Dialog contract that vendor/glkapi.js + * uses, persisting Zork save files per game. See src/Arcade/Zork/README.md. + */ +export interface ZorkFileRef { + filename: string; + usage: string; + gameid?: string; +} + +export class ZorkDialog { + /** glkapi checks this to pick the non-streaming file API. */ + readonly streaming = false; + private readonly gameKey: string; + + constructor(gameKey: string) { + this.gameKey = gameKey; + } + + private key(ref: ZorkFileRef): string { + return `zork.${this.gameKey}.${ref.usage}.${ref.filename}`; + } + + file_construct_ref(filename = "", usage = "", gameid = ""): ZorkFileRef { + return { filename, usage, gameid }; + } + + file_construct_temp_ref(usage = ""): ZorkFileRef { + return { filename: `_temp_${usage}`, usage, gameid: "" }; + } + + file_clean_fixed_name(filename: string, __usage: string): string { + return filename.replace(/["/\\<>:|?*]/g, ""); + } + + file_ref_exists(ref: ZorkFileRef): boolean { + return localStorage.getItem(this.key(ref)) !== null; + } + + file_remove_ref(ref: ZorkFileRef): void { + localStorage.removeItem(this.key(ref)); + } + + /** content is an array of byte values (israw) or a string. Returns success. */ + file_write(ref: ZorkFileRef, content: number[] | string, __israw?: boolean): boolean { + try { + const payload = + typeof content === "string" ? { text: content } : { bytes: btoa(String.fromCharCode(...content)) }; + localStorage.setItem(this.key(ref), JSON.stringify(payload)); + return true; + } catch (e) { + console.error("ZorkDialog.file_write failed", e); + return false; + } + } + + file_read(ref: ZorkFileRef, __israw?: boolean): number[] | string | null { + const raw = localStorage.getItem(this.key(ref)); + if (raw === null) return null; + try { + const payload = JSON.parse(raw) as { text?: string; bytes?: string }; + if (typeof payload.text === "string") return payload.text; + if (typeof payload.bytes === "string") { + return Array.from(atob(payload.bytes), (c) => c.charCodeAt(0)); + } + return null; + } catch { + return null; // corrupt entry: treat as missing + } + } + + /** ZVM autosave hooks; unused (we don't enable do_vm_autosave), kept safe. */ + autosave_read(__signature: string): null { + return null; + } + autosave_write(__signature: string, __snapshot: unknown): void { + // no-op + } +} diff --git a/test/Arcade/Zork/ZorkDialog.test.ts b/test/Arcade/Zork/ZorkDialog.test.ts new file mode 100644 index 0000000000..9b21e081b7 --- /dev/null +++ b/test/Arcade/Zork/ZorkDialog.test.ts @@ -0,0 +1,34 @@ +import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; + +describe("ZorkDialog", () => { + beforeEach(() => localStorage.clear()); + + it("round-trips binary save data through localStorage", () => { + const dialog = new ZorkDialog("zork1"); + const ref = dialog.file_construct_ref("slot1", "save", ""); + expect(dialog.file_ref_exists(ref)).toBe(false); + const bytes = [0, 1, 2, 127, 128, 255, 66]; + expect(dialog.file_write(ref, bytes, true)).toBe(true); + expect(dialog.file_ref_exists(ref)).toBe(true); + expect(dialog.file_read(ref, true)).toEqual(bytes); + expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); + }); + + it("keeps games separate and removes refs", () => { + const d1 = new ZorkDialog("zork1"); + const d2 = new ZorkDialog("zork2"); + const ref = d1.file_construct_ref("slot1", "save", ""); + d1.file_write(ref, [1], true); + expect(d2.file_ref_exists(d2.file_construct_ref("slot1", "save", ""))).toBe(false); + d1.file_remove_ref(ref); + expect(d1.file_ref_exists(ref)).toBe(false); + expect(d1.file_read(ref, true)).toBeNull(); + }); + + it("cleans fixed names and constructs temp refs", () => { + const d = new ZorkDialog("zork1"); + expect(d.file_clean_fixed_name('a/b\\c:d"e', "save")).toBe("abcde"); + const temp = d.file_construct_temp_ref("data"); + expect(temp.filename.startsWith("_temp")).toBe(true); + }); +}); From b7f8a302f90f60a7ac023b09af5f1c1e2631d856 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:22:28 -0700 Subject: [PATCH 05/12] Arcade: GlkOte display adapter feeding React state Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/GlkOteReact.ts | 253 +++++++++++++++++++++++++++ test/Arcade/Zork/GlkOteReact.test.ts | 79 +++++++++ 2 files changed, 332 insertions(+) create mode 100644 src/Arcade/Zork/GlkOteReact.ts create mode 100644 test/Arcade/Zork/GlkOteReact.test.ts diff --git a/src/Arcade/Zork/GlkOteReact.ts b/src/Arcade/Zork/GlkOteReact.ts new file mode 100644 index 0000000000..6e133631d6 --- /dev/null +++ b/src/Arcade/Zork/GlkOteReact.ts @@ -0,0 +1,253 @@ +/** + * A GlkOte-protocol display implementation that feeds React state instead of + * manipulating the DOM (replaces glkote.js, which requires jQuery). + * vendor/glkapi.js calls the GlkOte contract methods; the terminal UI calls + * the send* methods. Protocol: https://eblong.com/zarf/glk/glkote/docs.html + */ +import type { ZorkDialog } from "./ZorkDialog"; + +export interface StyledRun { + style: string; + text: string; +} + +export interface TerminalState { + gridLines: StyledRun[][]; + bufferLines: StyledRun[][]; + inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; + filePrompt: { filemode: string; filetype: string } | null; + disabled: boolean; + error: string | null; +} + +export type TerminalListener = (state: TerminalState) => void; + +type RunOrString = string | { style?: string; text?: string }; +interface WindowUpdate { + id: number; + type: "grid" | "buffer" | "graphics"; + rock: number; + gridwidth?: number; + gridheight?: number; +} +interface ContentUpdate { + id: number; + clear?: boolean; + lines?: { line: number; content?: RunOrString[] }[]; + text?: { append?: boolean; flowbreak?: boolean; content?: RunOrString[] }[]; +} +interface InputUpdate { + id: number; + gen: number; + type: "line" | "char"; + maxlen?: number; + initial?: string; +} +export interface GlkOteUpdate { + type: "update" | "pass" | "retry" | "error"; + gen?: number; + windows?: WindowUpdate[]; + content?: ContentUpdate[]; + input?: InputUpdate[]; + specialinput?: { type: string; filemode: string; filetype: string }; + disable?: boolean; + message?: string; +} +interface GlkOteEvent { + type: string; + gen: number; + [key: string]: unknown; +} +interface AcceptInterface { + accept: (event: GlkOteEvent) => void; +} + +// Fixed 80x25 terminal metrics; glkapi derives grid rows/cols from these. +const METRICS = { + width: 720, + height: 450, + outspacingx: 0, + outspacingy: 0, + inspacingx: 0, + inspacingy: 0, + gridcharwidth: 9, + gridcharheight: 18, + gridmarginx: 0, + gridmarginy: 0, + buffercharwidth: 9, + buffercharheight: 18, + buffermarginx: 0, + buffermarginy: 0, +}; + +// Cap scrollback so multi-hour sessions don't grow unbounded. +const MAX_BUFFER_LINES = 2000; + +function parseRuns(content: RunOrString[] | undefined): StyledRun[] { + const runs: StyledRun[] = []; + if (!content) return runs; + let i = 0; + while (i < content.length) { + const item = content[i]; + if (typeof item === "string") { + // Alternating "style", "text" string pairs. + const text = content[i + 1]; + runs.push({ style: item, text: typeof text === "string" ? text : "" }); + i += 2; + } else { + runs.push({ style: item.style ?? "normal", text: item.text ?? "" }); + i += 1; + } + } + return runs; +} + +export class GlkOteReact { + private readonly dialog: ZorkDialog; + private listener: TerminalListener | null; + private iface: AcceptInterface | null = null; + private generation = 0; + private windows = new Map(); + private state: TerminalState = { + gridLines: [], + bufferLines: [], + inputRequest: null, + filePrompt: null, + disabled: false, + error: null, + }; + + constructor(dialog: ZorkDialog, listener: TerminalListener) { + this.dialog = dialog; + this.listener = listener; + } + + /* ---- GlkOte contract, called by vendor/glkapi.js ---- */ + + init(iface: AcceptInterface): void { + this.iface = iface; + iface.accept({ type: "init", gen: 0, metrics: METRICS, support: [] }); + } + + update(arg: GlkOteUpdate): void { + if (arg.type === "error") { + this.state.error = arg.message ?? "Unknown interpreter error"; + this.emit(); + return; + } + if (arg.type !== "update") return; + if (typeof arg.gen === "number") this.generation = arg.gen; + + if (arg.windows) { + this.windows.clear(); + for (const win of arg.windows) this.windows.set(win.id, win); + } + if (arg.content) { + for (const update of arg.content) this.applyContent(update); + } + // Input requests fully replace the previous set each update. + this.state.inputRequest = null; + if (arg.input) { + for (const req of arg.input) { + const win = this.windows.get(req.id); + // Zork only reads from the buffer window; prefer it if several ask. + if (!this.state.inputRequest || win?.type === "buffer") { + this.state.inputRequest = { + id: req.id, + gen: req.gen, + type: req.type, + maxlen: req.maxlen ?? 200, + initial: req.initial ?? "", + }; + } + } + } + this.state.filePrompt = + arg.specialinput?.type === "fileref_prompt" + ? { filemode: arg.specialinput.filemode, filetype: arg.specialinput.filetype } + : null; + this.state.disabled = !!arg.disable || (!this.state.inputRequest && !this.state.filePrompt); + this.emit(); + } + + log(__msg: string): void { + // Interpreter debug output: intentionally dropped. + } + warning(msg: string): void { + console.warn("Zork glkapi warning:", msg); + } + error(msg: string): void { + this.state.error = String(msg); + this.emit(); + } + getlibrary(name: string): unknown { + return name === "Dialog" ? this.dialog : null; + } + getinterface(): AcceptInterface | null { + return this.iface; + } + save_allstate(): undefined { + return undefined; // autosave not enabled + } + + /* ---- Outgoing events, called by the terminal UI / session ---- */ + + sendLine(text: string): void { + const req = this.state.inputRequest; + if (!this.iface || !req || req.type !== "line") return; + this.iface.accept({ type: "line", gen: this.generation, window: req.id, value: text }); + } + + sendChar(key: string): void { + const req = this.state.inputRequest; + if (!this.iface || !req || req.type !== "char") return; + this.iface.accept({ type: "char", gen: this.generation, window: req.id, value: key }); + } + + sendFileref(value: { filename: string; usage: string } | null): void { + if (!this.iface) return; + this.iface.accept({ type: "specialresponse", gen: this.generation, response: "fileref_prompt", value }); + } + + detach(): void { + this.listener = null; + this.iface = null; + } + + /* ---- Internals ---- */ + + private applyContent(update: ContentUpdate): void { + const win = this.windows.get(update.id); + if (!win) return; + if (win.type === "grid") { + const height = win.gridheight ?? 1; + while (this.state.gridLines.length < height) this.state.gridLines.push([]); + this.state.gridLines.length = height; + for (const line of update.lines ?? []) { + if (line.line >= 0 && line.line < height) this.state.gridLines[line.line] = parseRuns(line.content); + } + } else if (win.type === "buffer") { + if (update.clear) this.state.bufferLines = []; + for (const para of update.text ?? []) { + const runs = parseRuns(para.content); + if (para.append && this.state.bufferLines.length > 0) { + this.state.bufferLines[this.state.bufferLines.length - 1].push(...runs); + } else { + this.state.bufferLines.push(runs); + } + } + if (this.state.bufferLines.length > MAX_BUFFER_LINES) { + this.state.bufferLines = this.state.bufferLines.slice(-MAX_BUFFER_LINES); + } + } + } + + private emit(): void { + if (!this.listener) return; + this.listener({ + ...this.state, + gridLines: this.state.gridLines.map((l) => [...l]), + bufferLines: this.state.bufferLines.map((l) => [...l]), + }); + } +} diff --git a/test/Arcade/Zork/GlkOteReact.test.ts b/test/Arcade/Zork/GlkOteReact.test.ts new file mode 100644 index 0000000000..939a8e011e --- /dev/null +++ b/test/Arcade/Zork/GlkOteReact.test.ts @@ -0,0 +1,79 @@ +import { GlkOteReact, TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; +import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; + +function makeGlkOte(): { glkote: GlkOteReact; states: TerminalState[]; accepted: unknown[] } { + const states: TerminalState[] = []; + const accepted: unknown[] = []; + const glkote = new GlkOteReact(new ZorkDialog("test"), (s) => states.push(s)); + glkote.init({ accept: (ev: unknown) => accepted.push(ev) }); + return { glkote, states, accepted }; +} + +describe("GlkOteReact", () => { + it("sends an init event with metrics on init()", () => { + const { accepted } = makeGlkOte(); + expect(accepted).toHaveLength(1); + const ev = accepted[0] as { type: string; gen: number; metrics: { width: number } }; + expect(ev.type).toBe("init"); + expect(ev.metrics.width).toBeGreaterThan(0); + }); + + it("parses grid and buffer content updates (both run formats)", () => { + const { glkote, states } = makeGlkOte(); + glkote.update({ + type: "update", + gen: 1, + windows: [ + { id: 1, type: "grid", rock: 202, gridwidth: 80, gridheight: 1 }, + { id: 2, type: "buffer", rock: 201 }, + ], + content: [ + { id: 1, lines: [{ line: 0, content: ["normal", " West of House"] }] }, + { + id: 2, + text: [ + { content: [{ style: "normal", text: "You are standing in an open field" }] }, + { append: true, content: ["normal", " west of a white house."] }, + ], + }, + ], + input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], + }); + const s = states[states.length - 1]; + expect(s.gridLines[0].map((r) => r.text).join("")).toContain("West of House"); + expect(s.bufferLines).toHaveLength(1); + expect(s.bufferLines[0].map((r) => r.text).join("")).toBe( + "You are standing in an open field west of a white house.", + ); + expect(s.inputRequest).toMatchObject({ id: 2, type: "line" }); + }); + + it("clears buffer on clear, surfaces file prompts, and echoes line events back", () => { + const { glkote, states, accepted } = makeGlkOte(); + glkote.update({ + type: "update", + gen: 1, + windows: [{ id: 2, type: "buffer", rock: 201 }], + content: [{ id: 2, text: [{ content: ["normal", "old text"] }] }], + input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], + }); + glkote.sendLine("save"); + const lineEv = accepted[accepted.length - 1] as { type: string; window: number; value: string }; + expect(lineEv).toMatchObject({ type: "line", window: 2, value: "save" }); + + glkote.update({ + type: "update", + gen: 2, + specialinput: { type: "fileref_prompt", filemode: "write", filetype: "save" }, + }); + expect(states[states.length - 1].filePrompt).toMatchObject({ filemode: "write" }); + + glkote.sendFileref({ filename: "slot1", usage: "save" }); + const frEv = accepted[accepted.length - 1] as { type: string; response: string }; + expect(frEv).toMatchObject({ type: "specialresponse", response: "fileref_prompt" }); + + glkote.update({ type: "update", gen: 3, content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }] }); + const s = states[states.length - 1]; + expect(s.bufferLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toBe("fresh"); + }); +}); From 3f3df47b3d3623e06d1d4080806a8a353fa78249 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:23:53 -0700 Subject: [PATCH 06/12] Arcade: ZVM session wiring + headless Zork I smoke test Real ZVM + glkapi + display adapter + dialog playing actual Zork I: boots to West of House, opens the mailbox, saves to localStorage. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/metadata.ts | 15 ++++++++++++ src/Arcade/Zork/session.ts | 28 +++++++++++++++++++++ test/Arcade/Zork/engine.test.ts | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/Arcade/Zork/metadata.ts create mode 100644 src/Arcade/Zork/session.ts create mode 100644 test/Arcade/Zork/engine.test.ts diff --git a/src/Arcade/Zork/metadata.ts b/src/Arcade/Zork/metadata.ts new file mode 100644 index 0000000000..8d751245df --- /dev/null +++ b/src/Arcade/Zork/metadata.ts @@ -0,0 +1,15 @@ +import zork1Url from "./games/zork1.z3"; +import zork2Url from "./games/zork2.z3"; +import zork3Url from "./games/zork3.z3"; + +export interface ZorkGame { + key: "zork1" | "zork2" | "zork3"; + title: string; + url: string; +} + +export const ZorkGames: ZorkGame[] = [ + { key: "zork1", title: "Zork I: The Great Underground Empire", url: zork1Url }, + { key: "zork2", title: "Zork II: The Wizard of Frobozz", url: zork2Url }, + { key: "zork3", title: "Zork III: The Dungeon Master", url: zork3Url }, +]; diff --git a/src/Arcade/Zork/session.ts b/src/Arcade/Zork/session.ts new file mode 100644 index 0000000000..ba054f9738 --- /dev/null +++ b/src/Arcade/Zork/session.ts @@ -0,0 +1,28 @@ +/** + * Wires a fresh ZVM + glkapi instance to our display adapter and dialog. + * Wiring pattern verified against ifvms 1.1.6 bin/zvm.js: + * vm.prepare(story, options); Glk.init(options); // Glk.init calls vm.init() + */ +import ZVM from "./vendor/zvm.js"; +import { GlkClass } from "./vendor/glkapi.js"; +import { GlkOteReact, TerminalListener } from "./GlkOteReact"; +import { ZorkDialog } from "./ZorkDialog"; + +export interface ZorkSession { + glkote: GlkOteReact; + dispose(): void; +} + +export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession { + const dialog = new ZorkDialog(gameKey); + const glkote = new GlkOteReact(dialog, listener); + const vm = new ZVM(); + const Glk = new GlkClass(); + const options = { vm, Glk, GlkOte: glkote, Dialog: dialog }; + vm.prepare(story, options); + Glk.init(options); // synchronously runs the VM to its first input request + return { + glkote, + dispose: () => glkote.detach(), + }; +} diff --git a/test/Arcade/Zork/engine.test.ts b/test/Arcade/Zork/engine.test.ts new file mode 100644 index 0000000000..d86e265de1 --- /dev/null +++ b/test/Arcade/Zork/engine.test.ts @@ -0,0 +1,43 @@ +import * as fs from "fs"; +import * as path from "path"; +import type { TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; +import { createZorkSession } from "../../../src/Arcade/Zork/session"; + +function textOf(state: TerminalState): string { + return state.bufferLines.map((line) => line.map((r) => r.text).join("")).join("\n"); +} + +describe("Zork I engine smoke test", () => { + beforeEach(() => localStorage.clear()); + + it("boots Zork I, walks the opening, and saves", () => { + const story = new Uint8Array(fs.readFileSync(path.join(__dirname, "../../../src/Arcade/Zork/games/zork1.z3"))); + let state: TerminalState | null = null; + const session = createZorkSession(story, "zork1", (s) => (state = s)); + + // ZVM runs synchronously until the first input request. + expect(state).not.toBeNull(); + let s = state as unknown as TerminalState; + expect(textOf(s)).toContain("West of House"); + expect(textOf(s)).toContain("white house"); + expect(s.inputRequest?.type).toBe("line"); + + session.glkote.sendLine("open mailbox"); + s = state as unknown as TerminalState; + expect(textOf(s)).toContain("leaflet"); + + // Status line (grid window) shows the room. + expect(s.gridLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toContain("West of House"); + + // save -> fileref prompt -> localStorage entry appears. + session.glkote.sendLine("save"); + s = state as unknown as TerminalState; + expect(s.filePrompt?.filemode).toBe("write"); + session.glkote.sendFileref({ filename: "slot1", usage: "save" }); + s = state as unknown as TerminalState; + expect(textOf(s).toLowerCase()).toContain("ok"); + expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); + + session.dispose(); + }); +}); From f20ac621e8ee68a1129ed8e00860c3f58540d19d Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:25:42 -0700 Subject: [PATCH 07/12] Arcade: Zork terminal UI component Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/ZorkTerminal.tsx | 162 +++++++++++++++++++++++++ test/Arcade/Zork/ZorkTerminal.test.tsx | 70 +++++++++++ 2 files changed, 232 insertions(+) create mode 100644 src/Arcade/Zork/ZorkTerminal.tsx create mode 100644 test/Arcade/Zork/ZorkTerminal.test.tsx diff --git a/src/Arcade/Zork/ZorkTerminal.tsx b/src/Arcade/Zork/ZorkTerminal.tsx new file mode 100644 index 0000000000..715662f7cd --- /dev/null +++ b/src/Arcade/Zork/ZorkTerminal.tsx @@ -0,0 +1,162 @@ +/** + * Retro terminal renderer for the Zork arcade cabinets. + * Pure presentation: renders TerminalState, forwards input upward. + * Colors come from Settings.theme so player themes apply. + */ +import React, { useEffect, useRef, useState } from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { Settings } from "../../Settings/Settings"; +import type { StyledRun, TerminalState } from "./GlkOteReact"; + +export interface ZorkTerminalProps { + state: TerminalState; + onLine: (text: string) => void; + onChar: (key: string) => void; + onFileref: (value: { filename: string; usage: string } | null) => void; +} + +const MONO: React.CSSProperties = { + fontFamily: '"Lucida Console", "Consolas", monospace', + fontSize: "14px", + lineHeight: "18px", +}; + +function runStyle(run: StyledRun): React.CSSProperties { + return { + fontStyle: run.style === "emphasized" ? "italic" : undefined, + fontWeight: run.style === "subheader" || run.style === "header" ? "bold" : undefined, + }; +} + +function Line({ runs }: { runs: StyledRun[] }): React.ReactElement { + return ( +
+ {runs.map((run, i) => ( + + {run.text} + + ))} +
+ ); +} + +export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalProps): React.ReactElement { + const [entry, setEntry] = useState(""); + const [slotName, setSlotName] = useState(""); + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + inputRef.current?.focus(); + }, [state]); + + function keyDown(event: React.KeyboardEvent): void { + // Keep keystrokes inside the cabinet: no Bitburner hotkeys mid-game. + event.stopPropagation(); + if (state.inputRequest?.type === "char") { + event.preventDefault(); + onChar(event.key.length === 1 ? event.key : event.key.toLowerCase()); + return; + } + if (event.key === "Enter") { + const text = entry; + setEntry(""); + setHistory((h) => (text.trim() ? [...h, text] : h)); + setHistoryIndex(-1); + onLine(text); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + const idx = historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1); + if (history[idx] !== undefined) { + setHistoryIndex(idx); + setEntry(history[idx]); + } + } else if (event.key === "ArrowDown") { + event.preventDefault(); + const idx = historyIndex + 1; + if (idx >= history.length) { + setHistoryIndex(-1); + setEntry(""); + } else { + setHistoryIndex(idx); + setEntry(history[idx]); + } + } + } + + return ( + + {/* Status line: classic inverse-video Infocom bar */} + + {state.gridLines.length > 0 ? ( + state.gridLines.map((runs, i) => ) + ) : ( +
+ )} + + {/* Story buffer */} + + {state.bufferLines.map((runs, i) => ( + + ))} + {state.error && [ {state.error} ]} + {state.disabled && !state.error && !state.filePrompt && ( + + [ The cabinet hums quietly. Press Back to leave. ] + + )} + {/* Input line */} + {state.inputRequest && !state.filePrompt && ( + + > + setEntry(e.target.value)} + onKeyDown={keyDown} + variant="standard" + autoFocus + fullWidth + InputProps={{ disableUnderline: true, sx: { color: Settings.theme.primary, ...MONO } }} + inputProps={{ maxLength: state.inputRequest.maxlen, "aria-label": "Zork command input" }} + /> + + )} + {/* Save/restore slot prompt */} + {state.filePrompt && ( + + + {state.filePrompt.filemode === "read" ? "Restore from slot:" : "Save to slot:"} + + setSlotName(e.target.value)} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === "Enter" && slotName.trim()) { + onFileref({ filename: slotName.trim(), usage: state.filePrompt?.filetype ?? "save" }); + setSlotName(""); + } + }} + variant="standard" + autoFocus + placeholder="slot name" + InputProps={{ sx: { color: Settings.theme.primary, ...MONO } }} + /> + + + )} + + + ); +} diff --git a/test/Arcade/Zork/ZorkTerminal.test.tsx b/test/Arcade/Zork/ZorkTerminal.test.tsx new file mode 100644 index 0000000000..e0750e99cb --- /dev/null +++ b/test/Arcade/Zork/ZorkTerminal.test.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { act, Simulate } from "react-dom/test-utils"; +import { ZorkTerminal } from "../../../src/Arcade/Zork/ZorkTerminal"; +import type { TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; + +const baseState: TerminalState = { + gridLines: [[{ style: "normal", text: " West of House Score: 0 Moves: 0" }]], + bufferLines: [ + [{ style: "normal", text: "You are standing in an open field west of a white house." }], + [{ style: "normal", text: "There is a small mailbox here." }], + ], + inputRequest: { id: 2, gen: 1, type: "line", maxlen: 200, initial: "" }, + filePrompt: null, + disabled: false, + error: null, +}; + +describe("ZorkTerminal", () => { + let container: HTMLDivElement; + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + afterEach(() => { + ReactDOM.unmountComponentAtNode(container); + container.remove(); + }); + + it("renders status line, story text, and an input", () => { + act(() => { + ReactDOM.render( + , + container, + ); + }); + expect(container.textContent).toContain("West of House"); + expect(container.textContent).toContain("small mailbox"); + expect(container.querySelector("input")).not.toBeNull(); + }); + + it("submits a line on Enter", () => { + const onLine = jest.fn(); + act(() => { + ReactDOM.render( + , + container, + ); + }); + const input = container.querySelector("input") as HTMLInputElement; + act(() => { + Simulate.change(input, { target: { value: "open mailbox" } as unknown as EventTarget & HTMLInputElement }); + }); + act(() => { + Simulate.keyDown(input, { key: "Enter" }); + }); + expect(onLine).toHaveBeenCalledWith("open mailbox"); + }); + + it("shows the save-slot prompt when filePrompt is set", () => { + const state: TerminalState = { ...baseState, inputRequest: null, filePrompt: { filemode: "write", filetype: "save" } }; + act(() => { + ReactDOM.render( + , + container, + ); + }); + expect(container.textContent).toMatch(/slot|file|name/i); + }); +}); From b9fa24917f7ddaf9447d797f67547e288aeeb76a Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:31:33 -0700 Subject: [PATCH 08/12] Arcade: Zork 1-3 cabinets in the New Tokyo arcade menu ZorkRoot loads the bundled story file and runs a ZVM session; ArcadeRoot gains three Zork buttons. Vendor typing moved to sibling .d.ts files so strict type-aware lint sees the real contracts. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/ZorkRoot.tsx | 61 ++++++++++++++++++++++++++ src/Arcade/Zork/ZorkTerminal.tsx | 6 ++- src/Arcade/Zork/vendor/glkapi.d.ts | 9 ++++ src/Arcade/Zork/vendor/vendor.d.ts | 15 ------- src/Arcade/Zork/vendor/zvm.d.ts | 17 +++++++ src/Arcade/ui/ArcadeRoot.tsx | 20 +++++++++ test/Arcade/Zork/GlkOteReact.test.ts | 6 ++- test/Arcade/Zork/ZorkTerminal.test.tsx | 6 ++- 8 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 src/Arcade/Zork/ZorkRoot.tsx create mode 100644 src/Arcade/Zork/vendor/glkapi.d.ts delete mode 100644 src/Arcade/Zork/vendor/vendor.d.ts create mode 100644 src/Arcade/Zork/vendor/zvm.d.ts diff --git a/src/Arcade/Zork/ZorkRoot.tsx b/src/Arcade/Zork/ZorkRoot.tsx new file mode 100644 index 0000000000..916bb8f9b5 --- /dev/null +++ b/src/Arcade/Zork/ZorkRoot.tsx @@ -0,0 +1,61 @@ +/** + * A Zork arcade cabinet: loads the bundled story file, runs a ZVM session, + * and renders the terminal. ArcadeRoot supplies the Back button. + */ +import React, { useEffect, useRef, useState } from "react"; +import Typography from "@mui/material/Typography"; +import { AlertEvents } from "../../ui/React/AlertManager"; +import { Settings } from "../../Settings/Settings"; +import { GlkOteReact, TerminalState } from "./GlkOteReact"; +import { ZorkTerminal } from "./ZorkTerminal"; +import { createZorkSession, ZorkSession } from "./session"; +import type { ZorkGame } from "./metadata"; + +export function ZorkRoot({ game }: { game: ZorkGame }): React.ReactElement { + const [state, setState] = useState(null); + const sessionRef = useRef(null); + + useEffect(() => { + let cancelled = false; + fetch(game.url) + .then((response) => { + if (!response.ok) throw new Error(`Failed to load ${game.title}: ${response.status}`); + return response.arrayBuffer(); + }) + .then((buffer) => { + if (cancelled) return; + sessionRef.current = createZorkSession(new Uint8Array(buffer), game.key, setState); + }) + .catch((error) => { + console.error(error); + if (!cancelled) AlertEvents.emit("This machine is broken."); + }); + return () => { + cancelled = true; + sessionRef.current?.dispose(); + sessionRef.current = null; + }; + }, [game]); + + if (!state) { + return The cabinet flickers to life...; + } + const glkote = (): GlkOteReact | undefined => sessionRef.current?.glkote; + return ( + <> + + {game.title} + + glkote()?.sendLine(text)} + onChar={(key) => glkote()?.sendChar(key)} + onFileref={(value) => glkote()?.sendFileref(value)} + /> + + Type "save" / "restore" to keep your progress. It is pitch black. You are likely to be eaten + by a grue. + + + ); +} diff --git a/src/Arcade/Zork/ZorkTerminal.tsx b/src/Arcade/Zork/ZorkTerminal.tsx index 715662f7cd..f7db601a92 100644 --- a/src/Arcade/Zork/ZorkTerminal.tsx +++ b/src/Arcade/Zork/ZorkTerminal.tsx @@ -92,7 +92,11 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP return ( {/* Status line: classic inverse-video Infocom bar */} diff --git a/src/Arcade/Zork/vendor/glkapi.d.ts b/src/Arcade/Zork/vendor/glkapi.d.ts new file mode 100644 index 0000000000..a975c94370 --- /dev/null +++ b/src/Arcade/Zork/vendor/glkapi.d.ts @@ -0,0 +1,9 @@ +// Type declarations for the verbatim vendored glkapi.js (GlkOte 2.3.7, MIT). +// Describes only the API surface our glue code uses. See LICENSE.md and +// README.md in src/Arcade/Zork. +export interface GlkInstance { + init(options: object): void; +} + +export const Glk: GlkInstance; +export const GlkClass: new () => GlkInstance; diff --git a/src/Arcade/Zork/vendor/vendor.d.ts b/src/Arcade/Zork/vendor/vendor.d.ts deleted file mode 100644 index f01f0e8da1..0000000000 --- a/src/Arcade/Zork/vendor/vendor.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Type declarations for the verbatim vendored MIT-licensed interpreter files. -// See src/Arcade/Zork/LICENSE.md and README.md. - -declare module "*/vendor/zvm.js" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ZVM: new () => any; - export default ZVM; -} - -declare module "*/vendor/glkapi.js" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export const Glk: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export const GlkClass: new () => any; -} diff --git a/src/Arcade/Zork/vendor/zvm.d.ts b/src/Arcade/Zork/vendor/zvm.d.ts new file mode 100644 index 0000000000..79581f8f01 --- /dev/null +++ b/src/Arcade/Zork/vendor/zvm.d.ts @@ -0,0 +1,17 @@ +// Type declarations for the verbatim vendored zvm.js (ifvms 1.1.6, MIT). +// Describes only the API surface our glue code uses; wiring verified against +// ifvms 1.1.6 bin/zvm.js. See LICENSE.md and README.md in src/Arcade/Zork. +declare class ZVM { + prepare(storydata: Uint8Array, options: ZVM.Options): void; +} + +declare namespace ZVM { + interface Options { + vm: ZVM; + Glk: unknown; + GlkOte: unknown; + Dialog: unknown; + } +} + +export = ZVM; diff --git a/src/Arcade/ui/ArcadeRoot.tsx b/src/Arcade/ui/ArcadeRoot.tsx index 81193a48e8..0843fbb26b 100644 --- a/src/Arcade/ui/ArcadeRoot.tsx +++ b/src/Arcade/ui/ArcadeRoot.tsx @@ -4,14 +4,18 @@ import { BBCabinetRoot } from "./BBCabinet"; import Button from "@mui/material/Button"; import { Player } from "@player"; import { AlertEvents } from "../../ui/React/AlertManager"; +import { ZorkRoot } from "../Zork/ZorkRoot"; +import { ZorkGames, ZorkGame } from "../Zork/metadata"; enum Page { None, Megabyteburner2000, + Zork, } export function ArcadeRoot(): React.ReactElement { const [page, setPage] = useState(Page.None); + const [zorkGame, setZorkGame] = useState(null); function mbBurner2000(): void { if (Player.activeSourceFileLvl(1) === 0) { @@ -21,10 +25,22 @@ export function ArcadeRoot(): React.ReactElement { } } + function playZork(game: ZorkGame): void { + setZorkGame(game); + setPage(Page.Zork); + } + if (page === Page.None) { return ( <> +
+ {ZorkGames.map((game) => ( + + +
+
+ ))} ); } @@ -32,6 +48,10 @@ export function ArcadeRoot(): React.ReactElement { switch (page) { case Page.Megabyteburner2000: currentGame = ; + break; + case Page.Zork: + if (zorkGame) currentGame = ; + break; } return ( <> diff --git a/test/Arcade/Zork/GlkOteReact.test.ts b/test/Arcade/Zork/GlkOteReact.test.ts index 939a8e011e..339aa0ba9f 100644 --- a/test/Arcade/Zork/GlkOteReact.test.ts +++ b/test/Arcade/Zork/GlkOteReact.test.ts @@ -72,7 +72,11 @@ describe("GlkOteReact", () => { const frEv = accepted[accepted.length - 1] as { type: string; response: string }; expect(frEv).toMatchObject({ type: "specialresponse", response: "fileref_prompt" }); - glkote.update({ type: "update", gen: 3, content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }] }); + glkote.update({ + type: "update", + gen: 3, + content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }], + }); const s = states[states.length - 1]; expect(s.bufferLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toBe("fresh"); }); diff --git a/test/Arcade/Zork/ZorkTerminal.test.tsx b/test/Arcade/Zork/ZorkTerminal.test.tsx index e0750e99cb..530ba1032b 100644 --- a/test/Arcade/Zork/ZorkTerminal.test.tsx +++ b/test/Arcade/Zork/ZorkTerminal.test.tsx @@ -58,7 +58,11 @@ describe("ZorkTerminal", () => { }); it("shows the save-slot prompt when filePrompt is set", () => { - const state: TerminalState = { ...baseState, inputRequest: null, filePrompt: { filemode: "write", filetype: "save" } }; + const state: TerminalState = { + ...baseState, + inputRequest: null, + filePrompt: { filemode: "write", filetype: "save" }, + }; act(() => { ReactDOM.render( , From 43da644265a67a2c0b44e238e692d6c09c6af121 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 21:53:31 -0700 Subject: [PATCH 09/12] Arcade: Zork input continues the game prompt line Live verification showed a doubled ">" (game echoes its own prompt into the buffer) and a themed gray input background; input now renders inline with the game prompt and transparent. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/ZorkTerminal.tsx | 36 +++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Arcade/Zork/ZorkTerminal.tsx b/src/Arcade/Zork/ZorkTerminal.tsx index f7db601a92..7c53bca174 100644 --- a/src/Arcade/Zork/ZorkTerminal.tsx +++ b/src/Arcade/Zork/ZorkTerminal.tsx @@ -56,6 +56,18 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP inputRef.current?.focus(); }, [state]); + // The game's own prompt (e.g. "> ") is the last buffer line while line input + // is pending; render the input as a continuation of it instead of doubling it. + const lastLine = state.bufferLines[state.bufferLines.length - 1]; + const inlineInput = + !!state.inputRequest && + !state.filePrompt && + !!lastLine && + lastLine + .map((run) => run.text) + .join("") + .trim().length <= 2; + function keyDown(event: React.KeyboardEvent): void { // Keep keystrokes inside the cabinet: no Bitburner hotkeys mid-game. event.stopPropagation(); @@ -106,9 +118,10 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP
)} - {/* Story buffer */} + {/* Story buffer. The game prints its own "> " prompt as the final buffer + line, so when line input is wanted the input field continues that line. */} - {state.bufferLines.map((runs, i) => ( + {(inlineInput ? state.bufferLines.slice(0, -1) : state.bufferLines).map((runs, i) => ( ))} {state.error && [ {state.error} ]} @@ -120,7 +133,13 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP {/* Input line */} {state.inputRequest && !state.filePrompt && ( - > + {inlineInput ? ( + + {state.bufferLines[state.bufferLines.length - 1].map((run) => run.text).join("")} + + ) : ( + > + )} )} From 2a178f5b342c415906873d0c495eb164b0f12bf8 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 22:17:38 -0700 Subject: [PATCH 10/12] Remove internal design docs from the branch Spec and plan were working artifacts; they remain in git history. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-07-zork-arcade.md | 1424 ----------------- .../specs/2026-07-07-zork-arcade-design.md | 171 -- src/Arcade/Zork/README.md | 2 - 3 files changed, 1597 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-07-zork-arcade.md delete mode 100644 docs/superpowers/specs/2026-07-07-zork-arcade-design.md diff --git a/docs/superpowers/plans/2026-07-07-zork-arcade.md b/docs/superpowers/plans/2026-07-07-zork-arcade.md deleted file mode 100644 index a6395e41e5..0000000000 --- a/docs/superpowers/plans/2026-07-07-zork-arcade.md +++ /dev/null @@ -1,1424 +0,0 @@ -# Zork 1–3 Arcade Integration Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Zork I, II, and III playable from the New Tokyo Arcade menu via the Parchment project's pure-JS ZVM interpreter, in a Bitburner-themed React terminal. - -**Architecture:** Vendored `zvm.js` (ifvms 1.1.6) makes `glk_*` calls into vendored `glkapi.js` (GlkOte 2.3.7); we implement the GlkOte display interface (`GlkOteReact`) as a state parser that feeds a React terminal component, and a minimal Dialog (`ZorkDialog`) that persists saves to localStorage. `ArcadeRoot.tsx` gets three new menu entries. Spec: `docs/superpowers/specs/2026-07-07-zork-arcade-design.md`. - -**Tech Stack:** React 17, TypeScript 5.9, MUI 5, webpack 5, jest 29 (jsdom). No new npm dependencies. - -## Global Constraints - -- No new entries in `package.json` dependencies or devDependencies. -- Vendored files (`src/Arcade/Zork/vendor/zvm.js`, `src/Arcade/Zork/vendor/glkapi.js`) are kept **verbatim** — never lint, format, or hand-edit them. -- All MIT-licensed material lives under `src/Arcade/Zork/`; `LICENSE.md` there scopes MIT to that folder only. -- All UI colors come from `Settings.theme` (import: `import { Settings } from "../../Settings/Settings";`) — no hardcoded colors except black text on the inverse status bar. -- No gating, no rewards: cabinets always playable. -- Existing commands must stay green: `npm run lint:report`, `npm run format:report`, `npm run test`. -- Commit after every task (no `--no-verify`). - -## Reference: verified third-party APIs - -Facts verified by reading the packages (do not re-derive): - -- **ZVM** (`ifvms@1.1.6`, `dist/zvm.js`, CommonJS export of a constructor): - `const vm = new ZVM(); vm.prepare(storyBytes, options); Glk.init(options)` where - `options = { vm, Glk, GlkOte, Dialog }`. `Glk.init` sets `options.accept` and calls - `GlkOte.init(options)`; the display then sends `{type:"init", gen:0, metrics}` through - `accept`, which starts the VM. Everything runs synchronously until the VM blocks on - `glk_select`, at which point `GlkOte.update(state)` is called. -- **glkapi.js** (GlkOte 2.3.7): exports `{ Glk, GlkClass }` (CommonJS, `new GlkClass()` - per session). Calls on the display object: `init(iface)`, `update(state)`, `log(msg)`, - `warning(msg)`, `error(msg)`, `getlibrary(name)` (asked for `"Dialog"` and `"Blorb"`), - `save_allstate()`. Calls on Dialog (non-streaming, `Dialog.streaming === false`): - `file_construct_ref(filename, usage, gameid)`, `file_construct_temp_ref(usage)`, - `file_ref_exists(ref)`, `file_remove_ref(ref)`, `file_write(ref, content, israw)`, - `file_read(ref, israw)`, `file_clean_fixed_name(filename, usage)`. Save/restore - prompts arrive at the display as `update.specialinput = -{type:"fileref_prompt", filemode, filetype, gameid}`; the display answers - `accept({type:"specialresponse", gen, response:"fileref_prompt", value})` where - `value` is `{filename, usage}` or `null` for cancel. -- **GlkOte update payload:** `{type:"update", gen, windows?, content?, input?, specialinput?, disable?}`. - `windows`: `[{id, type:"grid"|"buffer", rock, gridwidth?, gridheight?}]`. - Grid content: `{id, lines:[{line, content}]}`. Buffer content: - `{id, clear?, text:[{append?, flowbreak?, content?}]}`. A `content` array holds either - `{style, text}` objects or alternating `"style", "text"` strings. `input`: - `[{id, gen, type:"line"|"char", maxlen?, initial?}]`. -- Zork 1–3 are Z-machine v3; ZVM itself draws the status line into a grid window. - ---- - -### Task 1: Vendor drop, game files, licensing, toolchain config - -**Files:** - -- Create: `src/Arcade/Zork/vendor/zvm.js` (verbatim from ifvms 1.1.6) -- Create: `src/Arcade/Zork/vendor/glkapi.js` (verbatim from GlkOte 2.3.7) -- Create: `src/Arcade/Zork/vendor/vendor.d.ts` -- Create: `src/Arcade/Zork/games/zork1.z3`, `zork2.z3`, `zork3.z3` (from `C:\git\Zork`) -- Create: `src/Arcade/Zork/LICENSE.md`, `src/Arcade/Zork/README.md` -- Modify: `webpack.config.js` (asset rule, line ~148) -- Modify: `src/@types/global.d.ts` (add `*.z3` module) -- Modify: `jest.config.js` (moduleNameMapper for `.z3`) -- Modify: `.prettierignore` and ESLint ignore config (create/extend) - -**Interfaces:** - -- Produces: `require("src/Arcade/Zork/vendor/zvm.js")` → ZVM constructor; - `require("src/Arcade/Zork/vendor/glkapi.js")` → `{ Glk, GlkClass }`; - importable `*.z3` asset URLs. - -- [ ] **Step 1: Fetch vendor sources and copy game files** - -```bash -mkdir -p /tmp/zorkvendor && cd /tmp/zorkvendor -curl -sL https://registry.npmjs.org/ifvms/-/ifvms-1.1.6.tgz -o ifvms.tgz && tar -xzf ifvms.tgz -git clone --depth 1 https://github.com/erkyrath/glkote glkote -mkdir -p /c/git/bitburner-zork/src/Arcade/Zork/vendor /c/git/bitburner-zork/src/Arcade/Zork/games -cp package/dist/zvm.js /c/git/bitburner-zork/src/Arcade/Zork/vendor/zvm.js -cp glkote/glkapi.js /c/git/bitburner-zork/src/Arcade/Zork/vendor/glkapi.js -cp /c/git/Zork/zork1.z3 /c/git/Zork/zork2.z3 /c/git/Zork/zork3.z3 /c/git/bitburner-zork/src/Arcade/Zork/games/ -``` - -Verify: `head -10` of both vendor files shows their original MIT headers; `ls -l src/Arcade/Zork/games` shows three files ~87–93KB. - -- [ ] **Step 2: Fetch the exact Zork MIT license text** - -```bash -curl -sL https://raw.githubusercontent.com/historicalsource/zork1/master/LICENSE -o /tmp/zorkvendor/zork-license.txt -head -5 /tmp/zorkvendor/zork-license.txt -``` - -Expected: MIT license text with a Microsoft (or Infocom successor) copyright line. **Use this exact text in LICENSE.md below — do not paraphrase or invent the copyright line.** If the fetch fails, STOP and ask the user for the license text that shipped with their Zork copies. - -- [ ] **Step 3: Write `src/Arcade/Zork/LICENSE.md`** - -Structure (fill the license bodies with the exact texts gathered in steps 1–2; ifvms license is in `/tmp/zorkvendor/package/LICENSE`, GlkOte's in `/tmp/zorkvendor/glkote/LICENSE`): - -```markdown -# Licenses for src/Arcade/Zork/ - -Everything in this folder (and only this folder) is MIT-licensed third-party -material plus Bitburner glue code. The rest of Bitburner is licensed separately — -see `license.txt` in the repository root. - -## ZVM interpreter — `vendor/zvm.js` - -From ifvms.js 1.1.6 (https://github.com/curiousdannii/ifvms.js), part of the -Parchment project. - -[exact MIT text, Copyright (c) 2011-2017 Dannii Willis and other contributors] - -## Glk API layer — `vendor/glkapi.js` - -From GlkOte 2.3.7 (https://github.com/erkyrath/glkote). - -[exact MIT text, Copyright (c) 2008-2025, Andrew Plotkin] - -## Zork I, II, III — `games/*.z3` - -Compiled Z-machine story files, open-sourced under MIT -(https://github.com/historicalsource/zork1 et al.). - -[exact MIT text from step 2] -``` - -- [ ] **Step 4: Write `src/Arcade/Zork/README.md`** - -```markdown -# Zork in the Arcade - -Zork I–III cabinets for the New Tokyo Arcade, powered by the Parchment -project's pure-JS Z-machine (ZVM). - -- `vendor/zvm.js` — ifvms.js 1.1.6 `dist/zvm.js`, verbatim. Do not edit/format. -- `vendor/glkapi.js` — GlkOte 2.3.7 `glkapi.js`, verbatim. Do not edit/format. -- `games/*.z3` — compiled Zork story files (MIT, Microsoft open-source release). -- Everything else — Bitburner glue: React terminal, GlkOte display adapter, - localStorage save Dialog. - -Licensing: see LICENSE.md (scoped to this folder only). -To update vendor files, re-fetch from upstream and update versions here. -Design spec: docs/superpowers/specs/2026-07-07-zork-arcade-design.md -``` - -- [ ] **Step 5: Toolchain config** - -`webpack.config.js` — extend the existing asset rule: - -```js -{ test: /\.(ttf|woff2|png|jpe?g|gif|jp2|webp|svg|z3)$/, type: "asset/resource" }, -``` - -`src/@types/global.d.ts` — append: - -```ts -declare module "*.z3" { - const url: string; - export default url; -} -``` - -`jest.config.js` — add to `moduleNameMapper`: - -```js -"\\.z3$": "/test/__mocks__/fileMock.js", -``` - -`.prettierignore` (create if missing) — add: - -``` -src/Arcade/Zork/vendor/ -src/Arcade/Zork/games/ -``` - -ESLint: check for `.eslintignore` or `ignorePatterns` in the ESLint config; add `src/Arcade/Zork/vendor/` the same way the repo already ignores things (if nothing is ignored yet, add `ignorePatterns: ["src/Arcade/Zork/vendor/"]` to the root ESLint config). - -- [ ] **Step 6: `src/Arcade/Zork/vendor/vendor.d.ts`** - -```ts -// Type declarations for the verbatim vendored MIT-licensed interpreter files. -declare module "*/vendor/zvm.js" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ZVM: new () => any; - export = ZVM; -} -declare module "*/vendor/glkapi.js" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export const Glk: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export const GlkClass: new () => any; -} -``` - -- [ ] **Step 7: Verify toolchain is green** - -Run: `npm run lint:report && npm run format:report && npm run test` -Expected: all pass; vendor files not flagged. If prettier flags LICENSE.md/README.md, run `npm run format`. - -- [ ] **Step 8: Commit** - -```bash -git add src/Arcade/Zork webpack.config.js src/@types/global.d.ts jest.config.js .prettierignore -git commit -m "Arcade: vendor ZVM + glkapi and Zork 1-3 story files (MIT, scoped LICENSE.md)" -``` - ---- - -### Task 2: ZorkDialog — localStorage-backed Glk file layer (TDD) - -**Files:** - -- Create: `src/Arcade/Zork/ZorkDialog.ts` -- Test: `test/Arcade/Zork/ZorkDialog.test.ts` - -**Interfaces:** - -- Produces: `class ZorkDialog { constructor(gameKey: string) }` implementing the - non-streaming Dialog contract listed in the Reference section. `FileRef` type: - `{ filename: string; usage: string; gameid?: string }`. Storage key format: - `zork...`. - -- [ ] **Step 1: Write the failing test** - -```ts -import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; - -describe("ZorkDialog", () => { - beforeEach(() => localStorage.clear()); - - it("round-trips binary save data through localStorage", () => { - const dialog = new ZorkDialog("zork1"); - const ref = dialog.file_construct_ref("slot1", "save", ""); - expect(dialog.file_ref_exists(ref)).toBe(false); - const bytes = [0, 1, 2, 127, 128, 255, 66]; - expect(dialog.file_write(ref, bytes, true)).toBe(true); - expect(dialog.file_ref_exists(ref)).toBe(true); - expect(dialog.file_read(ref, true)).toEqual(bytes); - expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); - }); - - it("keeps games separate and removes refs", () => { - const d1 = new ZorkDialog("zork1"); - const d2 = new ZorkDialog("zork2"); - const ref = d1.file_construct_ref("slot1", "save", ""); - d1.file_write(ref, [1], true); - expect(d2.file_ref_exists(d2.file_construct_ref("slot1", "save", ""))).toBe(false); - d1.file_remove_ref(ref); - expect(d1.file_ref_exists(ref)).toBe(false); - expect(d1.file_read(ref, true)).toBeNull(); - }); - - it("cleans fixed names and constructs temp refs", () => { - const d = new ZorkDialog("zork1"); - expect(d.file_clean_fixed_name('a/b\\c:d"e', "save")).toBe("abcde"); - const temp = d.file_construct_temp_ref("data"); - expect(temp.filename.startsWith("_temp")).toBe(true); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `npx jest test/Arcade/Zork/ZorkDialog.test.ts` -Expected: FAIL — cannot find module ZorkDialog. - -- [ ] **Step 3: Implement `src/Arcade/Zork/ZorkDialog.ts`** - -```ts -/** - * Minimal non-streaming Glk Dialog implementation backed by localStorage. - * Implements the subset of the GlkOte Dialog contract that glkapi.js uses, - * persisting Zork save files per game. See src/Arcade/Zork/README.md. - */ -export interface ZorkFileRef { - filename: string; - usage: string; - gameid?: string; -} - -export class ZorkDialog { - /** glkapi checks this to pick the non-streaming file API. */ - readonly streaming = false; - private readonly gameKey: string; - - constructor(gameKey: string) { - this.gameKey = gameKey; - } - - private key(ref: ZorkFileRef): string { - return `zork.${this.gameKey}.${ref.usage}.${ref.filename}`; - } - - file_construct_ref(filename = "", usage = "", gameid = ""): ZorkFileRef { - return { filename, usage, gameid }; - } - - file_construct_temp_ref(usage = ""): ZorkFileRef { - return { filename: `_temp_${usage}`, usage, gameid: "" }; - } - - file_clean_fixed_name(filename: string, _usage: string): string { - return filename.replace(/["/\\<>:|?*]/g, ""); - } - - file_ref_exists(ref: ZorkFileRef): boolean { - return localStorage.getItem(this.key(ref)) !== null; - } - - file_remove_ref(ref: ZorkFileRef): void { - localStorage.removeItem(this.key(ref)); - } - - /** content is an array of byte values (israw) or a string. Returns success. */ - file_write(ref: ZorkFileRef, content: number[] | string, _israw?: boolean): boolean { - try { - const payload = - typeof content === "string" ? { text: content } : { bytes: btoa(String.fromCharCode(...content)) }; - localStorage.setItem(this.key(ref), JSON.stringify(payload)); - return true; - } catch (e) { - console.error("ZorkDialog.file_write failed", e); - return false; - } - } - - file_read(ref: ZorkFileRef, _israw?: boolean): number[] | string | null { - const raw = localStorage.getItem(this.key(ref)); - if (raw === null) return null; - try { - const payload = JSON.parse(raw) as { text?: string; bytes?: string }; - if (typeof payload.text === "string") return payload.text; - if (typeof payload.bytes === "string") { - return Array.from(atob(payload.bytes), (c) => c.charCodeAt(0)); - } - return null; - } catch { - return null; // corrupt entry: treat as missing (spec: error handling) - } - } - - /** ZVM autosave hooks; unused (we don't enable do_vm_autosave), kept safe. */ - autosave_read(_signature: string): null { - return null; - } - autosave_write(_signature: string, _snapshot: unknown): void { - // no-op - } -} -``` - -Note: `String.fromCharCode(...content)` is fine for save-sized arrays (a few KB); if implementation review worries about arg limits, chunk it — but do not prematurely complicate. - -- [ ] **Step 4: Run test to verify it passes** - -Run: `npx jest test/Arcade/Zork/ZorkDialog.test.ts` -Expected: PASS (3 tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/Arcade/Zork/ZorkDialog.ts test/Arcade/Zork/ZorkDialog.test.ts -git commit -m "Arcade: Zork save-file Dialog layer backed by localStorage" -``` - ---- - -### Task 3: GlkOteReact — display adapter / state parser (TDD) - -**Files:** - -- Create: `src/Arcade/Zork/GlkOteReact.ts` -- Test: `test/Arcade/Zork/GlkOteReact.test.ts` - -**Interfaces:** - -- Consumes: `ZorkDialog` from Task 2. -- Produces: - -```ts -export interface StyledRun { - style: string; - text: string; -} -export interface TerminalState { - gridLines: StyledRun[][]; // status-line rows - bufferLines: StyledRun[][]; // scrollback; one entry per display line - inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; - filePrompt: { filemode: string; filetype: string } | null; - disabled: boolean; // true after glk_exit / while no input - error: string | null; -} -export type TerminalListener = (state: TerminalState) => void; -export class GlkOteReact { - constructor(dialog: ZorkDialog, listener: TerminalListener); - // GlkOte contract (called by glkapi): init, update, log, warning, error, - // getlibrary, getinterface, save_allstate - // Outgoing (called by UI / session): - sendLine(text: string): void; - sendChar(key: string): void; - sendFileref(value: { filename: string; usage: string } | null): void; - detach(): void; -} -``` - -- [ ] **Step 1: Write the failing test** - -```ts -import { GlkOteReact, TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; -import { ZorkDialog } from "../../../src/Arcade/Zork/ZorkDialog"; - -function makeGlkOte(): { glkote: GlkOteReact; states: TerminalState[]; accepted: unknown[] } { - const states: TerminalState[] = []; - const accepted: unknown[] = []; - const glkote = new GlkOteReact(new ZorkDialog("test"), (s) => states.push(s)); - glkote.init({ accept: (ev: unknown) => accepted.push(ev) }); - return { glkote, states, accepted }; -} - -describe("GlkOteReact", () => { - it("sends an init event with metrics on init()", () => { - const { accepted } = makeGlkOte(); - expect(accepted).toHaveLength(1); - const ev = accepted[0] as { type: string; gen: number; metrics: { width: number } }; - expect(ev.type).toBe("init"); - expect(ev.metrics.width).toBeGreaterThan(0); - }); - - it("parses grid and buffer content updates (both run formats)", () => { - const { glkote, states } = makeGlkOte(); - glkote.update({ - type: "update", - gen: 1, - windows: [ - { id: 1, type: "grid", rock: 202, gridwidth: 80, gridheight: 1 }, - { id: 2, type: "buffer", rock: 201 }, - ], - content: [ - { id: 1, lines: [{ line: 0, content: ["normal", " West of House"] }] }, - { - id: 2, - text: [ - { content: [{ style: "normal", text: "You are standing in an open field" }] }, - { append: true, content: ["normal", " west of a white house."] }, - ], - }, - ], - input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], - }); - const s = states[states.length - 1]; - expect(s.gridLines[0].map((r) => r.text).join("")).toContain("West of House"); - expect(s.bufferLines).toHaveLength(1); - expect(s.bufferLines[0].map((r) => r.text).join("")).toBe( - "You are standing in an open field west of a white house.", - ); - expect(s.inputRequest).toMatchObject({ id: 2, type: "line" }); - }); - - it("clears buffer on clear, surfaces file prompts, and echoes line events back", () => { - const { glkote, states, accepted } = makeGlkOte(); - glkote.update({ - type: "update", - gen: 1, - windows: [{ id: 2, type: "buffer", rock: 201 }], - content: [{ id: 2, text: [{ content: ["normal", "old text"] }] }], - input: [{ id: 2, gen: 1, type: "line", maxlen: 200 }], - }); - glkote.sendLine("save"); - const lineEv = accepted[accepted.length - 1] as { type: string; window: number; value: string }; - expect(lineEv).toMatchObject({ type: "line", window: 2, value: "save" }); - - glkote.update({ - type: "update", - gen: 2, - specialinput: { type: "fileref_prompt", filemode: "write", filetype: "save" }, - }); - expect(states[states.length - 1].filePrompt).toMatchObject({ filemode: "write" }); - - glkote.sendFileref({ filename: "slot1", usage: "save" }); - const frEv = accepted[accepted.length - 1] as { type: string; response: string }; - expect(frEv).toMatchObject({ type: "specialresponse", response: "fileref_prompt" }); - - glkote.update({ - type: "update", - gen: 3, - content: [{ id: 2, clear: true, text: [{ content: ["normal", "fresh"] }] }], - }); - const s = states[states.length - 1]; - expect(s.bufferLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toBe("fresh"); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `npx jest test/Arcade/Zork/GlkOteReact.test.ts` -Expected: FAIL — cannot find module GlkOteReact. - -- [ ] **Step 3: Implement `src/Arcade/Zork/GlkOteReact.ts`** - -```ts -/** - * A GlkOte-protocol display implementation that feeds React state instead of - * manipulating the DOM (replaces glkote.js, which requires jQuery). - * glkapi.js (vendor/) calls the GlkOte contract methods; the terminal UI calls - * the send* methods. Protocol: https://eblong.com/zarf/glk/glkote/docs.html - */ -import type { ZorkDialog } from "./ZorkDialog"; - -export interface StyledRun { - style: string; - text: string; -} - -export interface TerminalState { - gridLines: StyledRun[][]; - bufferLines: StyledRun[][]; - inputRequest: { id: number; gen: number; type: "line" | "char"; maxlen: number; initial: string } | null; - filePrompt: { filemode: string; filetype: string } | null; - disabled: boolean; - error: string | null; -} - -export type TerminalListener = (state: TerminalState) => void; - -type RunOrString = string | { style?: string; text?: string }; -interface WindowUpdate { - id: number; - type: "grid" | "buffer" | "graphics"; - rock: number; - gridwidth?: number; - gridheight?: number; -} -interface ContentUpdate { - id: number; - clear?: boolean; - lines?: { line: number; content?: RunOrString[] }[]; - text?: { append?: boolean; flowbreak?: boolean; content?: RunOrString[] }[]; -} -interface InputUpdate { - id: number; - gen: number; - type: "line" | "char"; - maxlen?: number; - initial?: string; -} -interface GlkOteUpdate { - type: "update" | "pass" | "retry" | "error"; - gen?: number; - windows?: WindowUpdate[]; - content?: ContentUpdate[]; - input?: InputUpdate[]; - specialinput?: { type: string; filemode: string; filetype: string }; - disable?: boolean; - message?: string; -} -interface GlkOteEvent { - type: string; - gen: number; - [key: string]: unknown; -} -interface AcceptInterface { - accept: (event: GlkOteEvent) => void; -} - -// Fixed 80x25 terminal metrics; glkapi derives grid rows/cols from these. -const METRICS = { - width: 720, - height: 450, - outspacingx: 0, - outspacingy: 0, - inspacingx: 0, - inspacingy: 0, - gridcharwidth: 9, - gridcharheight: 18, - gridmarginx: 0, - gridmarginy: 0, - buffercharwidth: 9, - buffercharheight: 18, - buffermarginx: 0, - buffermarginy: 0, -}; - -function parseRuns(content: RunOrString[] | undefined): StyledRun[] { - const runs: StyledRun[] = []; - if (!content) return runs; - let i = 0; - while (i < content.length) { - const item = content[i]; - if (typeof item === "string") { - // Alternating "style", "text" string pairs. - const text = content[i + 1]; - runs.push({ style: item, text: typeof text === "string" ? text : "" }); - i += 2; - } else { - runs.push({ style: item.style ?? "normal", text: item.text ?? "" }); - i += 1; - } - } - return runs; -} - -export class GlkOteReact { - private readonly dialog: ZorkDialog; - private listener: TerminalListener | null; - private iface: AcceptInterface | null = null; - private generation = 0; - private windows = new Map(); - private state: TerminalState = { - gridLines: [], - bufferLines: [], - inputRequest: null, - filePrompt: null, - disabled: false, - error: null, - }; - - constructor(dialog: ZorkDialog, listener: TerminalListener) { - this.dialog = dialog; - this.listener = listener; - } - - /* ---- GlkOte contract, called by vendor/glkapi.js ---- */ - - init(iface: AcceptInterface): void { - this.iface = iface; - iface.accept({ type: "init", gen: 0, metrics: METRICS, support: [] }); - } - - update(arg: GlkOteUpdate): void { - if (arg.type === "error") { - this.state.error = arg.message ?? "Unknown interpreter error"; - this.emit(); - return; - } - if (arg.type !== "update") return; - if (typeof arg.gen === "number") this.generation = arg.gen; - - if (arg.windows) { - this.windows.clear(); - for (const win of arg.windows) this.windows.set(win.id, win); - } - if (arg.content) { - for (const update of arg.content) this.applyContent(update); - } - // Input requests fully replace the previous set each update. - this.state.inputRequest = null; - if (arg.input) { - for (const req of arg.input) { - const win = this.windows.get(req.id); - // Zork only reads from the buffer window; prefer it if several ask. - if (!this.state.inputRequest || win?.type === "buffer") { - this.state.inputRequest = { - id: req.id, - gen: req.gen, - type: req.type, - maxlen: req.maxlen ?? 200, - initial: req.initial ?? "", - }; - } - } - } - this.state.filePrompt = - arg.specialinput?.type === "fileref_prompt" - ? { filemode: arg.specialinput.filemode, filetype: arg.specialinput.filetype } - : null; - this.state.disabled = !!arg.disable || (!this.state.inputRequest && !this.state.filePrompt); - this.emit(); - } - - log(_msg: string): void { - // Interpreter debug output: intentionally dropped. - } - warning(msg: string): void { - console.warn("Zork glkapi warning:", msg); - } - error(msg: string): void { - this.state.error = String(msg); - this.emit(); - } - getlibrary(name: string): unknown { - return name === "Dialog" ? this.dialog : null; - } - getinterface(): AcceptInterface | null { - return this.iface; - } - save_allstate(): undefined { - return undefined; // autosave not enabled - } - - /* ---- Outgoing events, called by the terminal UI / session ---- */ - - sendLine(text: string): void { - const req = this.state.inputRequest; - if (!this.iface || !req || req.type !== "line") return; - this.iface.accept({ type: "line", gen: this.generation, window: req.id, value: text }); - } - - sendChar(key: string): void { - const req = this.state.inputRequest; - if (!this.iface || !req || req.type !== "char") return; - this.iface.accept({ type: "char", gen: this.generation, window: req.id, value: key }); - } - - sendFileref(value: { filename: string; usage: string } | null): void { - if (!this.iface) return; - this.iface.accept({ type: "specialresponse", gen: this.generation, response: "fileref_prompt", value }); - } - - detach(): void { - this.listener = null; - this.iface = null; - } - - /* ---- Internals ---- */ - - private applyContent(update: ContentUpdate): void { - const win = this.windows.get(update.id); - if (!win) return; - if (win.type === "grid") { - const height = win.gridheight ?? 1; - while (this.state.gridLines.length < height) this.state.gridLines.push([]); - this.state.gridLines.length = height; - for (const line of update.lines ?? []) { - if (line.line >= 0 && line.line < height) this.state.gridLines[line.line] = parseRuns(line.content); - } - } else if (win.type === "buffer") { - if (update.clear) this.state.bufferLines = []; - for (const para of update.text ?? []) { - const runs = parseRuns(para.content); - if (para.append && this.state.bufferLines.length > 0) { - this.state.bufferLines[this.state.bufferLines.length - 1].push(...runs); - } else { - this.state.bufferLines.push(runs); - } - } - // Cap scrollback so multi-hour sessions don't grow unbounded. - const MAX_LINES = 2000; - if (this.state.bufferLines.length > MAX_LINES) { - this.state.bufferLines = this.state.bufferLines.slice(-MAX_LINES); - } - } - } - - private emit(): void { - if (!this.listener) return; - this.listener({ - ...this.state, - gridLines: this.state.gridLines.map((l) => [...l]), - bufferLines: this.state.bufferLines.map((l) => [...l]), - }); - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `npx jest test/Arcade/Zork/GlkOteReact.test.ts` -Expected: PASS (3 tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/Arcade/Zork/GlkOteReact.ts test/Arcade/Zork/GlkOteReact.test.ts -git commit -m "Arcade: GlkOte display adapter feeding React state" -``` - ---- - -### Task 4: Engine session + headless Zork smoke test - -**Files:** - -- Create: `src/Arcade/Zork/session.ts` -- Create: `src/Arcade/Zork/metadata.ts` -- Test: `test/Arcade/Zork/engine.test.ts` - -**Interfaces:** - -- Consumes: `GlkOteReact`, `TerminalListener` (Task 3), `ZorkDialog` (Task 2), vendor modules (Task 1). -- Produces: - -```ts -// session.ts -export interface ZorkSession { - glkote: GlkOteReact; - dispose(): void; -} -export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession; -// metadata.ts -export interface ZorkGame { - key: "zork1" | "zork2" | "zork3"; - title: string; - url: string; -} -export const ZorkGames: ZorkGame[]; -``` - -- [ ] **Step 1: Write the failing smoke test** - -This is the money test: real ZVM + real glkapi + our adapter + our dialog, playing actual Zork I with scripted commands, no browser. - -```ts -import * as fs from "fs"; -import * as path from "path"; -import { GlkOteReact, TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; -import { createZorkSession } from "../../../src/Arcade/Zork/session"; - -function textOf(state: TerminalState): string { - return state.bufferLines.map((line) => line.map((r) => r.text).join("")).join("\n"); -} - -describe("Zork I engine smoke test", () => { - beforeEach(() => localStorage.clear()); - - it("boots Zork I, walks the opening, and saves", () => { - const story = new Uint8Array(fs.readFileSync(path.join(__dirname, "../../../src/Arcade/Zork/games/zork1.z3"))); - let state: TerminalState | null = null; - const session = createZorkSession(story, "zork1", (s) => (state = s)); - - // ZVM runs synchronously until the first input request. - expect(state).not.toBeNull(); - let s = state as unknown as TerminalState; - expect(textOf(s)).toContain("West of House"); - expect(textOf(s)).toContain("white house"); - expect(s.inputRequest?.type).toBe("line"); - - session.glkote.sendLine("open mailbox"); - s = state as unknown as TerminalState; - expect(textOf(s)).toContain("leaflet"); - - // status line (grid window) shows the room - expect(s.gridLines.map((l) => l.map((r) => r.text).join("")).join("\n")).toContain("West of House"); - - // save -> fileref prompt -> localStorage entry appears - session.glkote.sendLine("save"); - s = state as unknown as TerminalState; - expect(s.filePrompt?.filemode).toBe("write"); - session.glkote.sendFileref({ filename: "slot1", usage: "save" }); - s = state as unknown as TerminalState; - expect(textOf(s).toLowerCase()).toContain("ok"); - expect(localStorage.getItem("zork.zork1.save.slot1")).not.toBeNull(); - - session.dispose(); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `npx jest test/Arcade/Zork/engine.test.ts` -Expected: FAIL — cannot find module session. - -- [ ] **Step 3: Implement `src/Arcade/Zork/session.ts`** - -```ts -/** - * Wires a fresh ZVM + glkapi instance to our display adapter and dialog. - * Wiring pattern verified against ifvms 1.1.6 bin/zvm.js: - * vm.prepare(story, options); Glk.init(options); // Glk.init calls vm.init() - */ -// eslint-disable-next-line @typescript-eslint/no-var-requires -import ZVM = require("./vendor/zvm.js"); -import { GlkClass } from "./vendor/glkapi.js"; -import { GlkOteReact, TerminalListener } from "./GlkOteReact"; -import { ZorkDialog } from "./ZorkDialog"; - -export interface ZorkSession { - glkote: GlkOteReact; - dispose(): void; -} - -export function createZorkSession(story: Uint8Array, gameKey: string, listener: TerminalListener): ZorkSession { - const dialog = new ZorkDialog(gameKey); - const glkote = new GlkOteReact(dialog, listener); - const vm = new ZVM(); - const Glk = new GlkClass(); - const options = { vm, Glk, GlkOte: glkote, Dialog: dialog }; - vm.prepare(story, options); - Glk.init(options); // synchronously runs the VM to its first input request - return { - glkote, - dispose: () => glkote.detach(), - }; -} -``` - -If `import = require` syntax fights the repo's TS config, use `import ZVM from "./vendor/zvm.js";` and adjust `vendor.d.ts` (`export default ZVM`) — pick whichever compiles cleanly; do not add build config for it. - -- [ ] **Step 4: Implement `src/Arcade/Zork/metadata.ts`** - -```ts -import zork1Url from "./games/zork1.z3"; -import zork2Url from "./games/zork2.z3"; -import zork3Url from "./games/zork3.z3"; - -export interface ZorkGame { - key: "zork1" | "zork2" | "zork3"; - title: string; - url: string; -} - -export const ZorkGames: ZorkGame[] = [ - { key: "zork1", title: "Zork I: The Great Underground Empire", url: zork1Url }, - { key: "zork2", title: "Zork II: The Wizard of Frobozz", url: zork2Url }, - { key: "zork3", title: "Zork III: The Dungeon Master", url: zork3Url }, -]; -``` - -- [ ] **Step 5: Run the smoke test** - -Run: `npx jest test/Arcade/Zork/engine.test.ts` -Expected: PASS. Debugging notes if not: - -- "This is not a Z-Code file" → story bytes mangled; check fs read. -- No update received → check `Glk.init` was passed the same options object given to `vm.prepare`. -- Fileref/save shape mismatch → read the `fileref_prompt` handling in `vendor/glkapi.js` (search `fileref_prompt`) and adjust `sendFileref`'s `value` shape to what `gli_fileref_create_by_prompt_callback` expects; update the Task 3 test to match reality. The state of the art is the vendor file, not this plan. - -- [ ] **Step 6: Full test suite** - -Run: `npm run test` -Expected: all suites pass (new + existing). - -- [ ] **Step 7: Commit** - -```bash -git add src/Arcade/Zork/session.ts src/Arcade/Zork/metadata.ts test/Arcade/Zork/engine.test.ts -git commit -m "Arcade: ZVM session wiring + headless Zork I smoke test" -``` - ---- - -### Task 5: ZorkTerminal — the React terminal UI - -**Files:** - -- Create: `src/Arcade/Zork/ZorkTerminal.tsx` -- Test: `test/Arcade/Zork/ZorkTerminal.test.tsx` - -**Interfaces:** - -- Consumes: `TerminalState`, `StyledRun` (Task 3). -- Produces: - -```tsx -export interface ZorkTerminalProps { - state: TerminalState; - onLine: (text: string) => void; - onChar: (key: string) => void; - onFileref: (value: { filename: string; usage: string } | null) => void; -} -export function ZorkTerminal(props: ZorkTerminalProps): React.ReactElement; -``` - -- [ ] **Step 1: Write the failing render test** - -Match the repo's existing React test style if one exists (check `test/` for `.tsx` tests first and copy the harness idiom). Baseline version: - -```tsx -import React from "react"; -import ReactDOM from "react-dom"; -import { act } from "react-dom/test-utils"; -import { ZorkTerminal } from "../../../src/Arcade/Zork/ZorkTerminal"; -import type { TerminalState } from "../../../src/Arcade/Zork/GlkOteReact"; - -const baseState: TerminalState = { - gridLines: [[{ style: "normal", text: " West of House Score: 0 Moves: 0" }]], - bufferLines: [ - [{ style: "normal", text: "You are standing in an open field west of a white house." }], - [{ style: "normal", text: "There is a small mailbox here." }], - ], - inputRequest: { id: 2, gen: 1, type: "line", maxlen: 200, initial: "" }, - filePrompt: null, - disabled: false, - error: null, -}; - -describe("ZorkTerminal", () => { - let container: HTMLDivElement; - beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); - }); - afterEach(() => { - ReactDOM.unmountComponentAtNode(container); - container.remove(); - }); - - it("renders status line, story text, and a focused input", () => { - act(() => { - ReactDOM.render( - , - container, - ); - }); - expect(container.textContent).toContain("West of House"); - expect(container.textContent).toContain("small mailbox"); - expect(container.querySelector("input")).not.toBeNull(); - }); - - it("submits a line on Enter and clears the field", () => { - const onLine = jest.fn(); - act(() => { - ReactDOM.render( - , - container, - ); - }); - const input = container.querySelector("input") as HTMLInputElement; - act(() => { - input.value = "open mailbox"; - input.dispatchEvent(new Event("input", { bubbles: true })); - input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); - }); - expect(onLine).toHaveBeenCalledWith("open mailbox"); - }); - - it("shows the save-slot prompt when filePrompt is set", () => { - const state = { ...baseState, inputRequest: null, filePrompt: { filemode: "write", filetype: "save" } }; - act(() => { - ReactDOM.render( - , - container, - ); - }); - expect(container.textContent).toMatch(/slot|file|name/i); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `npx jest test/Arcade/Zork/ZorkTerminal.test.tsx` -Expected: FAIL — cannot find module ZorkTerminal. - -- [ ] **Step 3: Implement `src/Arcade/Zork/ZorkTerminal.tsx`** - -```tsx -/** - * Retro terminal renderer for the Zork arcade cabinets. - * Pure presentation: renders TerminalState, forwards input upward. - * Colors come from Settings.theme so player themes apply. - */ -import React, { useEffect, useRef, useState } from "react"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; -import Typography from "@mui/material/Typography"; -import { Settings } from "../../Settings/Settings"; -import type { StyledRun, TerminalState } from "./GlkOteReact"; - -export interface ZorkTerminalProps { - state: TerminalState; - onLine: (text: string) => void; - onChar: (key: string) => void; - onFileref: (value: { filename: string; usage: string } | null) => void; -} - -const MONO = { fontFamily: '"Lucida Console", "Consolas", monospace', fontSize: "14px", lineHeight: "18px" }; - -function runStyle(run: StyledRun): React.CSSProperties { - return { - fontStyle: run.style === "emphasized" ? "italic" : undefined, - fontWeight: run.style === "subheader" || run.style === "header" ? "bold" : undefined, - }; -} - -function Line({ runs }: { runs: StyledRun[] }): React.ReactElement { - return ( -
- {runs.map((run, i) => ( - - {run.text} - - ))} -
- ); -} - -export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalProps): React.ReactElement { - const [entry, setEntry] = useState(""); - const [slotName, setSlotName] = useState(""); - const [history, setHistory] = useState([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const scrollRef = useRef(null); - const inputRef = useRef(null); - - useEffect(() => { - scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight); - inputRef.current?.focus(); - }, [state]); - - function keyDown(event: React.KeyboardEvent): void { - // Keep keystrokes inside the cabinet: no Bitburner hotkeys mid-game. - event.stopPropagation(); - if (state.inputRequest?.type === "char") { - event.preventDefault(); - onChar(event.key.length === 1 ? event.key : event.key.toLowerCase()); - return; - } - if (event.key === "Enter") { - const text = entry; - setEntry(""); - setHistory((h) => (text.trim() ? [...h, text] : h)); - setHistoryIndex(-1); - onLine(text); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - const idx = historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1); - if (history[idx] !== undefined) { - setHistoryIndex(idx); - setEntry(history[idx]); - } - } else if (event.key === "ArrowDown") { - event.preventDefault(); - const idx = historyIndex + 1; - if (idx >= history.length) { - setHistoryIndex(-1); - setEntry(""); - } else { - setHistoryIndex(idx); - setEntry(history[idx]); - } - } - } - - const width = "740px"; - return ( - - {/* Status line: classic inverse-video Infocom bar */} - - {state.gridLines.length > 0 ? ( - state.gridLines.map((runs, i) => ) - ) : ( -
- )} - - {/* Story buffer */} - - {state.bufferLines.map((runs, i) => ( - - ))} - {state.error && [ {state.error} ]} - {state.disabled && !state.error && !state.filePrompt && ( - - [ The cabinet hums quietly. Press Back to leave. ] - - )} - {/* Input line */} - {state.inputRequest && !state.filePrompt && ( - - > - setEntry(e.target.value)} - onKeyDown={keyDown} - variant="standard" - autoFocus - fullWidth - InputProps={{ disableUnderline: true, sx: { color: Settings.theme.primary, ...MONO } }} - inputProps={{ maxLength: state.inputRequest.maxlen, "aria-label": "Zork command input" }} - /> - - )} - {/* Save/restore slot prompt */} - {state.filePrompt && ( - - - {state.filePrompt.filemode === "read" ? "Restore from slot:" : "Save to slot:"} - - setSlotName(e.target.value)} - onKeyDown={(e) => { - e.stopPropagation(); - if (e.key === "Enter" && slotName.trim()) { - onFileref({ filename: slotName.trim(), usage: state.filePrompt?.filetype ?? "save" }); - setSlotName(""); - } - }} - variant="standard" - autoFocus - placeholder="slot name" - InputProps={{ sx: { color: Settings.theme.primary, ...MONO } }} - /> - - - )} - - - ); -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `npx jest test/Arcade/Zork/ZorkTerminal.test.tsx` -Expected: PASS (3 tests). If MUI TextField swallows the synthetic events in jsdom, dispatch on the inner `input` element (querySelector already does) and ensure the `onChange` fires via `Simulate.change` from `react-dom/test-utils` instead — adjust the test, not the component. - -- [ ] **Step 5: Commit** - -```bash -git add src/Arcade/Zork/ZorkTerminal.tsx test/Arcade/Zork/ZorkTerminal.test.tsx -git commit -m "Arcade: Zork terminal UI component" -``` - ---- - -### Task 6: ZorkRoot + arcade menu integration - -**Files:** - -- Create: `src/Arcade/Zork/ZorkRoot.tsx` -- Modify: `src/Arcade/ui/ArcadeRoot.tsx` (all 43 lines shown in current form below) - -**Interfaces:** - -- Consumes: `createZorkSession` (Task 4), `ZorkTerminal` (Task 5), `ZorkGames`/`ZorkGame` (Task 4). -- Produces: `` rendered by ArcadeRoot. - -- [ ] **Step 1: Implement `src/Arcade/Zork/ZorkRoot.tsx`** - -```tsx -/** - * A Zork arcade cabinet: loads the bundled story file, runs a ZVM session, - * and renders the terminal. ArcadeRoot supplies the Back button. - */ -import React, { useEffect, useRef, useState } from "react"; -import Typography from "@mui/material/Typography"; -import { AlertEvents } from "../../ui/React/AlertManager"; -import { Settings } from "../../Settings/Settings"; -import { GlkOteReact, TerminalState } from "./GlkOteReact"; -import { ZorkTerminal } from "./ZorkTerminal"; -import { createZorkSession, ZorkSession } from "./session"; -import type { ZorkGame } from "./metadata"; - -export function ZorkRoot({ game }: { game: ZorkGame }): React.ReactElement { - const [state, setState] = useState(null); - const sessionRef = useRef(null); - - useEffect(() => { - let cancelled = false; - fetch(game.url) - .then((response) => { - if (!response.ok) throw new Error(`Failed to load ${game.title}: ${response.status}`); - return response.arrayBuffer(); - }) - .then((buffer) => { - if (cancelled) return; - sessionRef.current = createZorkSession(new Uint8Array(buffer), game.key, setState); - }) - .catch((error) => { - console.error(error); - if (!cancelled) AlertEvents.emit("This machine is broken."); - }); - return () => { - cancelled = true; - sessionRef.current?.dispose(); - sessionRef.current = null; - }; - }, [game]); - - if (!state) { - return The cabinet flickers to life...; - } - const glkote = (): GlkOteReact | undefined => sessionRef.current?.glkote; - return ( - <> - - {game.title} - - glkote()?.sendLine(text)} - onChar={(key) => glkote()?.sendChar(key)} - onFileref={(value) => glkote()?.sendFileref(value)} - /> - - Type "save" / "restore" to keep your progress. It is pitch black. You are likely to be eaten - by a grue. - - - ); -} -``` - -- [ ] **Step 2: Modify `src/Arcade/ui/ArcadeRoot.tsx`** - -Full new content (current file is 43 lines; this replaces it): - -```tsx -import React, { useState } from "react"; -import { BBCabinetRoot } from "./BBCabinet"; - -import Button from "@mui/material/Button"; -import { Player } from "@player"; -import { AlertEvents } from "../../ui/React/AlertManager"; -import { ZorkRoot } from "../Zork/ZorkRoot"; -import { ZorkGames, ZorkGame } from "../Zork/metadata"; - -enum Page { - None, - Megabyteburner2000, - Zork, -} - -export function ArcadeRoot(): React.ReactElement { - const [page, setPage] = useState(Page.None); - const [zorkGame, setZorkGame] = useState(null); - - function mbBurner2000(): void { - if (Player.activeSourceFileLvl(1) === 0) { - AlertEvents.emit("This machine is broken."); - } else { - setPage(Page.Megabyteburner2000); - } - } - - function playZork(game: ZorkGame): void { - setZorkGame(game); - setPage(Page.Zork); - } - - if (page === Page.None) { - return ( - <> - -
- {ZorkGames.map((game) => ( - - -
-
- ))} - - ); - } - let currentGame = <>; - switch (page) { - case Page.Megabyteburner2000: - currentGame = ; - break; - case Page.Zork: - if (zorkGame) currentGame = ; - break; - } - return ( - <> - - {currentGame} - - ); -} -``` - -- [ ] **Step 3: Typecheck, lint, tests** - -Run: `npx tsc --noEmit -p . && npm run lint:report && npm run test` -Expected: clean. Likely friction point: the `*.z3` import in metadata.ts needs the Task 1 `global.d.ts` declaration — if tsc complains, the declaration file isn't included; check `tsconfig.json` `include` covers `src/@types`. - -- [ ] **Step 4: Commit** - -```bash -git add src/Arcade/Zork/ZorkRoot.tsx src/Arcade/ui/ArcadeRoot.tsx -git commit -m "Arcade: Zork 1-3 cabinets in the New Tokyo arcade menu" -``` - ---- - -### Task 7: Full verification pass (build + manual play) - -**Files:** - -- None created; fixes only if verification finds problems. - -- [ ] **Step 1: Production-grade checks** - -Run: `npm run lint:report && npm run format:report && npm run test` -Expected: all green. - -- [ ] **Step 2: Dev build boots** - -Run: `npm run start:dev` (webpack dev server) in the background; open the served URL. -Expected: game loads with no console errors from the Zork additions. - -- [ ] **Step 3: Manual play verification (use the `verify` skill if available)** - -In the running game: create/load a save, travel to New Tokyo, open the Arcade location. - -- All four buttons visible (Megabyte + 3 Zorks). -- Launch Zork I: opening text renders, status bar shows "West of House". -- `open mailbox`, `read leaflet` — sensible responses; command history via ↑. -- Typing letters matching Bitburner hotkeys does NOT trigger app navigation. -- `save` → slot prompt → save → `restore` round-trips (check localStorage key `zork.zork1.save.` in devtools). -- Reload the page (dev server), reopen Zork I, `restore` — save survived. -- Back button returns to arcade menu; re-entering starts a fresh session. -- Launch Zork II and Zork III: openings render. -- Switch to a non-default theme in options; cabinet colors follow. - -- [ ] **Step 4: Bundle impact sanity check** - -Run: `ls -la dist/assets/*.z3 2>/dev/null || echo "run a build first"` after `npm run build:dev` (or check webpack dev output). -Expected: three ~87–93KB .z3 assets; JS bundle growth ≈ zvm.js + glkapi.js (~330KB pre-minify, less after). - -- [ ] **Step 5: Final commit (fixes from verification, if any)** - -```bash -git add -A && git commit -m "Arcade: Zork verification fixes" -``` - -Only commit if there are changes. - ---- - -## Plan Self-Review Notes - -- Spec coverage: layout ✔ (T1), interpreter stack ✔ (T1/T4), Dialog/saves ✔ (T2), display adapter ✔ (T3), terminal UI + theme + keyboard capture ✔ (T5), arcade menu ✔ (T6), licensing ✔ (T1 steps 2–3), error handling ✔ (T2 corrupt-save guard, T3 error state, T6 fetch/alert), testing ✔ (T2–T5 + T7 manual). -- Known uncertainty flagged inline: exact `fileref_prompt` response shape (T4 step 5 gives the resolution procedure against the vendor source); MUI TextField event simulation in jsdom (T5 step 4). -- The mockups' standalone "Back" button inside the cabinet was dropped: ArcadeRoot already renders Back above every game (existing convention). diff --git a/docs/superpowers/specs/2026-07-07-zork-arcade-design.md b/docs/superpowers/specs/2026-07-07-zork-arcade-design.md deleted file mode 100644 index cd85b40bc9..0000000000 --- a/docs/superpowers/specs/2026-07-07-zork-arcade-design.md +++ /dev/null @@ -1,171 +0,0 @@ -# Zork 1–3 in the New Tokyo Arcade — Design - -**Date:** 2026-07-07 -**Author:** Andrew Faust (with Claude) -**Status:** Approved direction: source mod, targeting upstream acceptance - -## Goal - -Add Zork I, Zork II, and Zork III as playable games in the New Tokyo Arcade. The player -walks to the Arcade, picks a Zork cabinet from the menu, and plays the real game in a -terminal UI that matches Bitburner's theme. The implementation must be clean enough to -submit as a PR to mainline Bitburner. - -## Decisions (settled with the user) - -- **Route:** Source mod in this fork (`bitburner-zork`), not user-mode scripts. -- **Upstream intent:** The work targets acceptance into mainline Bitburner. That drives - every technical choice: no new npm dependencies, no jQuery, no WASM, small bundle - impact, all third-party material isolated in one folder. -- **Licensing:** Bitburner is Apache 2.0 + Commons Clause. All MIT-licensed material - (interpreter code and game files) lives in a single subfolder with its own - `LICENSE.md` scoped to that folder only. -- **Gating/reward:** None. The cabinets are pure flavor — always playable, no unlock, - no in-game reward. -- **Saves:** Zork's `save`/`restore` commands persist to browser storage - (localStorage), keyed per game. Works identically in web and Electron (same - mechanism Bitburner's own saves use). Not part of the Bitburner save file; does not - travel with save export or Steam Cloud. This matches the persistence guarantees - Bitburner's own web save already has. - -## Architecture - -Parchment's modern engine for Z-machine games is Bocfel (a ~500KB WASM blob). We do -not use it. The Parchment project also maintains **ZVM** (from ifvms.js, pure JS, MIT), -which fully supports Z-machine v3 — the format of all three Zork files. Pure JS is the -right call for an upstream PR. - -Verified interpreter stack (all MIT): - -``` -zork1.z3 (Uint8Array, ~87KB each) - → ZVM (ifvms 1.1.6, dist/zvm.js, 104K — the Z-machine) - → glkapi.js (GlkOte 2.3.7, 224K — Glk API layer, zero deps, no DOM) - → GlkOte display (OURS — React component implementing the GlkOte interface) - → Dialog (OURS — minimal file-prompt layer backed by localStorage) -``` - -Key verified facts: - -- ZVM's API (`src/zvm.js` in the ifvms package): `prepare(storydata, {Glk})` then - `init()/start()`. It requires a Glk API object — it makes `glk_*` calls, it does not - speak the GlkOte JSON protocol directly. -- `glkapi.js` implements that Glk API and translates to GlkOte display updates. It has - **no dependencies** — no jQuery, no DOM. jQuery only appears in `glkote.js` (the - stock display layer), which we replace entirely with React. -- Zork v3 needs a small slice of the display protocol: one grid window (the status - line), one buffer window (scrolling story text), and line input. Char input - ("press any key") is also supported for completeness. -- Save/restore flows through `glk_fileref_create_by_prompt` into the Dialog layer, - producing Quetzal-format blobs of a few KB. - -## File layout - -Everything new lives under `src/Arcade/Zork/`: - -``` -src/Arcade/Zork/ - LICENSE.md # MIT licenses: ifvms.js, GlkOte (glkapi.js), Zork I-III - README.md # what this folder is, where the pieces came from, versions - vendor/ - zvm.js # verbatim from ifvms 1.1.6 dist/ (MIT header intact) - glkapi.js # verbatim from GlkOte 2.3.7 (MIT header intact) - games/ - zork1.z3 # compiled game files (MIT, Microsoft open-source release) - zork2.z3 - zork3.z3 - ZorkRoot.tsx # cabinet component: loads a .z3, wires ZVM+Glk+display - ZorkTerminal.tsx # React terminal: status grid, buffer, input line - GlkOteReact.ts # GlkOte-interface implementation that drives React state - ZorkDialog.ts # Dialog implementation; saves to localStorage - metadata.ts # game list: title, filename, per-game save key -``` - -Changed existing files (kept minimal for PR review): - -- `src/Arcade/ui/ArcadeRoot.tsx` — add `Page.Zork1/Zork2/Zork3` enum values, three - menu buttons, three switch cases rendering ``. -- `webpack.config.js` — add `.z3` to the `asset/resource` rule (one-line change) so - game files bundle as hashed assets. -- Vendored files excluded from ESLint/Prettier (config ignore entries) — they are - third-party code kept verbatim. - -## Component design - -**ZorkRoot.tsx** — mounts per cabinet. Fetches the bundled `.z3` asset as an -ArrayBuffer, instantiates ZVM, `prepare()`s it with the Glk object from `glkapi.js`, -provides our GlkOteReact display and ZorkDialog, and starts the machine. Unmount -disposes listeners. Engine errors are caught and surfaced via Bitburner's alert -mechanism with a "cabinet resets" flavor, returning to the arcade menu. - -**GlkOteReact.ts** — implements the GlkOte interface `glkapi.js` expects (`init`, -`update`, `error`, `getinterface`, ...). `update(state)` parses window/content/input -updates into a plain state object and pushes it to React via a subscription. Input -events (line submit, keypress) go back through the `accept` callback. - -**ZorkTerminal.tsx** — renders that state: - -- Status line: inverse-video bar (theme primary as background, black text) showing the - grid window contents (room name, score/moves) — exactly how Infocom terps did it. -- Buffer: scrolling story text, auto-scrolled to bottom, text styles (bold/italic) - honored. -- Input: inline text field at the prompt with a blinking block cursor. Keyboard events - are captured (`stopPropagation`) while the cabinet has focus so Bitburner hotkeys - don't fire mid-game. Up/down arrow recalls command history (session-scoped). -- All colors from `Settings.theme` (primary, background, etc.) so player themes apply. -- A "Back" button (matching Megabyte cabinet convention) leaves the game; in-game - `quit` also returns to the arcade menu. - -**ZorkDialog.ts** — implements the minimal Dialog contract glkapi uses for -save/restore/transcript prompts. Saves serialize to base64 in localStorage under -`zork..`; restore lists/reads them back. Transcript and command-record -streams are declined politely (not supported). - -## Licensing plan - -`src/Arcade/Zork/LICENSE.md` contains, clearly separated: - -1. The MIT license for ifvms.js (The ifvms.js team) — covers `vendor/zvm.js`. -2. The MIT license for GlkOte (Andrew Plotkin) — covers `vendor/glkapi.js`. -3. The MIT license for Zork I/II/III (Microsoft's open-source release) — covers - `games/*.z3`. - -The file opens with a scope statement: the license applies **only** to the contents of -`src/Arcade/Zork/`; the rest of Bitburner remains under its own license (see root -`license.txt`). The folder README records upstream versions/commits so future -maintainers can refresh the vendored files. - -## Error handling - -- Story file fails to load (asset fetch error): alert "This machine is broken." (the - established arcade idiom) and return to menu. -- ZVM runtime fault: catch, log to console, same "machine is broken" alert, return to - menu. No crash may escape into Bitburner's React tree (error boundary around the - cabinet). -- Corrupt save blob on restore: ZVM reports restore failure in-game (native behavior); - we additionally guard JSON/base64 decode and treat failures as "no such save". - -## Testing - -- **Headless engine test (jest):** load `zork1.z3` in ZVM with a scripted GlkOte stub, - feed `open mailbox` / `read leaflet`, assert expected output text appears. This - proves the vendored interpreter + our glue works without a browser. -- **Display state test:** feed recorded GlkOte update payloads into GlkOteReact's - parser, assert the resulting terminal state (status line text, buffer lines). -- **Save round-trip test:** save → localStorage entry exists → restore succeeds. -- Manual: play all three games' openings in web + Electron dev builds; verify theme - switching, keyboard capture, Back button, save/restore across an app restart. - -## Non-goals - -- No achievements, unlocks, stats, or any mechanical coupling to Bitburner systems. -- No support for other Z-machine games or a general IF player (YAGNI — but nothing - prevents it later; the Zork folder boundary keeps that door open). -- No transcript/scripting support, no sound, no graphics (v3 Zork uses none). -- Zork saves are not in the Bitburner save file (revisitable before the PR if - upstream prefers it). - -## Mockups - -See `mockups/zork-arcade-menu.svg` (arcade menu with three new cabinets) and -`mockups/zork-terminal.svg` (Zork I running: status bar, story buffer, prompt). diff --git a/src/Arcade/Zork/README.md b/src/Arcade/Zork/README.md index e29001973a..af74fab7af 100644 --- a/src/Arcade/Zork/README.md +++ b/src/Arcade/Zork/README.md @@ -37,5 +37,3 @@ Re-fetch from upstream and update the versions here and in `LICENSE.md`: - `https://registry.npmjs.org/ifvms/-/ifvms-.tgz` → `package/dist/zvm.js` - `https://github.com/erkyrath/glkote` → `glkapi.js` - -Design spec: `docs/superpowers/specs/2026-07-07-zork-arcade-design.md` From 9c9228cf06cddc1ab29e8f3fc361ed1ff940f8e5 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 22:18:41 -0700 Subject: [PATCH 11/12] Remove design mockups from the branch Working artifacts; recoverable from git history for the PR description. Co-Authored-By: Claude Opus 4.8 (1M context) --- mockups/zork-arcade-menu.svg | 58 ------------------------------- mockups/zork-terminal.svg | 66 ------------------------------------ 2 files changed, 124 deletions(-) delete mode 100644 mockups/zork-arcade-menu.svg delete mode 100644 mockups/zork-terminal.svg diff --git a/mockups/zork-arcade-menu.svg b/mockups/zork-arcade-menu.svg deleted file mode 100644 index 2b3252a2f8..0000000000 --- a/mockups/zork-arcade-menu.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - Hacking - Terminal - Script Editor - Active Scripts - Character - Stats - Factions - Augmentations - World - City - Travel - Job - Help - Documentation - Achievements - Options - - - - - < Return to World - Arcade - New Tokyo - - - - Megabyte burner 2000 - - - - Zork I: The Great Underground Empire - - - Zork II: The Wizard of Frobozz - - - Zork III: The Dungeon Master - - - A dusty row of cabinets hums in the corner. Three of them glow with - an ancient phosphor light, as if they have been running since 1980. - - - - - - ← new buttons added to - ArcadeRoot.tsx menu - - diff --git a/mockups/zork-terminal.svg b/mockups/zork-terminal.svg deleted file mode 100644 index 6f265217f7..0000000000 --- a/mockups/zork-terminal.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - Back - - - *** ZORK I: THE GREAT UNDERGROUND EMPIRE *** - - - - - - - West of House - Score: 0 Moves: 2 - - - - ZORK I: The Great Underground Empire - Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved. - ZORK is a registered trademark of Infocom, Inc. - Revision 88 / Serial number 840726 - - West of House - You are standing in an open field west of a white house, with a - boarded front door. - There is a small mailbox here. - - >open mailbox - Opening the small mailbox reveals a leaflet. - - >read leaflet - (Taken) - "WELCOME TO ZORK! - ZORK is a game of adventure, danger, and low cunning. In it you - will explore some of the most amazing territory ever seen by - mortals. No computer should be without one!" - - - > - - - - - - - Type 'save' to save your progress. Type 'quit' to return to the arcade. - - - - - ← status line: Z-machine - grid window (room, - score, moves) - ← scrolling buffer - window, colors from - Settings.theme - ← inline input, blinking - cursor; keys captured - so hotkeys don't fire - - From 46e9b4899eabd1caba8b297ab94f2a53b7d1f5a6 Mon Sep 17 00:00:00 2001 From: Andrew Faust Date: Tue, 7 Jul 2026 22:28:48 -0700 Subject: [PATCH 12/12] Arcade: Zork terminal shows focus state, click-to-focus Focused: bright border + glow. Unfocused: dimmed with darker border. Clicking anywhere on the terminal focuses the active input, unless the player is selecting text to copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Arcade/Zork/ZorkTerminal.tsx | 21 ++++++++++++++++++++- test/Arcade/Zork/ZorkTerminal.test.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Arcade/Zork/ZorkTerminal.tsx b/src/Arcade/Zork/ZorkTerminal.tsx index 7c53bca174..14f80040a8 100644 --- a/src/Arcade/Zork/ZorkTerminal.tsx +++ b/src/Arcade/Zork/ZorkTerminal.tsx @@ -48,8 +48,16 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP const [slotName, setSlotName] = useState(""); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); + const [focused, setFocused] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); + const slotInputRef = useRef(null); + + function focusGame(): void { + // Don't steal focus while the player is selecting text to copy. + if (window.getSelection()?.toString()) return; + (state.filePrompt ? slotInputRef : inputRef).current?.focus(); + } useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; @@ -104,9 +112,19 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP return ( setFocused(true)} + onBlur={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false); + }} + data-focused={focused} sx={{ width: "740px", - border: `1px solid ${Settings.theme.primary}`, + cursor: "text", + border: `1px solid ${focused ? Settings.theme.primary : Settings.theme.primarydark}`, + boxShadow: focused ? `0 0 8px ${Settings.theme.primary}` : "none", + opacity: focused ? 1 : 0.7, + transition: "opacity 150ms, box-shadow 150ms, border-color 150ms", backgroundColor: Settings.theme.backgroundprimary, }} > @@ -167,6 +185,7 @@ export function ZorkTerminal({ state, onLine, onChar, onFileref }: ZorkTerminalP {state.filePrompt.filemode === "read" ? "Restore from slot:" : "Save to slot:"} setSlotName(e.target.value)} onKeyDown={(e) => { diff --git a/test/Arcade/Zork/ZorkTerminal.test.tsx b/test/Arcade/Zork/ZorkTerminal.test.tsx index 530ba1032b..c439cb9c1a 100644 --- a/test/Arcade/Zork/ZorkTerminal.test.tsx +++ b/test/Arcade/Zork/ZorkTerminal.test.tsx @@ -71,4 +71,28 @@ describe("ZorkTerminal", () => { }); expect(container.textContent).toMatch(/slot|file|name/i); }); + + it("indicates focus state and refocuses the input when the terminal is clicked", () => { + act(() => { + ReactDOM.render( + , + container, + ); + }); + const root = container.querySelector("[data-focused]") as HTMLElement; + const input = container.querySelector("input") as HTMLInputElement; + act(() => { + input.focus(); + }); + expect(root.getAttribute("data-focused")).toBe("true"); + act(() => { + input.blur(); + }); + expect(root.getAttribute("data-focused")).toBe("false"); + act(() => { + root.click(); + }); + expect(document.activeElement).toBe(input); + expect(root.getAttribute("data-focused")).toBe("true"); + }); });