@moonbase-machines/player 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,35 @@
1
1
  # @moonbase-machines/player
2
2
 
3
- The player for Mark Wharton's titles of 1985 to 1995, the one way each is played in a page, in two ways to play:
4
-
5
- - **Original** (`original.js`): the game's own disk, untouched, on Moonbase's Apple IIe or Commodore 64
6
- (`@moonbase-machines/apple2e`, `c64`), on Moonbase's own ROMs, which need nothing of Apple's or Commodore's.
7
- - **Remake** (`remake/main.js`): the game's scenes, lifted from its code and compiled to JavaScript, on an engine:
8
- the remake's own for a title of scenes (`card` or `vector`), or the title's own, a module in its folder. Either
9
- way the player makes the page's part, the stage, the options line and the status; the title brings its engine
10
- and its view.
11
-
12
- What a player saves, a game saved, goes on **the player's disk** (`disk/`): a title's own disk of files in this
13
- browser (IndexedDB), with its own dialogs (`disk/dialogs.js`, in a modal window, `modal.js`). A game saved in one
14
- way loads in the other.
3
+ The player for Mark Wharton's titles of 1985 to 1995, one way in for every title and every way to play it (`play.js`):
4
+
5
+ - **Original** (`way: 'original'`, `engines/original.js`): the game's own disk, untouched, on Moonbase's Apple IIe or
6
+ Commodore 64 (`@moonbase-machines/apple2e`, `c64`), on Moonbase's own ROMs, which need nothing of Apple's or
7
+ Commodore's.
8
+ - **Remake** (`way: 'remake'`): the game's scenes, lifted from its code and compiled to JavaScript, on the remake's
9
+ engine (`engines/scenes.js`, a title of `card` or `vector` scenes), or the title's own engine, a module in its folder.
10
+
11
+ The player makes its part of the page and owns it, the same for every title: the section (classes `player`, the way,
12
+ the title's slug), the stage sized a whole number of the device's pixels to each of the screen's dots, the options
13
+ line and the status, the keys (one listener, each machine's codes, `keys.js`), the sound, a Commodore 64's printer,
14
+ the saves (`saves.js`), the settings the player and its engine act on, each declared where it acts and shown only
15
+ there (`settings.js`), and what the page asks of it (`watch`, `claim`, `pace`, keys and a route). An engine plays
16
+ the game on the stage and returns its handle (`play.js` says what it may have). The page tells the player everything
17
+ and the player reads no address and keeps nothing itself: the page gives the title's disk, a virtual disk of files
18
+ (`disk/disk.js` `openDisk`), and the person's settings, a typed store (`settings.js`: a setting is a declared name,
19
+ type, default and owner; only a declared setting can be read or kept, its value checked). Both come from a
20
+ **provider** (`provider.js`), which chooses once a visit where files and settings live, the backend (this browser's,
21
+ IndexedDB; an account's, a site's own); a site makes one a page from a module of its own, giving its policy of
22
+ settings (hidden, fixed, or a value for the visit) and the player never chooses. Nothing in the package knows of an
23
+ address: a page that lets its address set anything reads it itself, each input by its type. The disk has its own
24
+ dialogs (`disk/dialogs.js`, in a modal window, `modal.js`). A game saved in one way loads in the other.
15
25
 
16
26
  No title is named in it. A title is a folder of its own, kept with the title, which the page gives the player:
17
27
 
18
- - `title.js`, what the title is: its engine (`'card'`, `'vector'` or a module of its own), its scripts and the one
19
- it starts with, its `map` (the routines its scenes call, by address, and what its engine needs beside them:
20
- `remake/cards.js` and `remake/vector.js` say what each engine reads), the modules it brings (`modules`, its
21
- problems; `view`, its view's own drawing), its font's own glyphs (`font`), its stylesheet (`style`);
28
+ - `title.js`, what the title is: its engine (`'card'`, `'vector'` or a module of its own), its screen where it is not
29
+ its machine's (`screen`, in its own dots), its scripts and the one it starts with, its `map` (the routines its
30
+ scenes call, by address, and what its engine needs beside them: `remake/cards.js` and `remake/vector.js` say what
31
+ each engine reads), the modules it brings (`modules`, its problems; `view`, its view's own drawing), its font's own
32
+ glyphs (`font`), its stylesheet (`style`);
22
33
  - its scenes (`<name>.scene`) and `compiled/` (`moonbase-compile`).
23
34
 
24
35
  Beside it the game's data, `<name>.json` (its disks' files, its kind, and where it saves: `saves`).
@@ -28,10 +39,13 @@ Beside it the game's data, `<name>.json` (its disks' files, its kind, and where
28
39
  <script type="importmap">{"imports": {"@moonbase-machines/player/": "/packages/player/", "@moonbase-machines/apple2e/": "/packages/apple2e/", "@moonbase-machines/c64/": "/packages/c64/", "@moonbase-machines/charset/": "/packages/charset/", "@moonbase-machines/mos6502/": "/packages/mos6502/", "@moonbase-machines/vector/": "/packages/vector/"}}</script>
29
40
  <section id="player"></section>
30
41
  <script type="module">
31
- import { play } from '@moonbase-machines/player/remake/main.js';
32
- import { openDisk } from '@moonbase-machines/player/disk/disk.js';
33
- await play({ title: new URL('title/', location).href, data: new URL('game.json', location).href,
34
- root: document.getElementById('player'), disk: await openDisk('title/game'), words: { printer: { line: '', print: 'Print', tear: 'Tear off' } } });
42
+ import { play } from '@moonbase-machines/player/play.js';
43
+ import { titleDisk } from '@moonbase-machines/player/disk/disk.js';
44
+ import { provider } from '@moonbase-machines/player/provider.js';
45
+ const { disk, settings } = await provider(); // (this browser's)
46
+ await play({ way: 'remake', slug: 'game', title: new URL('title/', location).href, data: new URL('game.json', location).href,
47
+ root: document.getElementById('player'), disk: await disk(titleDisk('game')), settings,
48
+ words: { printer: { line: '', print: 'Print', tear: 'Tear off' } } });
35
49
  </script>
36
50
  ```
37
51
 
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // A title played on this machine: its page at http://127.0.0.1:8002/, its Remake where it has one, and an Original for
3
- // each game's data beside it (/?way=original, or with more than one /?game=<name>), on the browser's own player's
4
- // disk; each file sent with `Cache-Control: no-cache` so that a refresh asks for every module again. The folder holds
3
+ // each game's data beside it (/?way=original, or with more than one /?game=<name>); its disk and settings from the
4
+ // package's own provider (provider.js), this browser's, as the page has no site to give them; the page's own inputs
5
+ // its own, each checked: ?way and ?game one of the ways it has, ?keys=space,return keys to press at the start; each
6
+ // file sent with `Cache-Control: no-cache` so that a refresh asks for every module again. The folder holds
5
7
  // the title's folder, title/ (title.js, its scenes, compiled/), if it is remade, and its games' data, <name>.json
6
8
  // each, or for a title of its own engine data/; the @moonbase-machines packages are served from node_modules at
7
9
  // /packages/, where the page's import map finds them. A folder with an index.html of its own is served as it is.
@@ -30,9 +32,10 @@ async function page() {
30
32
  const games = await Promise.all(readdirSync(root).filter(f => f.endsWith('.json') && f !== 'package.json').sort((a, b) => (b === title?.slug + '.json') - (a === title?.slug + '.json') || a.slice(0, -5).localeCompare(b.slice(0, -5)))
31
33
  .map(async f => ({ name: f.slice(0, -5), data: f, game: JSON.parse(await readFile(join(root, f), 'utf8')) })));
32
34
  const slug = title?.slug ?? games[0]?.name, name = title?.name ?? games[0]?.game.title ?? slug;
33
- // (the ways: the remake, its data the title's own or, for a title of its own engine, data/; an original for each game)
34
- const ways = [...(title ? [{ label: 'Remake', search: '', module: 'remake/main.js', data: existsSync(join(root, slug + '.json')) ? slug + '.json' : 'data/' }] : []),
35
- ...games.map((g, i) => ({ label: games.length > 1 ? g.game.title : 'Original', search: !title && !i ? '' : games.length > 1 ? `?game=${g.name}` : '?way=original', module: 'original.js', data: g.data }))];
35
+ // (the ways: the remake, its data the title's own or, for a title of its own engine, data/; an original for each
36
+ // game, by the game's name where there are several; each with its address)
37
+ const ways = [...(title ? [{ label: 'Remake', search: '', way: 'remake', game: null, data: existsSync(join(root, slug + '.json')) ? slug + '.json' : 'data/' }] : []),
38
+ ...games.map((g, i) => ({ label: games.length > 1 ? g.game.title : 'Original', search: !title && !i ? '' : games.length > 1 ? `?game=${g.name}` : '?way=original', way: 'original', game: games.length > 1 ? g.name : null, data: g.data }))];
36
39
  const map = { imports: Object.fromEntries(PACKAGES.map(p => [`@moonbase-machines/${p}/`, `/packages/${p}/`])) };
37
40
  return `<!doctype html>
38
41
  <html lang="en">
@@ -52,11 +55,19 @@ ${ways.length > 1 ? `<nav>${ways.map(w => `<a href="/${w.search}">${esc(w.label)
52
55
  </main>
53
56
  <script type="module">
54
57
  const ways = ${JSON.stringify(ways)};
55
- const way = ways.find(w => w.search === location.search) ?? ways[0];
58
+ const [{ play }, { titleDisk }, { provider }] = await Promise.all(['play.js', 'disk/disk.js', 'provider.js'].map(m => import('@moonbase-machines/player/' + m)));
59
+ // (the page's own inputs, each checked: the way or a game by its name, one of those it has, else the first; the keys
60
+ // to press, names of letters, digits and dashes; anything else refused and said)
61
+ const q = new URLSearchParams(location.search), refused = (n, what) => { console.warn('?' + n + '=' + q.get(n) + ' refused: not ' + what); return null; };
62
+ const one = (n, of) => !q.has(n) ? null : of.includes(q.get(n)) ? q.get(n) : refused(n, 'one of ' + of.join(', '));
63
+ const askedGame = one('game', ways.map(w => w.game).filter(Boolean)), askedWay = one('way', [...new Set(ways.map(w => w.way))]);
64
+ const way = ways.find(w => askedGame ? w.game === askedGame : askedWay ? w.way === askedWay : true) ?? ways[0];
56
65
  for (const a of document.querySelectorAll('nav a')) if (a.search === way.search) a.setAttribute('aria-current', 'page');
57
- const [{ play }, { openDisk, titleDisk }] = await Promise.all([import('@moonbase-machines/player/' + way.module), import('@moonbase-machines/player/disk/disk.js')]);
66
+ const keys = !q.has('keys') ? undefined : /^[\\w-]+(,[\\w-]+)*$/.test(q.get('keys')) ? q.get('keys').split(',') : refused('keys', 'names separated by commas') ?? undefined;
58
67
  const at = path => new URL(path, location.origin + '/').href;
59
- await play({ title: at('title/'), data: at(way.data), root: document.getElementById('player'), disk: await openDisk(titleDisk(${JSON.stringify(slug)})),
68
+ const { disk, settings } = await provider();
69
+ await play({ way: way.way, slug: ${JSON.stringify(slug)}, title: ${title ? "at('title/')" : 'null'}, data: at(way.data), root: document.getElementById('player'),
70
+ disk: await disk(titleDisk(${JSON.stringify(slug)})), settings, keys,
60
71
  words: { printer: { line: 'What the printer has printed.', print: 'Print', tear: 'Tear off' } } });
61
72
  </script>
62
73
  </body>
package/disk/disk.js CHANGED
@@ -8,32 +8,34 @@
8
8
  // @moonbase-machines/c64/drive.js's find), so a read is never waited for. A write changes the copy at once and is stored behind
9
9
  // it, in order.
10
10
  //
11
- // const disk = await openDisk(name, { backend }) backend: local() (local.js, the default)
12
- // or memory() (memory.js)
11
+ // const disk = await openDisk(name, { backend }) backend: where its files are kept, the provider's choice
12
+ // (provider.js), never the disk's: local() (local.js, this
13
+ // browser's) or memory() (memory.js)
13
14
  // disk.list() [{ name, size, modified, meta }], newest first
14
15
  // disk.read(name) its bytes, or null
15
16
  // disk.write(name, bytes, meta) a promise, kept when stored
16
17
  // disk.remove(name) a promise
17
18
  // disk.written() a promise: every write so far stored
19
+ // disk.failed = err => ... told of a write or remove the backend could not store (the queue goes on)
18
20
  // disk.name, disk.kept the disk's name; whether its files outlast the page
21
+ // disk.where where they are kept, as the backend says ('this browser'), for the dialogs
19
22
  //
20
23
  // A disk's name says whose it is, so a title and a program can never share one: titleDisk(slug), a title's
21
24
  // player's, 'title/zoopak'; programDisk(name), one of the site's own programs', 'program/basic'.
22
25
  //
23
- // The backend is where a disk's files are kept: { kept, load(disk), put(disk, file), delete(disk, name) },
26
+ // The backend is where a disk's files are kept: { kept, where, load(disk), put(disk, file), delete(disk, name) },
24
27
  // a file { name, bytes, meta, modified }. This browser's is local(); an account's will be the API's.
25
- import { local } from './local.js';
26
- import { memory } from './memory.js';
27
28
 
28
- export async function openDisk(name, { backend } = {}) {
29
- // (a browser that keeps nothing, as a private window may: the disk in memory, and said so)
30
- if (!backend) { try { backend = await local(); } catch { backend = memory({ kept: false }); } }
29
+ export async function openDisk(name, { backend }) {
30
+ if (!backend) throw new Error('openDisk: a backend is the provider\'s to give (provider.js)');
31
31
  const files = new Map((await backend.load(name)).map(f => [f.name, f]));
32
32
  let queue = Promise.resolve();
33
- const later = job => (queue = queue.then(job));
34
- return {
33
+ // (each job stored in turn; one that fails is said (`failed`) and the next goes on, its own promise rejected for a
34
+ // caller that waits for it)
35
+ const later = job => { const p = queue.then(job); queue = p.catch(err => disk.failed?.(err)); p.catch(() => {}); return p; };
36
+ const disk = {
35
37
  name,
36
- kept: backend.kept,
38
+ kept: backend.kept, where: backend.where ?? null, failed: null,
37
39
  list: () => [...files.values()].sort((a, b) => b.modified - a.modified).map(({ name, bytes, meta, modified }) => ({ name, size: bytes.length, modified, meta })),
38
40
  read: n => files.get(n)?.bytes ?? null,
39
41
  write(n, bytes, meta = {}) {
@@ -47,6 +49,7 @@ export async function openDisk(name, { backend } = {}) {
47
49
  },
48
50
  written: () => queue,
49
51
  };
52
+ return disk;
50
53
  }
51
54
 
52
55
  export const titleDisk = slug => `title/${slug}`;
package/disk/local.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // keyed by its account, its disk and its name. The account is `local`, this browser's own, until there
3
3
  // are accounts; theirs will be kept by the API, behind the same calls.
4
4
  //
5
- // await local({ account }) a backend: { kept, load(disk), put(disk, file), delete(disk, name) }
5
+ // await local({ account }) a backend: { kept, where, load(disk), put(disk, file), delete(disk, name) }
6
6
  // (named here once, for a tool that puts a file on a disk too)
7
7
  export const DB = 'moonbase', VERSION = 1, STORE = 'files', KEY = ['account', 'disk', 'name'], ACCOUNT = 'local';
8
8
 
@@ -18,7 +18,7 @@ export async function local({ account = ACCOUNT } = {}) {
18
18
  const db = await opened();
19
19
  const store = mode => db.transaction(STORE, mode).objectStore(STORE);
20
20
  return {
21
- kept: true,
21
+ kept: true, where: 'this browser',
22
22
  // (the disk's files: those keyed from [account, disk] up to the next disk's)
23
23
  load: async disk => (await asked(store('readonly').getAll(IDBKeyRange.bound([account, disk], [account, disk, []]))))
24
24
  .map(({ name, bytes, meta, modified }) => ({ name, bytes, meta, modified })),