@zaisma/game-server 1.3.0 → 1.5.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 +2 -0
- package/bench/brand/.assetsignore +4 -0
- package/bench/brand/README.md +51 -0
- package/bench/brand/_headers +13 -0
- package/bench/brand/app-icon-1024.png +0 -0
- package/bench/brand/app-icon-192.png +0 -0
- package/bench/brand/app-icon-512.png +0 -0
- package/bench/brand/apple-touch-icon.png +0 -0
- package/bench/brand/cover-guide/coin-cascade-busy-fine.webp +0 -0
- package/bench/brand/cover-guide/gem-grid-cover.webp +0 -0
- package/bench/brand/cover-guide/gem-grid-screenshot.webp +0 -0
- package/bench/brand/cover-guide/good-arena.webp +0 -0
- package/bench/brand/cover-guide/good-four-in-a-row.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-extra-text.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-fake-reward.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-fine.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-overlay.webp +0 -0
- package/bench/brand/cover-guide/screenshot-fine.webp +0 -0
- package/bench/brand/cover-guide/screenshot-not-fine.webp +0 -0
- package/bench/brand/design/covers/ashen-crown.webp +0 -0
- package/bench/brand/design/covers/frostline.webp +0 -0
- package/bench/brand/design/covers/glasswing.webp +0 -0
- package/bench/brand/design/covers/hollow-tide.webp +0 -0
- package/bench/brand/design/covers/iron-vigil.webp +0 -0
- package/bench/brand/design/covers/kestrel.webp +0 -0
- package/bench/brand/design/covers/neon-drift.webp +0 -0
- package/bench/brand/design/covers/verdant.webp +0 -0
- package/bench/brand/favicon-32.png +0 -0
- package/bench/brand/favicon.ico +0 -0
- package/bench/brand/favicon.svg +1 -0
- package/bench/brand/flags/LICENSE +21 -0
- package/bench/brand/flags/ad.png +0 -0
- package/bench/brand/flags/ae.png +0 -0
- package/bench/brand/flags/af.png +0 -0
- package/bench/brand/flags/ag.png +0 -0
- package/bench/brand/flags/ai.png +0 -0
- package/bench/brand/flags/al.png +0 -0
- package/bench/brand/flags/am.png +0 -0
- package/bench/brand/flags/ao.png +0 -0
- package/bench/brand/flags/aq.png +0 -0
- package/bench/brand/flags/ar.png +0 -0
- package/bench/brand/flags/as.png +0 -0
- package/bench/brand/flags/at.png +0 -0
- package/bench/brand/flags/au.png +0 -0
- package/bench/brand/flags/aw.png +0 -0
- package/bench/brand/flags/ax.png +0 -0
- package/bench/brand/flags/az.png +0 -0
- package/bench/brand/flags/ba.png +0 -0
- package/bench/brand/flags/bb.png +0 -0
- package/bench/brand/flags/bd.png +0 -0
- package/bench/brand/flags/be.png +0 -0
- package/bench/brand/flags/bf.png +0 -0
- package/bench/brand/flags/bg.png +0 -0
- package/bench/brand/flags/bh.png +0 -0
- package/bench/brand/flags/bi.png +0 -0
- package/bench/brand/flags/bj.png +0 -0
- package/bench/brand/flags/bl.png +0 -0
- package/bench/brand/flags/bm.png +0 -0
- package/bench/brand/flags/bn.png +0 -0
- package/bench/brand/flags/bo.png +0 -0
- package/bench/brand/flags/bq.png +0 -0
- package/bench/brand/flags/br.png +0 -0
- package/bench/brand/flags/bs.png +0 -0
- package/bench/brand/flags/bt.png +0 -0
- package/bench/brand/flags/bv.png +0 -0
- package/bench/brand/flags/bw.png +0 -0
- package/bench/brand/flags/by.png +0 -0
- package/bench/brand/flags/bz.png +0 -0
- package/bench/brand/flags/ca.png +0 -0
- package/bench/brand/flags/cc.png +0 -0
- package/bench/brand/flags/cd.png +0 -0
- package/bench/brand/flags/cf.png +0 -0
- package/bench/brand/flags/cg.png +0 -0
- package/bench/brand/flags/ch.png +0 -0
- package/bench/brand/flags/ci.png +0 -0
- package/bench/brand/flags/ck.png +0 -0
- package/bench/brand/flags/cl.png +0 -0
- package/bench/brand/flags/cm.png +0 -0
- package/bench/brand/flags/cn.png +0 -0
- package/bench/brand/flags/co.png +0 -0
- package/bench/brand/flags/cp.png +0 -0
- package/bench/brand/flags/cr.png +0 -0
- package/bench/brand/flags/cu.png +0 -0
- package/bench/brand/flags/cv.png +0 -0
- package/bench/brand/flags/cw.png +0 -0
- package/bench/brand/flags/cx.png +0 -0
- package/bench/brand/flags/cy.png +0 -0
- package/bench/brand/flags/cz.png +0 -0
- package/bench/brand/flags/de.png +0 -0
- package/bench/brand/flags/dg.png +0 -0
- package/bench/brand/flags/dj.png +0 -0
- package/bench/brand/flags/dk.png +0 -0
- package/bench/brand/flags/dm.png +0 -0
- package/bench/brand/flags/do.png +0 -0
- package/bench/brand/flags/dz.png +0 -0
- package/bench/brand/flags/ec.png +0 -0
- package/bench/brand/flags/ee.png +0 -0
- package/bench/brand/flags/eg.png +0 -0
- package/bench/brand/flags/eh.png +0 -0
- package/bench/brand/flags/er.png +0 -0
- package/bench/brand/flags/es.png +0 -0
- package/bench/brand/flags/et.png +0 -0
- package/bench/brand/flags/eu.png +0 -0
- package/bench/brand/flags/fi.png +0 -0
- package/bench/brand/flags/fj.png +0 -0
- package/bench/brand/flags/fk.png +0 -0
- package/bench/brand/flags/fm.png +0 -0
- package/bench/brand/flags/fo.png +0 -0
- package/bench/brand/flags/fr.png +0 -0
- package/bench/brand/flags/ga.png +0 -0
- package/bench/brand/flags/gb.png +0 -0
- package/bench/brand/flags/gd.png +0 -0
- package/bench/brand/flags/ge.png +0 -0
- package/bench/brand/flags/gf.png +0 -0
- package/bench/brand/flags/gg.png +0 -0
- package/bench/brand/flags/gh.png +0 -0
- package/bench/brand/flags/gi.png +0 -0
- package/bench/brand/flags/gl.png +0 -0
- package/bench/brand/flags/gm.png +0 -0
- package/bench/brand/flags/gn.png +0 -0
- package/bench/brand/flags/gp.png +0 -0
- package/bench/brand/flags/gq.png +0 -0
- package/bench/brand/flags/gr.png +0 -0
- package/bench/brand/flags/gs.png +0 -0
- package/bench/brand/flags/gt.png +0 -0
- package/bench/brand/flags/gu.png +0 -0
- package/bench/brand/flags/gw.png +0 -0
- package/bench/brand/flags/gy.png +0 -0
- package/bench/brand/flags/hk.png +0 -0
- package/bench/brand/flags/hm.png +0 -0
- package/bench/brand/flags/hn.png +0 -0
- package/bench/brand/flags/hr.png +0 -0
- package/bench/brand/flags/ht.png +0 -0
- package/bench/brand/flags/hu.png +0 -0
- package/bench/brand/flags/ic.png +0 -0
- package/bench/brand/flags/id.png +0 -0
- package/bench/brand/flags/ie.png +0 -0
- package/bench/brand/flags/il.png +0 -0
- package/bench/brand/flags/im.png +0 -0
- package/bench/brand/flags/in.png +0 -0
- package/bench/brand/flags/io.png +0 -0
- package/bench/brand/flags/iq.png +0 -0
- package/bench/brand/flags/ir.png +0 -0
- package/bench/brand/flags/is.png +0 -0
- package/bench/brand/flags/it.png +0 -0
- package/bench/brand/flags/je.png +0 -0
- package/bench/brand/flags/jm.png +0 -0
- package/bench/brand/flags/jo.png +0 -0
- package/bench/brand/flags/jp.png +0 -0
- package/bench/brand/flags/ke.png +0 -0
- package/bench/brand/flags/kg.png +0 -0
- package/bench/brand/flags/kh.png +0 -0
- package/bench/brand/flags/ki.png +0 -0
- package/bench/brand/flags/km.png +0 -0
- package/bench/brand/flags/kn.png +0 -0
- package/bench/brand/flags/kp.png +0 -0
- package/bench/brand/flags/kr.png +0 -0
- package/bench/brand/flags/kw.png +0 -0
- package/bench/brand/flags/ky.png +0 -0
- package/bench/brand/flags/kz.png +0 -0
- package/bench/brand/flags/la.png +0 -0
- package/bench/brand/flags/lb.png +0 -0
- package/bench/brand/flags/lc.png +0 -0
- package/bench/brand/flags/li.png +0 -0
- package/bench/brand/flags/lk.png +0 -0
- package/bench/brand/flags/lr.png +0 -0
- package/bench/brand/flags/ls.png +0 -0
- package/bench/brand/flags/lt.png +0 -0
- package/bench/brand/flags/lu.png +0 -0
- package/bench/brand/flags/lv.png +0 -0
- package/bench/brand/flags/ly.png +0 -0
- package/bench/brand/flags/ma.png +0 -0
- package/bench/brand/flags/mc.png +0 -0
- package/bench/brand/flags/md.png +0 -0
- package/bench/brand/flags/me.png +0 -0
- package/bench/brand/flags/mf.png +0 -0
- package/bench/brand/flags/mg.png +0 -0
- package/bench/brand/flags/mh.png +0 -0
- package/bench/brand/flags/mk.png +0 -0
- package/bench/brand/flags/ml.png +0 -0
- package/bench/brand/flags/mm.png +0 -0
- package/bench/brand/flags/mn.png +0 -0
- package/bench/brand/flags/mo.png +0 -0
- package/bench/brand/flags/mp.png +0 -0
- package/bench/brand/flags/mq.png +0 -0
- package/bench/brand/flags/mr.png +0 -0
- package/bench/brand/flags/ms.png +0 -0
- package/bench/brand/flags/mt.png +0 -0
- package/bench/brand/flags/mu.png +0 -0
- package/bench/brand/flags/mv.png +0 -0
- package/bench/brand/flags/mw.png +0 -0
- package/bench/brand/flags/mx.png +0 -0
- package/bench/brand/flags/my.png +0 -0
- package/bench/brand/flags/mz.png +0 -0
- package/bench/brand/flags/na.png +0 -0
- package/bench/brand/flags/nc.png +0 -0
- package/bench/brand/flags/ne.png +0 -0
- package/bench/brand/flags/nf.png +0 -0
- package/bench/brand/flags/ng.png +0 -0
- package/bench/brand/flags/ni.png +0 -0
- package/bench/brand/flags/nl.png +0 -0
- package/bench/brand/flags/no.png +0 -0
- package/bench/brand/flags/np.png +0 -0
- package/bench/brand/flags/nr.png +0 -0
- package/bench/brand/flags/nu.png +0 -0
- package/bench/brand/flags/nz.png +0 -0
- package/bench/brand/flags/om.png +0 -0
- package/bench/brand/flags/pa.png +0 -0
- package/bench/brand/flags/pc.png +0 -0
- package/bench/brand/flags/pe.png +0 -0
- package/bench/brand/flags/pf.png +0 -0
- package/bench/brand/flags/pg.png +0 -0
- package/bench/brand/flags/ph.png +0 -0
- package/bench/brand/flags/pk.png +0 -0
- package/bench/brand/flags/pl.png +0 -0
- package/bench/brand/flags/pm.png +0 -0
- package/bench/brand/flags/pn.png +0 -0
- package/bench/brand/flags/pr.png +0 -0
- package/bench/brand/flags/ps.png +0 -0
- package/bench/brand/flags/pt.png +0 -0
- package/bench/brand/flags/pw.png +0 -0
- package/bench/brand/flags/py.png +0 -0
- package/bench/brand/flags/qa.png +0 -0
- package/bench/brand/flags/re.png +0 -0
- package/bench/brand/flags/ro.png +0 -0
- package/bench/brand/flags/rs.png +0 -0
- package/bench/brand/flags/ru.png +0 -0
- package/bench/brand/flags/rw.png +0 -0
- package/bench/brand/flags/sa.png +0 -0
- package/bench/brand/flags/sb.png +0 -0
- package/bench/brand/flags/sc.png +0 -0
- package/bench/brand/flags/sd.png +0 -0
- package/bench/brand/flags/se.png +0 -0
- package/bench/brand/flags/sg.png +0 -0
- package/bench/brand/flags/sh.png +0 -0
- package/bench/brand/flags/si.png +0 -0
- package/bench/brand/flags/sj.png +0 -0
- package/bench/brand/flags/sk.png +0 -0
- package/bench/brand/flags/sl.png +0 -0
- package/bench/brand/flags/sm.png +0 -0
- package/bench/brand/flags/sn.png +0 -0
- package/bench/brand/flags/so.png +0 -0
- package/bench/brand/flags/sr.png +0 -0
- package/bench/brand/flags/ss.png +0 -0
- package/bench/brand/flags/st.png +0 -0
- package/bench/brand/flags/sv.png +0 -0
- package/bench/brand/flags/sx.png +0 -0
- package/bench/brand/flags/sy.png +0 -0
- package/bench/brand/flags/sz.png +0 -0
- package/bench/brand/flags/tc.png +0 -0
- package/bench/brand/flags/td.png +0 -0
- package/bench/brand/flags/tf.png +0 -0
- package/bench/brand/flags/tg.png +0 -0
- package/bench/brand/flags/th.png +0 -0
- package/bench/brand/flags/tj.png +0 -0
- package/bench/brand/flags/tk.png +0 -0
- package/bench/brand/flags/tl.png +0 -0
- package/bench/brand/flags/tm.png +0 -0
- package/bench/brand/flags/tn.png +0 -0
- package/bench/brand/flags/to.png +0 -0
- package/bench/brand/flags/tr.png +0 -0
- package/bench/brand/flags/tt.png +0 -0
- package/bench/brand/flags/tv.png +0 -0
- package/bench/brand/flags/tw.png +0 -0
- package/bench/brand/flags/tz.png +0 -0
- package/bench/brand/flags/ua.png +0 -0
- package/bench/brand/flags/ug.png +0 -0
- package/bench/brand/flags/um.png +0 -0
- package/bench/brand/flags/un.png +0 -0
- package/bench/brand/flags/us.png +0 -0
- package/bench/brand/flags/uy.png +0 -0
- package/bench/brand/flags/uz.png +0 -0
- package/bench/brand/flags/va.png +0 -0
- package/bench/brand/flags/vc.png +0 -0
- package/bench/brand/flags/ve.png +0 -0
- package/bench/brand/flags/vg.png +0 -0
- package/bench/brand/flags/vi.png +0 -0
- package/bench/brand/flags/vn.png +0 -0
- package/bench/brand/flags/vu.png +0 -0
- package/bench/brand/flags/wf.png +0 -0
- package/bench/brand/flags/ws.png +0 -0
- package/bench/brand/flags/xk.png +0 -0
- package/bench/brand/flags/xx.png +0 -0
- package/bench/brand/flags/ye.png +0 -0
- package/bench/brand/flags/yt.png +0 -0
- package/bench/brand/flags/za.png +0 -0
- package/bench/brand/flags/zm.png +0 -0
- package/bench/brand/flags/zw.png +0 -0
- package/bench/brand/fonts/OFL.txt +93 -0
- package/bench/brand/fonts/jost.7726a5cd.woff2 +0 -0
- package/bench/brand/loader/gif/zaisma-loader-black-128.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-black-512.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-white-128.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-white-512.gif +0 -0
- package/bench/brand/loader/png/zaisma-loader-black-128.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-black-512.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-white-128.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-white-512.png +0 -0
- package/bench/brand/loader/webp/zaisma-loader-black-128.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-black-512.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-white-128.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-white-512.webp +0 -0
- package/bench/brand/loader/zaisma-loader-black.svg +1 -0
- package/bench/brand/loader/zaisma-loader-white.svg +1 -0
- package/bench/brand/loader/zaisma-loader.svg +1 -0
- package/bench/brand/sign-in/city-day-phone.webp +0 -0
- package/bench/brand/sign-in/city-day.webp +0 -0
- package/bench/brand/sign-in/city-night-phone.webp +0 -0
- package/bench/brand/sign-in/city-night.webp +0 -0
- package/bench/brand/zaisma-app-icon.svg +1 -0
- package/bench/brand/zaisma-mark-black-2048.png +0 -0
- package/bench/brand/zaisma-mark-black.svg +1 -0
- package/bench/brand/zaisma-mark-metal-2048.png +0 -0
- package/bench/brand/zaisma-mark-metal.svg +1 -0
- package/bench/brand/zaisma-mark-small-black.svg +1 -0
- package/bench/brand/zaisma-mark-small-white.svg +1 -0
- package/bench/brand/zaisma-mark-tricolor.svg +1 -0
- package/bench/brand/zaisma-mark-white-2048.png +0 -0
- package/bench/brand/zaisma-mark-white.svg +1 -0
- package/bench/brand/zaisma-wordmark-black-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-black.svg +1 -0
- package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-metal.svg +1 -0
- package/bench/brand/zaisma-wordmark-white-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-white.svg +1 -0
- package/bench/glass.js +46466 -0
- package/bin/local-play-bench.d.ts +8 -0
- package/bin/local-play-bench.js +218 -0
- package/bin/local-play-certificate.js +99 -0
- package/bin/local-play-runner.js +43 -0
- package/bin/local-play.d.ts +41 -0
- package/bin/local-play.js +1567 -0
- package/bin/node.js +71 -5
- package/package.json +1 -1
- package/sdk/picture.js +97 -0
- package/sdk/relay.js +269 -0
- package/sdk/saves.js +884 -0
- package/sdk/zaisma.d.ts +1720 -0
- package/sdk/zaisma.js +2374 -0
- package/src/qr-code.d.ts +20 -0
- package/src/qr-code.js +319 -0
- package/src/room.js +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** What the dev page's frame lets the player's page pass on to the game: the game page's own list. */
|
|
2
|
+
export const FRAME_ALLOW: string;
|
|
3
|
+
/** The dev page's script. */
|
|
4
|
+
export function devScriptSource(): string;
|
|
5
|
+
/** The end of a sign-in on the bench (/account/kept). */
|
|
6
|
+
export const KEPT_SCRIPT_SOURCE: string;
|
|
7
|
+
/** Where a tab arrives: its own player number again, or a new player's. */
|
|
8
|
+
export const ARRIVE_SCRIPT_SOURCE: string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// The dev page's own scripts (local-play.js serves them as /bench/dev.js and /bench/kept.js). The dev page is the bench's: its bar
|
|
2
|
+
// (bench/glass.ts) and one frame, the player's page, which is zaisma.com's game page with its own scripts. These never reach into it.
|
|
3
|
+
|
|
4
|
+
/** What the dev page's frame lets the player's page pass on to the game: the game page's own list (apps/site/src/pages.ts; test/node/local-play-parity.test.ts holds the two together). */
|
|
5
|
+
export const FRAME_ALLOW = "fullscreen; gamepad; autoplay; web-share; clipboard-write; cross-origin-isolated";
|
|
6
|
+
|
|
7
|
+
/** The dev page's script: it keeps the player's place, reloads the game when a file changes, runs the dev bar's menus and the Players panel. */
|
|
8
|
+
export const devScriptSource = () => `(${devScript.toString()})();\n`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The end of a sign-in on the bench (/account/kept): it tells the open game pages, as zaisma.com's own page does. Opened as a tab of its own,
|
|
12
|
+
* it closes; reached in the player's own tab (a blocked pop-up sends the sign-in there), it goes back to the player's dev page and game.
|
|
13
|
+
*/
|
|
14
|
+
export const KEPT_SCRIPT_SOURCE = `try { new BroadcastChannel("zaisma").postMessage("signed-in"); } catch {}
|
|
15
|
+
var keptPlayerNumber = Number(new URL(document.currentScript.src).searchParams.get("player")) || 0;
|
|
16
|
+
setTimeout(function () {
|
|
17
|
+
if (window.opener) window.close();
|
|
18
|
+
else location.replace(keptPlayerNumber ? "/?player=" + keptPlayerNumber : "/");
|
|
19
|
+
}, 800);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
/** Where a tab arrives (/ with no player): the tab's own player number again, or a new player's, then the dev page. */
|
|
23
|
+
export const ARRIVE_SCRIPT_SOURCE = `(function () {
|
|
24
|
+
var kept = null;
|
|
25
|
+
try { kept = Number(sessionStorage.getItem("zaisma-bench:player")) || null; } catch (error) {}
|
|
26
|
+
var room = /^[A-Z0-9]{6}$/.test(new URLSearchParams(location.search).get("room") || "") ? new URLSearchParams(location.search).get("room") : null;
|
|
27
|
+
fetch("/bench/claim", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ keptPlayerNumber: kept }) })
|
|
28
|
+
.then(function (response) { return response.json(); })
|
|
29
|
+
.then(function (answer) { location.replace("/?player=" + answer.playerNumber + (room ? "&room=" + room : "")); });
|
|
30
|
+
})();
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/** Run in the browser. Written as a function so it stays real code; the bench serves its text. */
|
|
34
|
+
function devScript() {
|
|
35
|
+
const scriptUrl = new URL(document.currentScript.src);
|
|
36
|
+
const isPage = scriptUrl.searchParams.get("page") === "players";
|
|
37
|
+
let playerNumber = Number(scriptUrl.searchParams.get("player")) || 0;
|
|
38
|
+
try {
|
|
39
|
+
// The Players page a sign-in opened knows its player from the tab (a tab opened by a page starts with a copy of the page's storage).
|
|
40
|
+
if (isPage && !playerNumber) playerNumber = Number(sessionStorage.getItem("zaisma-bench:player")) || 0;
|
|
41
|
+
else sessionStorage.setItem("zaisma-bench:player", String(playerNumber));
|
|
42
|
+
} catch {
|
|
43
|
+
// Storage off: the tab is a new player each time it comes to /.
|
|
44
|
+
}
|
|
45
|
+
const frame = document.getElementById("bench-player");
|
|
46
|
+
const panel = document.getElementById("bench-panel");
|
|
47
|
+
const playersButton = document.getElementById("bench-players-open");
|
|
48
|
+
const addTab = document.getElementById("bench-add-tab");
|
|
49
|
+
let reloadNumber = null;
|
|
50
|
+
let roomCode = null;
|
|
51
|
+
|
|
52
|
+
const refresh = (name) =>
|
|
53
|
+
fetch(`/bench/sheet/${name}?player=${playerNumber}`)
|
|
54
|
+
.then((response) => response.text())
|
|
55
|
+
.then((text) => {
|
|
56
|
+
const body = document.getElementById(`bench-${name}-body`);
|
|
57
|
+
if (body) body.innerHTML = text;
|
|
58
|
+
})
|
|
59
|
+
.catch(() => {});
|
|
60
|
+
|
|
61
|
+
// --- The Players panel: drawn by the bench, and every change through the bench's API, the same calls a developer's AI makes ---
|
|
62
|
+
let panelView = { view: "list", playerId: "", tab: "profile", search: "" };
|
|
63
|
+
const drawPanel = (changes = {}) => {
|
|
64
|
+
panelView = { ...panelView, ...changes };
|
|
65
|
+
const query = new URLSearchParams({ player: String(playerNumber), view: panelView.view, player_id: panelView.playerId, tab: panelView.tab, search: panelView.search, page: isPage ? "yes" : "" });
|
|
66
|
+
return fetch(`/bench/panel?${query}`)
|
|
67
|
+
.then((response) => response.text())
|
|
68
|
+
.then((text) => {
|
|
69
|
+
panel.innerHTML = text;
|
|
70
|
+
const search = panel.querySelector("[data-bench-search]");
|
|
71
|
+
if (search && panelView.search) {
|
|
72
|
+
search.focus();
|
|
73
|
+
search.setSelectionRange(search.value.length, search.value.length);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {});
|
|
77
|
+
};
|
|
78
|
+
const showPanel = (isShown) => {
|
|
79
|
+
if (!panel || isPage) return;
|
|
80
|
+
panel.hidden = !isShown;
|
|
81
|
+
playersButton?.setAttribute("aria-pressed", String(isShown));
|
|
82
|
+
if (isShown) drawPanel();
|
|
83
|
+
};
|
|
84
|
+
const api = (method, path, body) =>
|
|
85
|
+
fetch(path, { method, headers: { "content-type": "application/json" }, ...(method === "GET" || method === "DELETE" ? {} : { body: JSON.stringify(body ?? {}) }) }).then(async (response) => ({ isOk: response.ok, answer: await response.json().catch(() => ({})) }));
|
|
86
|
+
const showError = (answer) => {
|
|
87
|
+
const line = panel.querySelector("[data-bench-error]");
|
|
88
|
+
if (line && answer?.error) {
|
|
89
|
+
line.textContent = answer.error.message;
|
|
90
|
+
line.hidden = false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
// Playing as someone starts the game again as them: here the dev page comes back with the game; on the page a sign-in opened, the
|
|
94
|
+
// game page hears it as zaisma.com's own end of a sign-in, and the page closes or goes back to the player's game.
|
|
95
|
+
const playAs = (playerId) =>
|
|
96
|
+
api("PUT", `/bench/api/open-players/${playerNumber}`, { player_id: playerId === "guest" ? null : playerId }).then(({ isOk }) => {
|
|
97
|
+
if (!isOk) return;
|
|
98
|
+
if (!isPage) return location.assign(`/?player=${playerNumber}`);
|
|
99
|
+
try {
|
|
100
|
+
new BroadcastChannel("zaisma").postMessage("signed-in");
|
|
101
|
+
} catch {
|
|
102
|
+
// No channel: the game page checks again as its tab comes back into view.
|
|
103
|
+
}
|
|
104
|
+
if (window.opener) window.close();
|
|
105
|
+
else location.assign(`/?player=${playerNumber}`);
|
|
106
|
+
});
|
|
107
|
+
if (panel) {
|
|
108
|
+
panel.addEventListener("click", (event) => {
|
|
109
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
110
|
+
const closer = target?.closest("[data-bench-panel]");
|
|
111
|
+
if (closer) return showPanel(false);
|
|
112
|
+
const viewer = target?.closest("[data-bench-view]");
|
|
113
|
+
if (viewer) return drawPanel({ view: viewer.dataset.benchView, playerId: viewer.dataset.playerId ?? panelView.playerId, tab: "profile", search: "" });
|
|
114
|
+
const tab = target?.closest("[data-bench-tab]");
|
|
115
|
+
if (tab) return drawPanel({ tab: tab.dataset.benchTab, search: "" });
|
|
116
|
+
const caller = target?.closest("[data-bench-call]");
|
|
117
|
+
if (caller) {
|
|
118
|
+
const [method, path] = caller.dataset.benchCall.split(" ");
|
|
119
|
+
return api(method, path, JSON.parse(caller.dataset.benchBody || "{}")).then(({ isOk, answer }) => (isOk ? drawPanel() : showError(answer)));
|
|
120
|
+
}
|
|
121
|
+
const maker = target?.closest("[data-bench-make]");
|
|
122
|
+
if (maker) {
|
|
123
|
+
const form = maker.closest("[data-bench-form]");
|
|
124
|
+
const field = (name) => form.querySelector(`[name="${name}"]`).value;
|
|
125
|
+
return api("POST", "/bench/api/players", { name: field("name"), age_band: field("age_band"), social_mode: field("social_mode") }).then(({ isOk, answer }) =>
|
|
126
|
+
isOk ? drawPanel({ view: "player", playerId: answer.player.player_id, tab: "profile", search: "" }) : showError(answer),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const deleter = target?.closest("[data-bench-delete]");
|
|
130
|
+
if (deleter && confirm(`Delete ${deleter.dataset.playerName}? Their saves, items, purchases, friends and blocks on the bench go too.`)) {
|
|
131
|
+
return api("DELETE", deleter.dataset.benchDelete).then(() => drawPanel({ view: "list", playerId: "" }));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// A profile's fields save as they change; picking a friend or a block adds them.
|
|
135
|
+
panel.addEventListener("change", (event) => {
|
|
136
|
+
const field = event.target;
|
|
137
|
+
if (!(field instanceof HTMLInputElement || field instanceof HTMLSelectElement)) return;
|
|
138
|
+
if (field.dataset.benchField) {
|
|
139
|
+
api("PATCH", field.dataset.benchField, { [field.name]: field.value }).then(({ isOk, answer }) => (isOk ? drawPanel() : showError(answer)));
|
|
140
|
+
} else if (field.dataset.benchAdd && field.value) {
|
|
141
|
+
api("PUT", `${field.dataset.benchAdd}${field.value}`).then(({ isOk, answer }) => (isOk ? drawPanel() : showError(answer)));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
let searchTimer = null;
|
|
145
|
+
panel.addEventListener("input", (event) => {
|
|
146
|
+
if (!(event.target instanceof HTMLInputElement) || !event.target.hasAttribute("data-bench-search")) return;
|
|
147
|
+
const search = event.target.value;
|
|
148
|
+
clearTimeout(searchTimer);
|
|
149
|
+
searchTimer = setTimeout(() => drawPanel({ search }), 200);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
playersButton?.addEventListener("click", () => showPanel(panel.hidden));
|
|
153
|
+
|
|
154
|
+
// --- The dev bar's menus: each opens under its button, drawn afresh where it shows what changes, and closes on a pick, Escape or a click away ---
|
|
155
|
+
const closeMenus = () => {
|
|
156
|
+
for (const menu of document.querySelectorAll(".bench-menu")) menu.hidden = true;
|
|
157
|
+
for (const button of document.querySelectorAll("[data-bench-menu]")) button.setAttribute("aria-expanded", "false");
|
|
158
|
+
};
|
|
159
|
+
const openMenu = (button) => {
|
|
160
|
+
const menu = document.getElementById(button.dataset.benchMenu);
|
|
161
|
+
const wasOpen = !menu.hidden;
|
|
162
|
+
closeMenus();
|
|
163
|
+
if (wasOpen) return;
|
|
164
|
+
menu.hidden = false;
|
|
165
|
+
const buttonBox = button.getBoundingClientRect();
|
|
166
|
+
menu.style.left = `${Math.max(8, Math.min(buttonBox.left, innerWidth - menu.offsetWidth - 8))}px`;
|
|
167
|
+
button.setAttribute("aria-expanded", "true");
|
|
168
|
+
if (menu.id === "bench-add-menu") refresh("phone");
|
|
169
|
+
if (menu.id === "bench-log-menu") refresh("log");
|
|
170
|
+
menu.querySelector(".bm-item")?.focus();
|
|
171
|
+
};
|
|
172
|
+
document.addEventListener("click", (event) => {
|
|
173
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
174
|
+
const menuButton = target?.closest("[data-bench-menu]");
|
|
175
|
+
if (menuButton) return openMenu(menuButton);
|
|
176
|
+
// Playing as someone, from the player menu or the Players panel.
|
|
177
|
+
const player = target?.closest("[data-bench-play-as]");
|
|
178
|
+
if (player) {
|
|
179
|
+
closeMenus();
|
|
180
|
+
return playAs(player.dataset.benchPlayAs);
|
|
181
|
+
}
|
|
182
|
+
// The player menu's All players opens the panel.
|
|
183
|
+
const opener = target?.closest('[data-bench-panel="open"]');
|
|
184
|
+
if (opener) {
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
closeMenus();
|
|
187
|
+
return showPanel(true);
|
|
188
|
+
}
|
|
189
|
+
if (target?.closest(".bm-item")) return setTimeout(closeMenus, 0);
|
|
190
|
+
if (!target?.closest(".bench-menu")) closeMenus();
|
|
191
|
+
});
|
|
192
|
+
addEventListener("keydown", (event) => {
|
|
193
|
+
if (event.key === "Escape") closeMenus();
|
|
194
|
+
});
|
|
195
|
+
if (isPage) {
|
|
196
|
+
drawPanel();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// About once a second: the player is still here, a game file changed (reload the game), and the room they're in (Add a player joins it).
|
|
201
|
+
const check = () =>
|
|
202
|
+
fetch(`/bench/state?player=${playerNumber}`)
|
|
203
|
+
.then((response) => response.json())
|
|
204
|
+
.then((answer) => {
|
|
205
|
+
if (reloadNumber !== null && answer.reloadNumber !== reloadNumber) frame.contentWindow?.location.reload();
|
|
206
|
+
reloadNumber = answer.reloadNumber;
|
|
207
|
+
if (answer.roomCode !== roomCode) {
|
|
208
|
+
roomCode = answer.roomCode;
|
|
209
|
+
// A new tab is a new player (a link to a new tab starts with nothing of this one's), into this room.
|
|
210
|
+
addTab.href = roomCode ? `/?room=${roomCode}` : "/";
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.catch(() => {});
|
|
214
|
+
check();
|
|
215
|
+
setInterval(check, 1000);
|
|
216
|
+
addEventListener("pagehide", () => navigator.sendBeacon(`/bench/leave?player=${playerNumber}`));
|
|
217
|
+
if (new URLSearchParams(location.search).get("panel") === "players") showPanel(true);
|
|
218
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The test bench's own HTTPS certificate (local-play.js uses it with --on-network): self-signed, made on this computer for its own
|
|
2
|
+
// names and addresses, so a phone that asks for HTTPS gets it. No one vouches for it, so the phone warns once before it opens the bench.
|
|
3
|
+
// Written with Node's crypto alone (an EC key, and the certificate's DER put together here), so the package needs no dependency for it.
|
|
4
|
+
import { X509Certificate, createPrivateKey, generateKeyPairSync, randomBytes, sign } from "node:crypto";
|
|
5
|
+
import { isIP } from "node:net";
|
|
6
|
+
|
|
7
|
+
/** How long a certificate lasts: under the 398 days browsers take, and made again before it ends. */
|
|
8
|
+
const CERTIFICATE_DURATION_DAYS = 397;
|
|
9
|
+
const DAY_MS = 24 * 3600 * 1000;
|
|
10
|
+
|
|
11
|
+
// DER, the encoding certificates are written in: a tag, the length, then the bytes.
|
|
12
|
+
function der(tag, content) {
|
|
13
|
+
const contentSizeBytes = content.length;
|
|
14
|
+
const lengthBytes = contentSizeBytes < 128 ? [contentSizeBytes] : contentSizeBytes < 256 ? [0x81, contentSizeBytes] : [0x82, contentSizeBytes >> 8, contentSizeBytes & 0xff];
|
|
15
|
+
return Buffer.concat([Buffer.from([tag, ...lengthBytes]), content]);
|
|
16
|
+
}
|
|
17
|
+
const sequence = (...parts) => der(0x30, Buffer.concat(parts));
|
|
18
|
+
const set = (...parts) => der(0x31, Buffer.concat(parts));
|
|
19
|
+
const explicit = (tagNumber, content) => der(0xa0 + tagNumber, content);
|
|
20
|
+
const octets = (bytes) => der(0x04, bytes);
|
|
21
|
+
const bits = (bytes) => der(0x03, Buffer.concat([Buffer.from([0]), bytes]));
|
|
22
|
+
const utf8 = (text) => der(0x0c, Buffer.from(text, "utf8"));
|
|
23
|
+
function integer(bytes) {
|
|
24
|
+
// A positive integer: a leading zero byte when the first bit is set.
|
|
25
|
+
return der(0x02, bytes[0] & 0x80 ? Buffer.concat([Buffer.from([0]), bytes]) : bytes);
|
|
26
|
+
}
|
|
27
|
+
function objectId(dotted) {
|
|
28
|
+
const [first, second, ...rest] = dotted.split(".").map(Number);
|
|
29
|
+
const bytes = [first * 40 + second];
|
|
30
|
+
for (const part of rest) {
|
|
31
|
+
const groups = [];
|
|
32
|
+
let remaining = part;
|
|
33
|
+
do {
|
|
34
|
+
groups.unshift(remaining & 0x7f);
|
|
35
|
+
remaining = Math.floor(remaining / 128);
|
|
36
|
+
} while (remaining > 0);
|
|
37
|
+
bytes.push(...groups.map((groupNumber, groupIndex) => (groupIndex < groups.length - 1 ? groupNumber | 0x80 : groupNumber)));
|
|
38
|
+
}
|
|
39
|
+
return der(0x06, Buffer.from(bytes));
|
|
40
|
+
}
|
|
41
|
+
function utcTime(atMs) {
|
|
42
|
+
const shown = new Date(atMs).toISOString();
|
|
43
|
+
return der(0x17, Buffer.from(`${shown.slice(2, 4)}${shown.slice(5, 7)}${shown.slice(8, 10)}${shown.slice(11, 13)}${shown.slice(14, 16)}${shown.slice(17, 19)}Z`, "ascii"));
|
|
44
|
+
}
|
|
45
|
+
/** An address as subjectAltName writes it: 4 bytes for IPv4, 16 for IPv6. */
|
|
46
|
+
function addressBytes(address) {
|
|
47
|
+
if (isIP(address) === 4) return Buffer.from(address.split(".").map(Number));
|
|
48
|
+
const halves = address.split("::");
|
|
49
|
+
const groupsOf = (text) => (text ? text.split(":") : []);
|
|
50
|
+
const head = groupsOf(halves[0]);
|
|
51
|
+
const tail = groupsOf(halves[1] ?? "");
|
|
52
|
+
const groups = [...head, ...Array(8 - head.length - tail.length).fill("0"), ...tail];
|
|
53
|
+
return Buffer.from(groups.flatMap((group) => [parseInt(group, 16) >> 8, parseInt(group, 16) & 0xff]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A self-signed certificate for these names and addresses ("localhost", "192.168.1.20"), with its key, as PEM text:
|
|
58
|
+
* { certificatePem, keyPem }. ECDSA P-256 and SHA-256, for server authentication, as phones ask of a server's certificate.
|
|
59
|
+
*/
|
|
60
|
+
export function makeCertificate(names, nowMs = Date.now()) {
|
|
61
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
62
|
+
const name = sequence(set(sequence(objectId("2.5.4.3"), utf8("Zaisma test bench"))));
|
|
63
|
+
const signatureAlgorithm = sequence(objectId("1.2.840.10045.4.3.2"));
|
|
64
|
+
const alternativeNames = sequence(...names.map((each) => (isIP(each) ? der(0x87, addressBytes(each)) : der(0x82, Buffer.from(each, "ascii")))));
|
|
65
|
+
const extensions = explicit(3, sequence(
|
|
66
|
+
sequence(objectId("2.5.29.17"), octets(alternativeNames)),
|
|
67
|
+
// Extended key usage: server authentication.
|
|
68
|
+
sequence(objectId("2.5.29.37"), octets(sequence(objectId("1.3.6.1.5.5.7.3.1")))),
|
|
69
|
+
// Basic constraints: not a certificate authority.
|
|
70
|
+
sequence(objectId("2.5.29.19"), octets(sequence())),
|
|
71
|
+
));
|
|
72
|
+
const serial = randomBytes(16);
|
|
73
|
+
serial[0] &= 0x7f;
|
|
74
|
+
const toBeSigned = sequence(
|
|
75
|
+
explicit(0, integer(Buffer.from([2]))),
|
|
76
|
+
integer(serial),
|
|
77
|
+
signatureAlgorithm,
|
|
78
|
+
name,
|
|
79
|
+
sequence(utcTime(nowMs - DAY_MS), utcTime(nowMs + CERTIFICATE_DURATION_DAYS * DAY_MS)),
|
|
80
|
+
name,
|
|
81
|
+
publicKey.export({ type: "spki", format: "der" }),
|
|
82
|
+
extensions,
|
|
83
|
+
);
|
|
84
|
+
const certificate = sequence(toBeSigned, signatureAlgorithm, bits(sign("sha256", toBeSigned, privateKey)));
|
|
85
|
+
const lines = certificate.toString("base64").match(/.{1,64}/g) ?? [];
|
|
86
|
+
return { certificatePem: `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----\n`, keyPem: privateKey.export({ type: "pkcs8", format: "pem" }) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Whether a kept certificate still serves: it reads, covers every one of these names, and has more than a week left. */
|
|
90
|
+
export function isCertificateGood(kept, names, nowMs = Date.now()) {
|
|
91
|
+
try {
|
|
92
|
+
const certificate = new X509Certificate(kept.certificatePem);
|
|
93
|
+
createPrivateKey(kept.keyPem);
|
|
94
|
+
const covers = (each) => (isIP(each) ? certificate.checkIP(each) : certificate.checkHost(each)) !== undefined;
|
|
95
|
+
return Date.parse(certificate.validTo) - nowMs > 7 * DAY_MS && names.every(covers);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The game server of the local bench (local-play.js), as a process of its own: a change to the game server's files
|
|
2
|
+
// stops this process (every room saves first) and starts a new one, so the new code is read from scratch.
|
|
3
|
+
// Not a command: local-play.js starts it with one JSON argument and hears ZAISMA_LOCAL_RUNNER_READY when it listens.
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { relay } from "../src/internal.js";
|
|
7
|
+
import { localDatabase } from "./node-parts.js";
|
|
8
|
+
import { startServer } from "./node.js";
|
|
9
|
+
|
|
10
|
+
const config = JSON.parse(process.argv[2] ?? "{}");
|
|
11
|
+
try {
|
|
12
|
+
// Said plainly before anything listens: the runner's own listen error would end this process with a stack trace.
|
|
13
|
+
await new Promise((resolve, reject) => {
|
|
14
|
+
const probe = createServer();
|
|
15
|
+
probe.once("error", (error) => reject(error?.code === "EADDRINUSE" ? new Error(`Port ${config.port} is already in use, so your game server can't start. Stop what's using it, or start the bench with another --port.`) : error));
|
|
16
|
+
probe.listen(config.port, () => probe.close(resolve));
|
|
17
|
+
});
|
|
18
|
+
const gameServer = config.entry ? (await import(pathToFileURL(config.entry).href)).default : relay;
|
|
19
|
+
const running = startServer({
|
|
20
|
+
gameServer,
|
|
21
|
+
gameSlug: config.gameSlug,
|
|
22
|
+
port: config.port,
|
|
23
|
+
origins: [config.origin],
|
|
24
|
+
issuerUrl: config.issuerUrl,
|
|
25
|
+
database: localDatabase(config.databaseFile),
|
|
26
|
+
roomStateFile: config.roomStateFile,
|
|
27
|
+
// onPurchase and onRefund hear purchases through the stand-in developer API on the bench's rooms.
|
|
28
|
+
zaismaKey: config.zaismaKey,
|
|
29
|
+
developerApiUrl: config.developerApiUrl,
|
|
30
|
+
});
|
|
31
|
+
let stopping = null;
|
|
32
|
+
const stop = () => (stopping ??= running.close().then(() => process.exit(0)));
|
|
33
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, stop);
|
|
34
|
+
// The bench holds this pipe open while it runs: when it ends, however it ended, this game server stops too (rooms save first).
|
|
35
|
+
process.stdin.once("close", stop);
|
|
36
|
+
process.stdin.resume();
|
|
37
|
+
const isListening = () => console.log("ZAISMA_LOCAL_RUNNER_READY");
|
|
38
|
+
if (running.httpServer.listening) isListening();
|
|
39
|
+
else running.httpServer.once("listening", isListening);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(error instanceof Error ? error.message : error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** What startLocalPlay takes. Only gameFolder is required; the rest have the defaults named here. */
|
|
2
|
+
export interface LocalPlayOptions {
|
|
3
|
+
/** The folder of your game, the one with index.html at the top. Its game server is zaisma-server/index.js or zaisma-server.js, else the relay. */
|
|
4
|
+
gameFolder: string;
|
|
5
|
+
/** The first of four ports: the bench, the game's site, the stand-in rooms and your game server take it and the next three. Default 4800. */
|
|
6
|
+
port?: number;
|
|
7
|
+
/** Listen on every address, so a phone on the same Wi-Fi can open the bench. Default false: this computer only. */
|
|
8
|
+
isOnNetwork?: boolean;
|
|
9
|
+
/** Your game's Zaisma slug. Default: the folder's name. */
|
|
10
|
+
gameSlug?: string | null;
|
|
11
|
+
/** An API key; with gameSlug, each start brings in the game's items from Zaisma. Default: the ZAISMA_KEY environment variable. */
|
|
12
|
+
zaismaKey?: string;
|
|
13
|
+
/** Zaisma's developer API, where that copy is read from. Default: https://zaisma.com/api/dev. */
|
|
14
|
+
developerApiUrl?: string;
|
|
15
|
+
/** Where lines of text go. Default: the terminal. */
|
|
16
|
+
log?: (line: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocalPlay {
|
|
20
|
+
/** The test bench's address, on this computer. */
|
|
21
|
+
benchUrl: string;
|
|
22
|
+
gameSiteUrl: string;
|
|
23
|
+
roomsUrl: string;
|
|
24
|
+
/** The bench's address for a phone on the same Wi-Fi; null without isOnNetwork. */
|
|
25
|
+
networkUrl: string | null;
|
|
26
|
+
/** How many times the game server has been started: more than one after a change to its files. */
|
|
27
|
+
readonly runnerStartCount: number;
|
|
28
|
+
/** Saves every room, then stops everything. */
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_PORT_NUMBER: number;
|
|
33
|
+
export const LOCAL_VERSION_ID: string;
|
|
34
|
+
export const LOCAL_FOLDER: string;
|
|
35
|
+
export const SERVED_ZAISMA_PATHS: string[];
|
|
36
|
+
export const ONLY_ON_ZAISMA_PATHS: string[];
|
|
37
|
+
export function startLocalPlay(options: LocalPlayOptions): Promise<LocalPlay>;
|
|
38
|
+
export function serverEntryOf(gameFolder: string): string | null;
|
|
39
|
+
export function withScripts(page: string, scriptUrls: string[]): string;
|
|
40
|
+
export function localGameHeaders(contentType: string, benchOrigin: string, connectOrigins: string[]): Record<string, string>;
|
|
41
|
+
export function terminalQr(text: string): string;
|