@homie-rocks/studio 0.31.1 → 0.32.1
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/CHANGELOG.md +63 -1
- package/README.md +41 -0
- package/bin/homie-studio.mjs +42 -5
- package/lib/build.mjs +1 -1
- package/lib/doctor.mjs +13 -1
- package/lib/mcp-tools.mjs +5 -2
- package/lib/mcp.mjs +2 -0
- package/lib/standalone-cli.mjs +134 -0
- package/lib/standalone-files.mjs +360 -0
- package/lib/standalone-icons.mjs +106 -0
- package/lib/standalone-lock.json +3404 -0
- package/lib/standalone-pins.json +18 -0
- package/lib/standalone-tools.mjs +81 -0
- package/lib/standalone.mjs +1074 -0
- package/lib/template-history.json +80 -0
- package/netplay/NETPLAY.md +10 -0
- package/netplay/netplay.ts +15 -5
- package/package.json +2 -1
- package/standalone/STANDALONE.md +368 -0
- package/standalone/electron/files.cjs +50 -0
- package/standalone/electron/main.cjs +139 -0
- package/standalone/electron/pack.mjs +82 -0
- package/standalone/shell/index.html +36 -0
- package/standalone/shell/shell.css +46 -0
- package/standalone/shell/shell.js +346 -0
- package/worker/index.mjs +55 -8
- package/worker/standalone.mjs +34 -0
- package/worker/version.mjs +1 -1
|
@@ -1652,6 +1652,86 @@
|
|
|
1652
1652
|
}
|
|
1653
1653
|
}
|
|
1654
1654
|
}
|
|
1655
|
+
},
|
|
1656
|
+
"0.32.0": {
|
|
1657
|
+
"files": {
|
|
1658
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
1659
|
+
"README.md": "684b3cca8001673c",
|
|
1660
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
1661
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
1662
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
1663
|
+
"videos/README.md": "16babc3437d5545b",
|
|
1664
|
+
"posts/README.md": "37841aae476bd540",
|
|
1665
|
+
"site/README.md": "732894273a32dc3f",
|
|
1666
|
+
"site/src/worker.mjs": "ca3f1630f2420ad3"
|
|
1667
|
+
},
|
|
1668
|
+
"sections": {
|
|
1669
|
+
"AGENTS.md": {
|
|
1670
|
+
"(intro)": "214aa46eb3a895d5",
|
|
1671
|
+
"## Layout": "8baf49e4291e7e88",
|
|
1672
|
+
"## Commands (all through the pinned CLI in node_modules)": "29a04ed3d956610a",
|
|
1673
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
1674
|
+
"## Making games": "2da7fa74c8884834",
|
|
1675
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
1676
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
1677
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
1678
|
+
"## Servers and AI seats": "98d08705087a33ec",
|
|
1679
|
+
"## Selling things (the shop)": "b04b0a2b25f524f2",
|
|
1680
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
1681
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
1682
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
1683
|
+
"## Beta": "9d02638207a26e73"
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
"named": {
|
|
1687
|
+
"Homie Arcade": {
|
|
1688
|
+
"sections": {
|
|
1689
|
+
"AGENTS.md": {
|
|
1690
|
+
"## Commands (all through the pinned CLI in node_modules)": "5d1c068adda1116b"
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
"0.32.1": {
|
|
1697
|
+
"files": {
|
|
1698
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
1699
|
+
"README.md": "684b3cca8001673c",
|
|
1700
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
1701
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
1702
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
1703
|
+
"videos/README.md": "16babc3437d5545b",
|
|
1704
|
+
"posts/README.md": "37841aae476bd540",
|
|
1705
|
+
"site/README.md": "732894273a32dc3f",
|
|
1706
|
+
"site/src/worker.mjs": "ca3f1630f2420ad3"
|
|
1707
|
+
},
|
|
1708
|
+
"sections": {
|
|
1709
|
+
"AGENTS.md": {
|
|
1710
|
+
"(intro)": "214aa46eb3a895d5",
|
|
1711
|
+
"## Layout": "8baf49e4291e7e88",
|
|
1712
|
+
"## Commands (all through the pinned CLI in node_modules)": "29a04ed3d956610a",
|
|
1713
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
1714
|
+
"## Making games": "2da7fa74c8884834",
|
|
1715
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
1716
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
1717
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
1718
|
+
"## Servers and AI seats": "98d08705087a33ec",
|
|
1719
|
+
"## Selling things (the shop)": "b04b0a2b25f524f2",
|
|
1720
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
1721
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
1722
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
1723
|
+
"## Beta": "9d02638207a26e73"
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
"named": {
|
|
1727
|
+
"Homie Arcade": {
|
|
1728
|
+
"sections": {
|
|
1729
|
+
"AGENTS.md": {
|
|
1730
|
+
"## Commands (all through the pinned CLI in node_modules)": "5d1c068adda1116b"
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1655
1735
|
}
|
|
1656
1736
|
}
|
|
1657
1737
|
}
|
package/netplay/NETPLAY.md
CHANGED
|
@@ -1662,6 +1662,16 @@ reads the other's snapshot with the wrong code.
|
|
|
1662
1662
|
names no version is one pool, as before. A relay nobody tells which build is live (no Worker in
|
|
1663
1663
|
front) uses the hello's `ver`: two builds still never share a room, and nobody is called stale.
|
|
1664
1664
|
|
|
1665
|
+
**A standalone copy** (a desktop or phone app, `standalone/STANDALONE.md`) is one build for good:
|
|
1666
|
+
it cannot load a newer one, only be updated. So it says its own revision to the Lobby
|
|
1667
|
+
(`/api/lobby?gv=`), which matches it with copies of that revision whatever is live now, and its
|
|
1668
|
+
`HOMIE_NET.app` makes the helper's lines truthful: "A new version is out. Update the app to play
|
|
1669
|
+
online.", and a tap reloads nothing. A copy that knocks at a room of the live revision is refused
|
|
1670
|
+
like any old tab, and plays on by itself. What a copy says of itself is its own word, so the Lobby
|
|
1671
|
+
keeps rooms for at most eight revisions besides the live one (a copy on a ninth plays offline for
|
|
1672
|
+
now), and a helper's `closed` word to its page names the room it is about. Nothing on the wire differs. `homie-studio deploy` says
|
|
1673
|
+
when a deploy makes another revision live than the copies a studio has built.
|
|
1674
|
+
|
|
1665
1675
|
**What a revision cannot see.** Until a game's bundle has a content-hashed
|
|
1666
1676
|
name, the edge may serve the previous `main.js` for up to a minute after a deploy while the page
|
|
1667
1677
|
around it is already stamped with the new version. A tab loaded in that minute says it is the new
|
package/netplay/netplay.ts
CHANGED
|
@@ -139,6 +139,12 @@ export interface NetConfig {
|
|
|
139
139
|
params?: Record<string, string>;
|
|
140
140
|
/** Revision 9: the page around the frame keeps `net.prefs` (a play page from before it does not say so). */
|
|
141
141
|
prefs?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* This game runs in a standalone copy (a desktop or phone app, standalone/STANDALONE.md), whose files are the
|
|
144
|
+
* build it was made from: a reload cannot bring a newer one, only an update of the app can. The lines about a newer
|
|
145
|
+
* version say so, and a tap on one reloads nothing. Nothing on the wire changes.
|
|
146
|
+
*/
|
|
147
|
+
app?: boolean;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
/* ------------------------------------------------- the link, revisions and the page (revision 9, sections 22-24) */
|
|
@@ -1282,6 +1288,8 @@ const HEARTBEAT_MAX_MS = 4000;
|
|
|
1282
1288
|
const RECONNECT_MAX_MS = 20_000;
|
|
1283
1289
|
/** The link overlay: a reconnect shorter than this is never drawn. */
|
|
1284
1290
|
const LINK_OVERLAY_MS = 700;
|
|
1291
|
+
/** What a standalone copy says when a newer build of the game is live: the way to it is the app's own update. */
|
|
1292
|
+
export const APP_STALE_LINE = 'A new version is out. Update the app to play online.';
|
|
1285
1293
|
/** `net.prefs` (section 24): what the play page keeps for one game on one browser. worker/pages.mjs has the same numbers. */
|
|
1286
1294
|
export const PREFS_LIMITS = Object.freeze({ bytes: 16_384, keys: 32, key: 64 });
|
|
1287
1295
|
const PREFS_WAIT_MS = 2000;
|
|
@@ -1678,6 +1686,8 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
1678
1686
|
let link: LinkState = offline ? 'offline' : 'connecting';
|
|
1679
1687
|
/** The line over the game while the link is down: made on first need, in this document, so a game's CSS reaches it. */
|
|
1680
1688
|
const overlayOn = opts.linkOverlay !== false && !offline;
|
|
1689
|
+
/** A standalone copy (HOMIE_NET.app): a newer build is reached by updating the app, never by a reload. */
|
|
1690
|
+
const inApp = cfg?.app === true;
|
|
1681
1691
|
let overlay: HTMLElement | null = null;
|
|
1682
1692
|
let overlayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1683
1693
|
let overlayHide: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -1695,7 +1705,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
1695
1705
|
el.setAttribute('role', 'status');
|
|
1696
1706
|
el.setAttribute('aria-live', 'polite');
|
|
1697
1707
|
el.hidden = true;
|
|
1698
|
-
el.addEventListener('click', () => { if (el.getAttribute('data-homie-link') === 'stale') { try { (globalThis as { location?: Location }).location?.reload(); } catch { /* not a page */ } } });
|
|
1708
|
+
el.addEventListener('click', () => { if (!inApp && el.getAttribute('data-homie-link') === 'stale') { try { (globalThis as { location?: Location }).location?.reload(); } catch { /* not a page */ } } });
|
|
1699
1709
|
doc.body.appendChild(el);
|
|
1700
1710
|
overlay = el;
|
|
1701
1711
|
} catch { overlay = null; }
|
|
@@ -1729,7 +1739,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
1729
1739
|
if (closedWhy === 'room-full' || closedWhy === 'too-many') return offline ? 'This room is full · playing on your own' : 'This room is full.';
|
|
1730
1740
|
if (closedWhy === 'replaced') return 'This game is open in another tab.';
|
|
1731
1741
|
if (closedWhy === 'kicked' || closedWhy === 'room-closed') return 'This room is closed.';
|
|
1732
|
-
if (closedWhy === 'version') return 'This game needs a reload to play online.';
|
|
1742
|
+
if (closedWhy === 'version') return inApp ? APP_STALE_LINE : 'This game needs a reload to play online.';
|
|
1733
1743
|
return null;
|
|
1734
1744
|
}
|
|
1735
1745
|
function paintLink(): void {
|
|
@@ -1740,7 +1750,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
1740
1750
|
// A blip shorter than a blink is never drawn.
|
|
1741
1751
|
overlayTimer = setTimeout(() => { overlayTimer = null; if (link === 'reconnecting') showOverlay('reconnecting', 'Reconnecting…'); }, LINK_OVERLAY_MS);
|
|
1742
1752
|
} else if (link === 'alone') showOverlay('alone', aloneText());
|
|
1743
|
-
else if (link === 'closed' && closedWhy === 'stale') showOverlay('stale', 'This game was updated. Tap to reload.');
|
|
1753
|
+
else if (link === 'closed' && closedWhy === 'stale') showOverlay('stale', inApp ? APP_STALE_LINE : 'This game was updated. Tap to reload.');
|
|
1744
1754
|
else if (closedSays) showOverlay('closed', closedSays);
|
|
1745
1755
|
// In the room: the game's own sentence about where this player stands, else that the room is full.
|
|
1746
1756
|
else if (link === 'online' && gameLine) showOverlay('seat', gameLine);
|
|
@@ -1763,7 +1773,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
1763
1773
|
staleVer = next;
|
|
1764
1774
|
if (!first && !final) return;
|
|
1765
1775
|
post?.({ what: 'stale', ver: next || null, mine: version, final });
|
|
1766
|
-
if (!final) showOverlay('stale', 'A new version is ready. Tap to reload.', 10_000);
|
|
1776
|
+
if (!final) showOverlay('stale', inApp ? APP_STALE_LINE : 'A new version is ready. Tap to reload.', 10_000);
|
|
1767
1777
|
emit('stale', { ver: next || null, mine: version, final });
|
|
1768
1778
|
}
|
|
1769
1779
|
|
|
@@ -2306,7 +2316,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
2306
2316
|
if (FINAL_ERRORS.has(code)) {
|
|
2307
2317
|
closed = true;
|
|
2308
2318
|
closedWhy = code;
|
|
2309
|
-
post?.({ what: 'closed', why: code, ...(typeof m['until'] === 'number' ? { until: m['until'] } : {}), ...(typeof m['message'] === 'string' ? { message: String(m['message']).slice(0, 200) } : {}) });
|
|
2319
|
+
post?.({ what: 'closed', why: code, room: cfg?.room ?? null, ...(typeof m['until'] === 'number' ? { until: m['until'] } : {}), ...(typeof m['message'] === 'string' ? { message: String(m['message']).slice(0, 200) } : {}) });
|
|
2310
2320
|
// Kept out of a room for running an older build (section 23): the page reloads the game; the game may too.
|
|
2311
2321
|
if (code === 'stale') noteStale(m['ver'], true);
|
|
2312
2322
|
setLink('closed', code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"site/",
|
|
79
79
|
"chat/",
|
|
80
80
|
"parts/",
|
|
81
|
+
"standalone/",
|
|
81
82
|
"README.md",
|
|
82
83
|
"CHANGELOG.md",
|
|
83
84
|
"LICENSE",
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Standalone copies of a game
|
|
2
|
+
|
|
3
|
+
`homie-studio standalone` turns a game of your studio into an app of its own:
|
|
4
|
+
|
|
5
|
+
| Target | Made with | What comes out | Where it can go |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| `mac` | Electron | `out/mac/<debug or release>/<Name>.app` | Steam |
|
|
8
|
+
| `windows` | Electron | `out/windows/<debug or release>/<id>.exe` and its files | Steam |
|
|
9
|
+
| `linux` | Electron | `out/linux/<debug or release>/<id>` and its files | Steam |
|
|
10
|
+
| `ios` | Capacitor, Xcode | a simulator `.app`, or an `.ipa` with `--release` | the App Store |
|
|
11
|
+
| `android` | Capacitor, Gradle | a debug `.apk`, or an `.aab` with `--release` | Google Play |
|
|
12
|
+
|
|
13
|
+
Each is **the kind of file that store accepts for upload**. Whether a store takes your game is the store's
|
|
14
|
+
decision: its review, its fees and its rules are its own, and nothing here makes an app pass (see "Before you
|
|
15
|
+
spend money", below).
|
|
16
|
+
|
|
17
|
+
It is the **same web build** of the game, the one `homie-studio build` makes and your site serves, in the
|
|
18
|
+
thinnest shell each platform has. Nothing about one game lives in a shell. Multiplayer still goes through your
|
|
19
|
+
studio's own deployed site: the app asks its Lobby for a room and opens the room's socket, exactly as the play
|
|
20
|
+
page does. With no connection, or when no room answers, the game plays offline with its bots.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx --no-install homie-studio standalone plan <game> # what would be built, what is missing; changes nothing
|
|
24
|
+
npx --no-install homie-studio standalone build <game> # every target this computer can make
|
|
25
|
+
npx --no-install homie-studio standalone build <game> --for mac,windows
|
|
26
|
+
npx --no-install homie-studio standalone run <game> # the built desktop copy, here (--for ios | android)
|
|
27
|
+
npx --no-install homie-studio standalone run <game> --for ios --device # onto the iPhone or iPad plugged in
|
|
28
|
+
npx --no-install homie-studio standalone steam <game> # Steam's build file; uploads nothing
|
|
29
|
+
npx --no-install homie-studio standalone ci <game> # a GitHub workflow that builds all five
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
In chat it is one tool, `game_standalone` (with `plan: true` first).
|
|
33
|
+
|
|
34
|
+
## What the standalone game does not have (v1)
|
|
35
|
+
|
|
36
|
+
Say this to whoever will ship it, before they do.
|
|
37
|
+
|
|
38
|
+
- **Player accounts and sign-in.** The app has no sign-in and no name entry. A player is a guest with a two-word
|
|
39
|
+
handle; an account made on the site is not known to the app.
|
|
40
|
+
- **Cloud saves.** Saves and settings are kept on the device only (the saves helper's local mode). They do not
|
|
41
|
+
follow a player to the site or to another device, and they are **lost when the app is uninstalled**.
|
|
42
|
+
- **The shop.** Nothing is sold in the app. The app stores have their own rules for selling, and none of that is
|
|
43
|
+
built.
|
|
44
|
+
- **Room chat.** Off in the app: it has no report or mute controls yet, and a chat without them is not shipped.
|
|
45
|
+
- **Watching, the big screen, the owner's controls.** The app only plays.
|
|
46
|
+
- **Servers and private doors.** The app plays on the public server through an open door. A game that is private
|
|
47
|
+
or invite-only, or a server behind an account or an invite, plays offline in the app.
|
|
48
|
+
- **Automatic updates.** The app is the build it was made from. A newer version reaches players only when you
|
|
49
|
+
build again and ship the update through Steam or the store.
|
|
50
|
+
- **Steam's overlay, achievements and friends.** None of it has been run. The desktop app passes the switch the
|
|
51
|
+
overlay needs when Steam starts it, but Electron apps often do not show Steam's overlay, and achievements,
|
|
52
|
+
rich presence and friend invites are not wired.
|
|
53
|
+
- **Uploading, and a store's yes.** Builds are made on your computer and the upload steps are printed. Nothing is
|
|
54
|
+
sent to Steam, App Store Connect or Google Play for you, and nothing here makes an app pass a review.
|
|
55
|
+
- **A signed Windows build.** Unsigned in v1: opened outside Steam, Windows shows its unknown-publisher warning.
|
|
56
|
+
|
|
57
|
+
## What has been run and what has not
|
|
58
|
+
|
|
59
|
+
This is version 1, and it says what it knows. By the people who wrote it, on one Mac and one iPhone; each line
|
|
60
|
+
was true on 2026-10-06 unless it says another day:
|
|
61
|
+
|
|
62
|
+
**Run and seen:**
|
|
63
|
+
|
|
64
|
+
- The macOS build (Apple silicon, and the universal one a release makes) started and the game loaded with its
|
|
65
|
+
bots, from its own `app://game` origin, with no file failing to load. Every desktop build for the computer it
|
|
66
|
+
is made on is started once like this, and its result says "started here and loaded the game: yes" or "NO".
|
|
67
|
+
- That build, pointed at a studio's local dev server, got a room from the Lobby and opened the room's socket.
|
|
68
|
+
- A macOS build signed ad hoc with the hardened runtime and the Steam entitlements started and loaded the game.
|
|
69
|
+
- The iOS simulator build ran in the iOS Simulator; the Android debug build ran in the Android emulator. Both
|
|
70
|
+
offline, with their bots.
|
|
71
|
+
- An Android release bundle was signed by Gradle with a test key (the key's passwords given through the
|
|
72
|
+
environment), and the bundle carries that key's certificate.
|
|
73
|
+
- The Windows build's program carries the game's icon and its name; the Linux build is a Linux program. Both
|
|
74
|
+
were made on the Mac.
|
|
75
|
+
- **On a real iPhone (2026-10-07).** A 3D game's iOS build was signed for a development team, installed over
|
|
76
|
+
the cable and started by hand, with the same three tools `standalone run --for ios --device` now runs. It
|
|
77
|
+
loaded and played offline with its bots.
|
|
78
|
+
- **Online from that phone (2026-10-07).** A private room made in the app, joined from another device by its
|
|
79
|
+
invite link, connected through a studio's live Worker. That site was on 0.31.0, from before standalone copies.
|
|
80
|
+
- **`standalone run --for ios --device` itself (2026-10-07).** It found the phone, read the team from the
|
|
81
|
+
keychain, built and signed the app for the phone and installed it; the phone's own list of apps had it. The
|
|
82
|
+
phone was locked, so the command waited a minute, said so, and stopped with "the game is on the phone, and
|
|
83
|
+
the phone is locked". Its last two steps, starting the game and finding it among the phone's running programs,
|
|
84
|
+
have been run against stand-ins only.
|
|
85
|
+
|
|
86
|
+
**Never run:**
|
|
87
|
+
|
|
88
|
+
- **The Windows and Linux builds have never been started**, on any computer. On Ubuntu 24.04 and later a program
|
|
89
|
+
in a plain folder like `out/linux/` may refuse to start outside Steam until its `chrome-sandbox` file is owned
|
|
90
|
+
by root with mode 4755, or it is started with `--no-sandbox`; an installer normally does the first, and there
|
|
91
|
+
is none.
|
|
92
|
+
- **Nothing has been started by Steam.** Not the overlay, not the Linux build inside Steam's runtime, not the
|
|
93
|
+
Steam API binding. `steam/app_build.vdf` is written from Steam's documented format and has never been run
|
|
94
|
+
through `steamcmd`.
|
|
95
|
+
- **A macOS app signed with a Developer ID, and notarization.** The code asks the signer and the notary and then
|
|
96
|
+
checks the result (`codesign --verify`, `xcrun stapler validate`); it says "notarized" only when the second
|
|
97
|
+
agrees. That path has not been run with a real identity. Opening a notarized app on another Mac was not tried.
|
|
98
|
+
- **The iOS archive and its `.ipa`.** It needs Xcode signed in to an Apple account of your team; it was not run.
|
|
99
|
+
- **Quick play online from a phone.** The one site a phone build has met was older than 0.32.0, where an app
|
|
100
|
+
cannot read the Lobby's answer: Quick play was offline there, as it should be.
|
|
101
|
+
- **A real Android phone.** The Android build has run in the emulator only. `standalone run --for android` hands
|
|
102
|
+
over to Capacitor, which asks which phone or emulator; `--device` is for an iPhone or iPad.
|
|
103
|
+
- **`run --device` starting a game on an unlocked phone**, and each of its answers for a phone that is not
|
|
104
|
+
ready (Developer Mode off, not trusted, no Apple account in Xcode), other than against stand-ins. The words
|
|
105
|
+
it matches for those are the ones Xcode printed on 2026-10-07.
|
|
106
|
+
- **The GitHub workflow.** It is checked as text. Read the first run's log.
|
|
107
|
+
- **A copy meeting a deployed site**, and an old copy meeting a newer site, outside the tests.
|
|
108
|
+
- **Building from Windows or Linux.**
|
|
109
|
+
|
|
110
|
+
When one of these fails for you, that is news to us: tell Homie.
|
|
111
|
+
|
|
112
|
+
## Before Quick play can find a room
|
|
113
|
+
|
|
114
|
+
For **Quick play**, your studio must be on `@homie-rocks/studio` 0.32.0 or later **and deployed**. The app's
|
|
115
|
+
page is not served by your site, so it is another origin to your Worker; 0.32.0 is the first Worker that lets an
|
|
116
|
+
app read the Lobby's answer, which is how Quick play learns which room to join. With an older site, Quick play
|
|
117
|
+
is offline with the game's bots.
|
|
118
|
+
|
|
119
|
+
**A room made or joined by its code does not need that.** The room's own socket never asked where a page came
|
|
120
|
+
from, so "New private room" and "Join a room" connect through an older site too. That has been seen against a
|
|
121
|
+
site on 0.31.0; it is not promised for every older version.
|
|
122
|
+
|
|
123
|
+
The site's address is **built into every copy** (`--site <address>`, else `HOMIE_STANDALONE_SITE`, else the
|
|
124
|
+
address your last deploy from this computer got). A copy that has shipped keeps it for good, so put the studio
|
|
125
|
+
on its own domain (`studio.json` `cloudflare.domain`) before you ship: a workers.dev address belongs to the
|
|
126
|
+
Cloudflare account and changes if the Worker is renamed.
|
|
127
|
+
|
|
128
|
+
A game that loads something from another site at run time (a web font, a picture, an API) still needs a
|
|
129
|
+
connection for that in the app, exactly as on the web: bundle what the game must have offline. And a game whose
|
|
130
|
+
`index.html` carries a Content-Security-Policy that forbids inline scripts never hears the shell (a copy adds
|
|
131
|
+
one inline script to that page) and plays offline only; the plan warns when it sees one.
|
|
132
|
+
|
|
133
|
+
## On your own iPhone or iPad
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
npx --no-install homie-studio standalone run <game> --for ios --device
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
builds the game for the **one** iPhone or iPad plugged in to this Mac, signs it for your Apple team, installs it
|
|
140
|
+
over the cable, starts it, and then looks for it among the phone's running programs. It says the game is running
|
|
141
|
+
only when it found it there.
|
|
142
|
+
|
|
143
|
+
**`--device` is your yes to what this changes outside your computer:** it adds the phone to your Apple team's
|
|
144
|
+
list of development devices (a team may register a limited number a year, and a device stays on the list until
|
|
145
|
+
the membership year renews) and lets Xcode make a development profile for the app. The app is signed for that
|
|
146
|
+
phone only. It is a build to try, never one for the store, and it stops opening when its profile ends (a week
|
|
147
|
+
with a free Apple account, a year with a Developer membership).
|
|
148
|
+
|
|
149
|
+
What it needs, and what it says when one is missing:
|
|
150
|
+
|
|
151
|
+
- **A Mac with Xcode, signed in to your Apple account** (Xcode, Settings, Accounts). Not signed in: it says so.
|
|
152
|
+
- **Your team.** `HOMIE_APPLE_TEAM`, else the one team among the keychain's development identities (it says
|
|
153
|
+
which it used, never an identity's name). None, or more than one: it stops and asks for `HOMIE_APPLE_TEAM`.
|
|
154
|
+
- **One phone, plugged in, unlocked, trusting this Mac.** None, or several: it says which models it sees and
|
|
155
|
+
builds nothing. Locked: it waits a minute for you to unlock it. Not trusted: unlock it and tap Trust.
|
|
156
|
+
- **Developer Mode on**: on the phone, Settings, Privacy & Security, Developer Mode. The phone restarts and asks
|
|
157
|
+
once more. The switch appears only after the phone has been plugged in to a Mac with Xcode.
|
|
158
|
+
|
|
159
|
+
Nothing it prints or returns names the phone, its id or your team's id: a phone is said by its model.
|
|
160
|
+
|
|
161
|
+
For an Android phone there is no `--device` yet: `standalone run <game> --for android` hands over to Capacitor,
|
|
162
|
+
with the JDK and SDK the build uses, and Capacitor asks which phone or emulator. That has been run on an
|
|
163
|
+
emulator, never on a real Android phone.
|
|
164
|
+
|
|
165
|
+
## Versions: an old copy and a new site
|
|
166
|
+
|
|
167
|
+
Name the game's revision in `game.json` (`"netplay": { "version": "1" }`, NETPLAY.md section 23) before you
|
|
168
|
+
ship, and raise it with every change an older copy cannot play with. Then:
|
|
169
|
+
|
|
170
|
+
- a copy asks the Lobby for a room of **its own** revision, so copies of one revision keep playing with each
|
|
171
|
+
other, whatever the site runs now. The Lobby keeps rooms for the live revision and at most eight others at a
|
|
172
|
+
time; a copy on a ninth is told to play offline for now;
|
|
173
|
+
- a copy that knocks at a room on a newer revision (a friend's invite link) is refused, plays on by itself, and
|
|
174
|
+
says so: "Update <Name> to play online with everyone." It never reloads: only an update of the app brings a
|
|
175
|
+
newer build. From then on it shows that line in place of Invite, because its invite would send a friend's
|
|
176
|
+
browser to the newer version, which the copy cannot join;
|
|
177
|
+
- `homie-studio deploy` (and `deploy --plan`) says when a deploy makes a different revision live than the copies
|
|
178
|
+
you built, and what that means for them.
|
|
179
|
+
|
|
180
|
+
## game.json
|
|
181
|
+
|
|
182
|
+
Everything is optional until a release:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
"standalone": {
|
|
186
|
+
"appId": "com.example.gemrush",
|
|
187
|
+
"version": "1.0.0",
|
|
188
|
+
"build": 1,
|
|
189
|
+
"orientation": "landscape",
|
|
190
|
+
"icon": "icon.png",
|
|
191
|
+
"steam": { "app": 0, "depots": { "windows": 0, "mac": 0, "linux": 0 } }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `appId`: the app's id in both stores (letters and digits in dotted parts, each starting with a letter). With
|
|
196
|
+
none, a build to try uses a made-up one: your domain reversed, else `rocks.homie.<studio>`, then the game's id.
|
|
197
|
+
**`--release` refuses until you write one**: a store never lets it change.
|
|
198
|
+
- `version`, `build`: what a player sees and the store's build number (`--build <n>` overrides the number).
|
|
199
|
+
- `orientation`: `landscape`, `portrait` or `any` (phones).
|
|
200
|
+
- `icon`: a picture in the game's folder. Without it: `games/<id>/icon.png`, else the cover cropped square, else
|
|
201
|
+
a tile with the game's first letter. Stores want a real square 1024 by 1024 picture.
|
|
202
|
+
- `steam`: your App ID and one Depot ID for each operating system, for `standalone steam`.
|
|
203
|
+
|
|
204
|
+
The app's name is the game's name; its publisher is the studio's. A name with a character a file's name cannot
|
|
205
|
+
have (`: / \ " < > | ? *`) is still the app's name on phones and in the window's title; the desktop app's file,
|
|
206
|
+
and what macOS shows in its menu bar and Dock, has a `-` in its place. Every path the command reports is where
|
|
207
|
+
the file is.
|
|
208
|
+
|
|
209
|
+
## Where everything is
|
|
210
|
+
|
|
211
|
+
All of it is in `.studio/standalone/<id>/`, which is git-ignored. Never edit it; never commit it.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
web/ the shell's page (index.html, shell.js, shell.css), config.js, and game/ (the built game)
|
|
215
|
+
electron/ main.cjs, files.cjs and app.json, with a copy of web/
|
|
216
|
+
ios/ android/ the native projects Capacitor makes
|
|
217
|
+
icons/ assets/ the icon in every size
|
|
218
|
+
steam/ app_build.vdf
|
|
219
|
+
out/<target>/debug/ out/<target>/release/ what was built; a debug build never replaces a release
|
|
220
|
+
built.json for each target, what its debug and its release copy were built from
|
|
221
|
+
node_modules/ package-lock.json the wrapper tools
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Made again by every build:** `web/`, `electron/`, the icons, the config files, and the `out/` folder of what
|
|
225
|
+
is being built. **Kept between builds:** `node_modules/` (installed on the first build, a few hundred MB, and
|
|
226
|
+
again only when a pin changed or an install did not finish), `ios/` and `android/` (made by Capacitor once, and
|
|
227
|
+
again when the app's id or its name changed; their versions, orientation and icons are set on every build), and
|
|
228
|
+
the other kind's `out/` folder.
|
|
229
|
+
|
|
230
|
+
The wrapper tools are pinned twice: each tool at one exact version (`lib/standalone-pins.json`), and the whole
|
|
231
|
+
tree under them at exact versions with checksums (`lib/standalone-lock.json`, copied in as the project's
|
|
232
|
+
lockfile). A studio that never makes a standalone copy downloads none of them. When a file of theirs is
|
|
233
|
+
missing, the build installs them again, once, and says so.
|
|
234
|
+
|
|
235
|
+
`web/game/` is your site's build byte for byte, but for one script added to its `index.html`, which hands the
|
|
236
|
+
netplay helper the shell's word as your Worker does on the site.
|
|
237
|
+
|
|
238
|
+
## What each target needs
|
|
239
|
+
|
|
240
|
+
`standalone plan` checks this computer and says, for each target, whether it can be built here. A target whose
|
|
241
|
+
tool is missing is **skipped** with its fix; the others still build. Nothing is installed for you. A row says
|
|
242
|
+
"ok" only for what was looked at: a name in the environment is said as "set, checked later", not as working.
|
|
243
|
+
|
|
244
|
+
- **mac**: a Mac.
|
|
245
|
+
- **windows**, **linux**: any computer (a Windows build from a Mac needs no Wine).
|
|
246
|
+
- **ios**: a Mac with Xcode and its iOS platform.
|
|
247
|
+
- **android**: the Android SDK with the Android 36 platform (Android Studio installs both) and JDK 21 (22 to 24
|
|
248
|
+
also work; Android Studio's own JDK is used when the one on your PATH does not fit, for every step).
|
|
249
|
+
|
|
250
|
+
A computer that cannot make a target can still get it: `standalone ci <game>` writes
|
|
251
|
+
`.github/workflows/standalone.yml`, which builds each target on GitHub's own Windows, Linux and macOS machines
|
|
252
|
+
when you start it by hand. Set the repository variable `HOMIE_SITE` to your studio's address first. Its macOS
|
|
253
|
+
and iOS jobs never make a release (it has no Apple certificate): the macOS app it makes is unsigned and the iOS
|
|
254
|
+
one is a simulator build.
|
|
255
|
+
|
|
256
|
+
## What a build's result means
|
|
257
|
+
|
|
258
|
+
- **built**: the tool finished and the file is at the path given. The desktop build for this computer's own
|
|
259
|
+
system was also started once: "started here and loaded the game: yes" or "NO". Every other target says
|
|
260
|
+
"built, not started on this computer". Built is not the same as works.
|
|
261
|
+
- **skipped**: a tool is missing; the fix is printed.
|
|
262
|
+
- **failed**: in the tool's own words.
|
|
263
|
+
- **UNSIGNED**: a release that was built and could not be signed (or signed and not notarized). No store takes
|
|
264
|
+
it, and the command does not report success.
|
|
265
|
+
|
|
266
|
+
The command succeeds only when something was built, nothing failed, no release came out unsigned, and every
|
|
267
|
+
target you named with `--for` was built.
|
|
268
|
+
|
|
269
|
+
## A release, and signing
|
|
270
|
+
|
|
271
|
+
`--release` makes the file a store or Steam accepts for upload. Signing reads **environment variables only**:
|
|
272
|
+
never a file in the studio, never the chat. Only the one tool that signs is given them; npm, Capacitor and
|
|
273
|
+
every script they run are not.
|
|
274
|
+
|
|
275
|
+
| Variable | For |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| `HOMIE_APPLE_IDENTITY` | macOS: the name of your "Developer ID Application" identity. The app is signed with the hardened runtime |
|
|
278
|
+
| `HOMIE_APPLE_NOTARY_PROFILE` | macOS: a notarytool keychain profile. Make it yourself, in your own terminal: `xcrun notarytool store-credentials "homie-notary"` |
|
|
279
|
+
| `HOMIE_APPLE_TEAM` | iOS: your team id. Xcode, signed in to an Apple account of that team, signs the archive and exports an `.ipa` |
|
|
280
|
+
| `HOMIE_ANDROID_KEYSTORE`, `HOMIE_ANDROID_KEYSTORE_PASSWORD`, `HOMIE_ANDROID_KEY_ALIAS`, `HOMIE_ANDROID_KEY_PASSWORD` | Android: your upload key. The passwords reach Gradle through its environment, never a command line |
|
|
281
|
+
|
|
282
|
+
Set them where your AI's commands run, never in the chat: in a terminal, `export NAME=…` and start your AI from
|
|
283
|
+
that terminal; for a desktop app on a Mac, `launchctl setenv NAME …` in Terminal and then open the app again; in
|
|
284
|
+
the GitHub workflow they are repository secrets.
|
|
285
|
+
|
|
286
|
+
A macOS release without the identity is built and marked UNSIGNED. Signed without the profile it is marked NOT
|
|
287
|
+
NOTARIZED. With both, it is called notarized only when `xcrun stapler validate` confirms the ticket on the
|
|
288
|
+
finished app. An Android release without all four values is built and marked UNSIGNED. A macOS game with a
|
|
289
|
+
Steam app is signed with entitlements that let the Steam client load its overlay library; any other is signed
|
|
290
|
+
with the signer's own defaults.
|
|
291
|
+
|
|
292
|
+
## Before you spend money
|
|
293
|
+
|
|
294
|
+
What a store asks of you changes; these were true when this was written (2026-10-06), so check each store's own
|
|
295
|
+
page before you pay.
|
|
296
|
+
|
|
297
|
+
- **Apple**: the Apple Developer Program is a yearly membership (US$99 a year), needed to sign for iOS and to
|
|
298
|
+
notarize for macOS. Apple reviews every app, and its guidelines let it reject one it judges to be a web site
|
|
299
|
+
in a wrapper. A game whose files are all in the app and that plays offline is a better case than a page that
|
|
300
|
+
loads from the web, and nothing guarantees approval.
|
|
301
|
+
- **Google Play**: a one-time registration fee (US$25). A personal account created since late 2023 must run a
|
|
302
|
+
closed test with a number of testers for two weeks before it may publish to everyone.
|
|
303
|
+
- **Steam**: Steam Direct's fee is paid for each app (US$100, returned after the app earns a set amount), and
|
|
304
|
+
Steam makes a new partner and a new app wait (about thirty days after paying, and a store page live for about
|
|
305
|
+
two weeks) before release. Steam asks macOS builds to be signed and notarized.
|
|
306
|
+
|
|
307
|
+
## Steam
|
|
308
|
+
|
|
309
|
+
`standalone steam <game>` writes `steam/app_build.vdf` from your numbers, with each depot inside it pointing at
|
|
310
|
+
that system's release build (`out/<os>/release/`), and prints the one `steamcmd` command to run yourself. With
|
|
311
|
+
no numbers yet it says what to create in Steamworks, and the launch options to set there: `<id>.exe`,
|
|
312
|
+
`<Name>.app`, `<id>`. **For the Linux one, put `--no-sandbox` in its arguments**: inside Steam's Linux runtime
|
|
313
|
+
the browser engine's own sandbox cannot start, and the app does not turn it off by itself.
|
|
314
|
+
|
|
315
|
+
When Steam starts the game (Steam sets `SteamAppId` for what it starts), the app passes the one switch Steam's
|
|
316
|
+
overlay needs to draw (`in-process-gpu`). That is all it does for the overlay, it has not been run under Steam,
|
|
317
|
+
and many Electron apps show no overlay at all. `standalone run` puts `steam_appid.txt` beside the app it starts
|
|
318
|
+
for development, never in `out/`.
|
|
319
|
+
|
|
320
|
+
Steam's own API is off unless you ask: `"steam": { "api": true }` and `HOMIE_STEAMWORKS_SDK=<the SDK folder you
|
|
321
|
+
downloaded from Steamworks>`. The binding (`steamworks-ffi-node`) is then installed into the desktop app and
|
|
322
|
+
Steam's libraries are copied from your SDK. It has never been run. The app's log says whether it started
|
|
323
|
+
("the Steamworks binding started", or that it did not and why), and the game runs either way. No achievements
|
|
324
|
+
are wired in v1.
|
|
325
|
+
|
|
326
|
+
## The shells, briefly
|
|
327
|
+
|
|
328
|
+
- **Desktop** (`standalone/electron/main.cjs`): one window on `app://game/`, served from the app's own `web/`
|
|
329
|
+
folder and nothing else (`files.cjs` decides which file an address names; a test runs it with hostile
|
|
330
|
+
addresses). No Node in the page, no preload, sandboxed; a link opens in the person's browser; the page may
|
|
331
|
+
only lock the pointer, go full screen and copy an invite link. Its storage and its single-instance lock are
|
|
332
|
+
kept under the app's id, so two games never share them. A shipped copy has no developer tools and, on macOS, a
|
|
333
|
+
menu of its own (the app, Edit, Window). F11 or Alt+Enter toggles full screen. `--smoke` is its self-check: it
|
|
334
|
+
loads, waits, and prints one line of JSON.
|
|
335
|
+
- **Phones**: Capacitor's stock shell with the web folder bundled (`capacitor://localhost` on iOS,
|
|
336
|
+
`https://localhost` on Android). Never `server.url`: the app does not load its page from a site.
|
|
337
|
+
- **The page** (`standalone/shell/`): the room button and its sheet, as on the play page. Invite shares the
|
|
338
|
+
room's address on your site, so a friend in a browser joins the same room; Join a room takes a code; New
|
|
339
|
+
private room makes one; Quick play asks the Lobby. When the Lobby has not answered in a second and a half the
|
|
340
|
+
game starts offline, and a room that arrives later is offered, never switched to. A seat is remembered across
|
|
341
|
+
restarts of the app.
|
|
342
|
+
|
|
343
|
+
## What the game's own code can reach
|
|
344
|
+
|
|
345
|
+
On the web, your Worker serves a game into a sandboxed frame that cannot touch the page around it. In an app the
|
|
346
|
+
game's frame is the shell's own origin (that is what lets its saves work), so **the game's code is trusted with
|
|
347
|
+
everything the app's page has**:
|
|
348
|
+
|
|
349
|
+
- its own storage (saves, settings, the room key), and the shell's page around it;
|
|
350
|
+
- on phones, **Capacitor's native bridge**. The shell uses no plugin, but the bridge is in every frame of the
|
|
351
|
+
app, and through it code can ask the native side for things a web page cannot: requests that skip the
|
|
352
|
+
browser's cross-origin rules, the native cookie store, and whatever plugins a future version adds.
|
|
353
|
+
|
|
354
|
+
This is your own game's code, so it is yours to trust. It matters when a game runs **somebody else's** code: an
|
|
355
|
+
ad or analytics script, a script loaded from another site, a part you did not read. In the app that code has
|
|
356
|
+
the same reach. Do not load scripts from the network in a game you ship as an app.
|
|
357
|
+
|
|
358
|
+
## What the Worker allows an app
|
|
359
|
+
|
|
360
|
+
One thing: `POST /<game>/api/lobby` from `app://game`, `capacitor://localhost` or `https://localhost` gets an
|
|
361
|
+
answer the app's page may read, whatever the answer is (`worker/standalone.mjs`). No credentials cross, the
|
|
362
|
+
Lobby is never opened to every origin, an app may only ask for the public server, and every POST that must come
|
|
363
|
+
from your own site's pages (players, saves, the shop, chat reports, the office) still refuses an app. That is
|
|
364
|
+
why an app has no account, no cloud saves and no shop.
|
|
365
|
+
|
|
366
|
+
An app says which revision it is, and anybody can claim to be an app. So the Lobby keeps rooms for at most
|
|
367
|
+
eight revisions besides the live one, never gives a server more rooms than it may have whatever revision asks,
|
|
368
|
+
and keeps at most 512 rooms for a game in all (rooms nobody is seated in make way first).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Which file a request to the desktop shell names, and which bytes of it (standalone/STANDALONE.md). Nothing here
|
|
3
|
+
* needs Electron, so a test runs it with hostile addresses: main.cjs serves exactly what these two say.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
/** The file an `app://game/…` address names inside `web`, or null: another host, a path that climbs out, or nothing there. */
|
|
10
|
+
function fileFor(web, address) {
|
|
11
|
+
let url;
|
|
12
|
+
try { url = new URL(address); } catch { return null; }
|
|
13
|
+
if (url.protocol !== 'app:' || url.host !== 'game') return null;
|
|
14
|
+
let rel;
|
|
15
|
+
try { rel = decodeURIComponent(url.pathname); } catch { return null; }
|
|
16
|
+
if (rel.includes('\0') || rel.includes('\\')) return null;
|
|
17
|
+
const abs = path.resolve(web, `.${path.posix.normalize(`/${rel}`)}`);
|
|
18
|
+
if (abs !== web && !abs.startsWith(web + path.sep)) return null;
|
|
19
|
+
for (const f of [abs, path.join(abs, 'index.html')]) {
|
|
20
|
+
try {
|
|
21
|
+
// A link inside web/ that points out of it is not a file of the game.
|
|
22
|
+
const real = fs.realpathSync(f);
|
|
23
|
+
if (fs.statSync(real).isFile() && (real === fs.realpathSync(web) || real.startsWith(fs.realpathSync(web) + path.sep))) return f;
|
|
24
|
+
} catch { /* next */ }
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The part of a file of `size` bytes a Range header asks for: { start, end } (both inside the file), 'whole' when
|
|
31
|
+
* there is no usable header (the whole file is the answer), or 'none' when the range lies outside the file (416).
|
|
32
|
+
*/
|
|
33
|
+
function rangeOf(header, size) {
|
|
34
|
+
const m = /^bytes=(\d{0,15})-(\d{0,15})$/.exec(String(header || '').trim());
|
|
35
|
+
if (!m || (m[1] === '' && m[2] === '')) return 'whole';
|
|
36
|
+
if (!(size > 0)) return 'none';
|
|
37
|
+
if (m[1] === '') { const n = Number(m[2]); return n > 0 ? { start: Math.max(0, size - n), end: size - 1 } : 'none'; }
|
|
38
|
+
const start = Number(m[1]);
|
|
39
|
+
const end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
40
|
+
return start > end || start >= size ? 'none' : { start, end };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Where a frame of the page may go: the app's own files; and for a frame inside the game, a blob or a srcdoc of its own. */
|
|
44
|
+
function mayNavigate(address, { main = true } = {}) {
|
|
45
|
+
const url = String(address);
|
|
46
|
+
if (url.startsWith('app://game/')) return true;
|
|
47
|
+
return !main && (url === 'about:srcdoc' || url === 'about:blank' || url.startsWith('blob:app://game/'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { fileFor, rangeOf, mayNavigate };
|