@onceworlds/cli 0.9.4 → 0.9.5
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 +7 -5
- package/dist/cli.js +799 -122
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
|
-
import { createHash as
|
|
6
|
-
import { existsSync as
|
|
5
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6
|
+
import { existsSync as existsSync9, watch } from "node:fs";
|
|
7
7
|
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
|
|
8
|
-
import { basename as
|
|
8
|
+
import { basename as basename5, dirname as dirname9, join as join13, resolve as resolve10, sep as sep3 } from "node:path";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
|
|
11
11
|
// src/check.ts
|
|
@@ -1151,6 +1151,9 @@ function missingControls(listed, wanted) {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
1153
|
// src/glyphs.ts
|
|
1154
|
+
function unionCoverage(parts, unchecked = []) {
|
|
1155
|
+
return { has: (code) => parts.some((p) => p.coverage.has(code)), names: parts.map((p) => p.name), unchecked };
|
|
1156
|
+
}
|
|
1154
1157
|
var SORA_RANGES = "U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD,U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF";
|
|
1155
1158
|
function rangeCoverage(ranges = SORA_RANGES) {
|
|
1156
1159
|
const spans = ranges.split(",").map((part) => {
|
|
@@ -1174,9 +1177,12 @@ function missingGlyphs(texts, font, max = 12) {
|
|
|
1174
1177
|
}
|
|
1175
1178
|
return out;
|
|
1176
1179
|
}
|
|
1177
|
-
function glyphNote(missing) {
|
|
1180
|
+
function glyphNote(missing, fonts = {}) {
|
|
1178
1181
|
const codes = missing.map((ch) => `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`);
|
|
1179
|
-
|
|
1182
|
+
const names = fonts.names && fonts.names.length > 0 ? fonts.names : ["Sora"];
|
|
1183
|
+
const which = names.length > 1 ? `its fonts (${names.join(", ")})` : names[0] === "Sora" ? "the engine's font (Sora)" : `its font (${names[0]})`;
|
|
1184
|
+
const unchecked = fonts.unchecked && fonts.unchecked.length > 0 ? ` (${fonts.unchecked.join(", ")} could not be had to look in)` : "";
|
|
1185
|
+
return `text uses characters ${which} can't draw: ${missing.join(" ")} (${codes.join(", ")})${unchecked}. A browser draws them in whatever font the device has, so they look different on every phone or come out as boxes (shot draws boxes). Draw them as shapes (Shape2D, ui.draw.circle and polygon, UI.custom), or load a font that has them (loadFont, the theme's font) and draw the text in it`;
|
|
1180
1186
|
}
|
|
1181
1187
|
|
|
1182
1188
|
// src/check.ts
|
|
@@ -1641,8 +1647,9 @@ async function checkEngineGame(options) {
|
|
|
1641
1647
|
);
|
|
1642
1648
|
}
|
|
1643
1649
|
lines.push(...budgetLines(smoke.budget));
|
|
1644
|
-
const
|
|
1645
|
-
|
|
1650
|
+
const coverage2 = options.font ?? rangeCoverage();
|
|
1651
|
+
const missing = smoke.budget?.chars ? missingGlyphs([smoke.budget.chars], coverage2) : [];
|
|
1652
|
+
if (missing.length > 0) lines.push(`! The ${glyphNote(missing, coverage2)}.`);
|
|
1646
1653
|
if (smoke.dump) lines.push("\xB7 The end of the first match (game.debug.text()):", ...smoke.dump.split("\n").slice(0, 24).map((l) => ` ${l}`));
|
|
1647
1654
|
const problems = smoke.problems ?? [];
|
|
1648
1655
|
if (problems.length > 0) {
|
|
@@ -2607,8 +2614,8 @@ function gitignoreText() {
|
|
|
2607
2614
|
}
|
|
2608
2615
|
function missingIgnores(existing) {
|
|
2609
2616
|
const norm = (line2) => line2.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
2610
|
-
const
|
|
2611
|
-
return IGNORED.filter(({ line: line2, needed }) => needed && !
|
|
2617
|
+
const have2 = new Set(existing.split(/\r?\n/).map(norm).filter((line2) => line2 && !line2.startsWith("#")));
|
|
2618
|
+
return IGNORED.filter(({ line: line2, needed }) => needed && !have2.has(norm(line2))).map(({ line: line2 }) => line2);
|
|
2612
2619
|
}
|
|
2613
2620
|
|
|
2614
2621
|
// src/init.ts
|
|
@@ -2751,7 +2758,7 @@ import { fileURLToPath } from "node:url";
|
|
|
2751
2758
|
// ../engine/package.json
|
|
2752
2759
|
var package_default = {
|
|
2753
2760
|
name: "@onceworlds/engine",
|
|
2754
|
-
version: "1.
|
|
2761
|
+
version: "1.4.0",
|
|
2755
2762
|
description: "The Onceworlds game engine: entities, components, systems, fixed-step time, and the platform's rooms, matches and saves built in.",
|
|
2756
2763
|
homepage: "https://onceworlds.com/docs",
|
|
2757
2764
|
type: "module",
|
|
@@ -3381,7 +3388,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
3381
3388
|
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "node:fs";
|
|
3382
3389
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
3383
3390
|
import { dirname as dirname5, join as join5 } from "node:path";
|
|
3384
|
-
var sleep = (ms) => new Promise((
|
|
3391
|
+
var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
3385
3392
|
var ARROWS = { ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40 };
|
|
3386
3393
|
function keyInfo(key) {
|
|
3387
3394
|
if (key in ARROWS) return { key, code: key, windowsVirtualKeyCode: ARROWS[key] };
|
|
@@ -3475,8 +3482,8 @@ async function launch(options) {
|
|
|
3475
3482
|
throw new Error(`The browser (${binary}) did not start.`);
|
|
3476
3483
|
}
|
|
3477
3484
|
const ws = new WebSocket(version.webSocketDebuggerUrl);
|
|
3478
|
-
await new Promise((
|
|
3479
|
-
ws.onopen = () =>
|
|
3485
|
+
await new Promise((resolve11, reject) => {
|
|
3486
|
+
ws.onopen = () => resolve11();
|
|
3480
3487
|
ws.onerror = () => reject(new Error("Could not talk to the browser."));
|
|
3481
3488
|
});
|
|
3482
3489
|
let nextId = 1;
|
|
@@ -3497,10 +3504,10 @@ async function launch(options) {
|
|
|
3497
3504
|
for (const waiting of pending.values()) waiting.reject(new Error("the browser is gone"));
|
|
3498
3505
|
pending.clear();
|
|
3499
3506
|
};
|
|
3500
|
-
const send2 = (method, params = {}, sessionId) => new Promise((
|
|
3507
|
+
const send2 = (method, params = {}, sessionId) => new Promise((resolve11, reject) => {
|
|
3501
3508
|
const id = nextId++;
|
|
3502
3509
|
const timer = setTimeout(() => (pending.delete(id), reject(new Error(`${method}: the browser did not answer in 90 s`))), 9e4);
|
|
3503
|
-
pending.set(id, { resolve: (value) => (clearTimeout(timer),
|
|
3510
|
+
pending.set(id, { resolve: (value) => (clearTimeout(timer), resolve11(value)), reject: (error) => (clearTimeout(timer), reject(error)) });
|
|
3504
3511
|
if (ws.readyState !== 1) return pending.get(id).reject(new Error(`${method}: the browser is gone`));
|
|
3505
3512
|
ws.send(JSON.stringify({ id, method, params, sessionId }));
|
|
3506
3513
|
});
|
|
@@ -3646,7 +3653,7 @@ function saveFile(file, bytes) {
|
|
|
3646
3653
|
}
|
|
3647
3654
|
|
|
3648
3655
|
// src/posters.ts
|
|
3649
|
-
var sleep2 = (ms) => new Promise((
|
|
3656
|
+
var sleep2 = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
3650
3657
|
function defaultPosterSize(name) {
|
|
3651
3658
|
if (/icon/i.test(name)) return { width: 512, height: 512 };
|
|
3652
3659
|
if (/badge/i.test(name)) return { width: 256, height: 256 };
|
|
@@ -3778,7 +3785,7 @@ function openBrowser(url) {
|
|
|
3778
3785
|
} catch {
|
|
3779
3786
|
}
|
|
3780
3787
|
}
|
|
3781
|
-
var sleep3 = (ms) => new Promise((
|
|
3788
|
+
var sleep3 = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
3782
3789
|
async function login(api2) {
|
|
3783
3790
|
const started = await post(`${api2}/api/cli/device`, { label: deviceLabel() });
|
|
3784
3791
|
if (started.status !== 200) throw new Error(String(started.data.message ?? `Login failed (${started.status}).`));
|
|
@@ -3822,7 +3829,7 @@ async function logout(api2) {
|
|
|
3822
3829
|
import { join as join7 } from "node:path";
|
|
3823
3830
|
var QUICK = ["join", "start", "reload-guest", "reload-host", "drop-host", "phone"];
|
|
3824
3831
|
var ALL = ["join", "start", "reload-guest", "reload-host", "drop-guest", "drop-host", "two-away", "spectator", "second-match", "rapid-ready", "phone"];
|
|
3825
|
-
var sleep4 = (ms) => new Promise((
|
|
3832
|
+
var sleep4 = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
3826
3833
|
async function until(fn, ms = 15e3, step = 250) {
|
|
3827
3834
|
const end = Date.now() + ms;
|
|
3828
3835
|
for (; ; ) {
|
|
@@ -4862,7 +4869,7 @@ var TOOLS = [
|
|
|
4862
4869
|
{
|
|
4863
4870
|
name: "screenshot",
|
|
4864
4871
|
title: "See the game",
|
|
4865
|
-
description: 'A picture of the game to look at: the first player\'s screen `at` seconds into a simulated match with bots (default 8), the title screen (phase: "title") or the lobby (phase: "lobby": Play pressed, nobody ready), or a poster (poster: "cover", "icon"...). story: true is the first minute as a new player lives it, frame by frame on one contact sheet, with the questions to ask of it. 2D draws without a browser; 3D (or browser: true) needs Chrome. Engine games, after npm install.',
|
|
4872
|
+
description: 'A picture of the game to look at: the first player\'s screen `at` seconds into a simulated match with bots (default 8), the title screen (phase: "title") or the lobby (phase: "lobby": Play pressed, nobody ready), or a poster (poster: "cover", "icon"...). story: true is the first minute as a new player lives it, frame by frame on one contact sheet, with the questions to ask of it. 2D draws without a browser, text in the game\'s own fonts (not the HTML overlay: browser: true draws it); 3D (or browser: true) needs Chrome. Engine games, after npm install.',
|
|
4866
4873
|
inputSchema: {
|
|
4867
4874
|
type: "object",
|
|
4868
4875
|
properties: {
|
|
@@ -5255,13 +5262,13 @@ function formatStatus(raw) {
|
|
|
5255
5262
|
}
|
|
5256
5263
|
|
|
5257
5264
|
// src/shot.ts
|
|
5258
|
-
import { existsSync as
|
|
5259
|
-
import { tmpdir as
|
|
5260
|
-
import { join as
|
|
5265
|
+
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5266
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
5267
|
+
import { join as join11, resolve as resolve8 } from "node:path";
|
|
5261
5268
|
import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL3 } from "node:url";
|
|
5262
5269
|
|
|
5263
5270
|
// src/font.ts
|
|
5264
|
-
import { brotliDecompressSync } from "node:zlib";
|
|
5271
|
+
import { brotliDecompressSync, inflateSync as inflateSync2 } from "node:zlib";
|
|
5265
5272
|
var KNOWN_TAGS = [
|
|
5266
5273
|
"cmap",
|
|
5267
5274
|
"head",
|
|
@@ -5564,7 +5571,8 @@ function plainGlyphs(glyf, loca, numGlyphs, longOffsets) {
|
|
|
5564
5571
|
continue;
|
|
5565
5572
|
}
|
|
5566
5573
|
const ends = Array.from({ length: count }, () => r.u16());
|
|
5567
|
-
|
|
5574
|
+
const instructions = r.u16();
|
|
5575
|
+
r.at += instructions;
|
|
5568
5576
|
const total = count ? ends[count - 1] + 1 : 0;
|
|
5569
5577
|
const fl = [];
|
|
5570
5578
|
while (fl.length < total) {
|
|
@@ -5649,10 +5657,35 @@ function readCmap(data) {
|
|
|
5649
5657
|
return 0;
|
|
5650
5658
|
};
|
|
5651
5659
|
}
|
|
5660
|
+
function woffTables(bytes) {
|
|
5661
|
+
const r = new Reader(bytes);
|
|
5662
|
+
r.at = 12;
|
|
5663
|
+
const numTables = r.u16();
|
|
5664
|
+
r.at = 44;
|
|
5665
|
+
const tables = /* @__PURE__ */ new Map();
|
|
5666
|
+
for (let i = 0; i < numTables; i++) {
|
|
5667
|
+
const tag = tagOf(r);
|
|
5668
|
+
const offset = r.u32();
|
|
5669
|
+
const compLength = r.u32();
|
|
5670
|
+
const origLength = r.u32();
|
|
5671
|
+
r.u32();
|
|
5672
|
+
if (offset + compLength > bytes.length) throw new Error("the font ends early");
|
|
5673
|
+
const stored = bytes.subarray(offset, offset + compLength);
|
|
5674
|
+
const data = compLength < origLength ? new Uint8Array(inflateSync2(stored)) : stored;
|
|
5675
|
+
tables.set(tag, { data, transformed: false });
|
|
5676
|
+
}
|
|
5677
|
+
return tables;
|
|
5678
|
+
}
|
|
5679
|
+
var tagAt = (bytes, at) => String.fromCharCode(bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]);
|
|
5652
5680
|
function readFont(bytes) {
|
|
5681
|
+
if (bytes.length < 12) throw new Error("not a font");
|
|
5653
5682
|
let tables;
|
|
5654
|
-
const magic =
|
|
5683
|
+
const magic = tagAt(bytes, 0);
|
|
5684
|
+
const flavor = magic === "wOF2" || magic === "wOFF" ? tagAt(bytes, 4) : magic;
|
|
5685
|
+
if (flavor === "OTTO") throw new Error("CFF outlines (an OpenType font with PostScript outlines) are not read");
|
|
5686
|
+
if (flavor !== "\0\0\0" && flavor !== "true") throw new Error(magic === "ttcf" ? "a font collection is not read" : "not a font");
|
|
5655
5687
|
if (magic === "wOF2") tables = woff2Tables(bytes).tables;
|
|
5688
|
+
else if (magic === "wOFF") tables = woffTables(bytes);
|
|
5656
5689
|
else {
|
|
5657
5690
|
tables = /* @__PURE__ */ new Map();
|
|
5658
5691
|
const r = new Reader(bytes);
|
|
@@ -5699,6 +5732,12 @@ function readFont(bytes) {
|
|
|
5699
5732
|
const raw = glyf.transformed ? transformedGlyphs(glyf.data) : plainGlyphs(glyf.data, table("loca").data, numGlyphs, longOffsets);
|
|
5700
5733
|
const toGlyph = readCmap(table("cmap").data);
|
|
5701
5734
|
let weight = 400;
|
|
5735
|
+
const os2 = tables.get("OS/2");
|
|
5736
|
+
if (os2 && os2.data.length >= 6) {
|
|
5737
|
+
const declared = os2.data[4] << 8 | os2.data[5];
|
|
5738
|
+
const w = declared > 0 && declared < 10 ? declared * 100 : declared;
|
|
5739
|
+
if (w >= 100 && w <= 1e3) weight = w;
|
|
5740
|
+
}
|
|
5702
5741
|
const fvar = tables.get("fvar");
|
|
5703
5742
|
if (fvar) {
|
|
5704
5743
|
const r = new Reader(fvar.data);
|
|
@@ -5764,6 +5803,591 @@ function fontStackOf(fonts) {
|
|
|
5764
5803
|
has: (code) => fonts.some((f) => f.has(code))
|
|
5765
5804
|
};
|
|
5766
5805
|
}
|
|
5806
|
+
var FONT_FAMILY_LIST = /(?:^|\s)[+-]?(?:\d+\.?\d*|\.\d+)(?:px|pt|pc|em|rem|ex|ch|%|vh|vw|vmin|vmax|mm|cm|in|q)(?:\s*\/\s*\S+)?\s+(\S.*)$/i;
|
|
5807
|
+
function familiesOf(css) {
|
|
5808
|
+
const list = FONT_FAMILY_LIST.exec(css)?.[1] ?? css;
|
|
5809
|
+
return list.split(",").map((f) => f.trim().replace(/^(['"])(.*)\1$/, "$2").trim().toLowerCase()).filter(Boolean);
|
|
5810
|
+
}
|
|
5811
|
+
var picked = /* @__PURE__ */ new WeakMap();
|
|
5812
|
+
function pickFont(book, css) {
|
|
5813
|
+
let known = picked.get(book);
|
|
5814
|
+
if (!known) picked.set(book, known = /* @__PURE__ */ new Map());
|
|
5815
|
+
const seen = known.get(css);
|
|
5816
|
+
if (seen !== void 0) return seen;
|
|
5817
|
+
let font = book.fallback;
|
|
5818
|
+
for (const family of familiesOf(css)) {
|
|
5819
|
+
const found = book.families.get(family);
|
|
5820
|
+
if (found) {
|
|
5821
|
+
font = found;
|
|
5822
|
+
break;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
if (known.size < 500) known.set(css, font);
|
|
5826
|
+
return font;
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
// src/game-fonts.ts
|
|
5830
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5831
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5832
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
5833
|
+
import { basename as basename4, dirname as dirname7, extname, join as join9, relative as relative3, resolve as resolve6, sep } from "node:path";
|
|
5834
|
+
var MAX_FAMILIES = 8;
|
|
5835
|
+
var NOT_HAD = /* @__PURE__ */ new Set([
|
|
5836
|
+
"serif",
|
|
5837
|
+
"sans-serif",
|
|
5838
|
+
"monospace",
|
|
5839
|
+
"cursive",
|
|
5840
|
+
"fantasy",
|
|
5841
|
+
"system-ui",
|
|
5842
|
+
"ui-serif",
|
|
5843
|
+
"ui-sans-serif",
|
|
5844
|
+
"ui-monospace",
|
|
5845
|
+
"ui-rounded",
|
|
5846
|
+
"math",
|
|
5847
|
+
"emoji",
|
|
5848
|
+
"fangsong",
|
|
5849
|
+
"-apple-system",
|
|
5850
|
+
"blinkmacsystemfont",
|
|
5851
|
+
"arial",
|
|
5852
|
+
"arial black",
|
|
5853
|
+
"helvetica",
|
|
5854
|
+
"helvetica neue",
|
|
5855
|
+
"segoe ui",
|
|
5856
|
+
"segoe ui emoji",
|
|
5857
|
+
"segoe ui symbol",
|
|
5858
|
+
"times",
|
|
5859
|
+
"times new roman",
|
|
5860
|
+
"courier",
|
|
5861
|
+
"courier new",
|
|
5862
|
+
"georgia",
|
|
5863
|
+
"verdana",
|
|
5864
|
+
"tahoma",
|
|
5865
|
+
"trebuchet ms",
|
|
5866
|
+
"impact",
|
|
5867
|
+
"comic sans ms",
|
|
5868
|
+
"lucida grande",
|
|
5869
|
+
"lucida console",
|
|
5870
|
+
"menlo",
|
|
5871
|
+
"monaco",
|
|
5872
|
+
"consolas",
|
|
5873
|
+
"sf pro",
|
|
5874
|
+
"sf pro display",
|
|
5875
|
+
"sf pro text",
|
|
5876
|
+
"sf mono",
|
|
5877
|
+
"apple color emoji",
|
|
5878
|
+
"noto color emoji",
|
|
5879
|
+
"palatino",
|
|
5880
|
+
"palatino linotype",
|
|
5881
|
+
// Words that aren't families: CSS keywords and values a `font`-named setting may hold.
|
|
5882
|
+
"inherit",
|
|
5883
|
+
"initial",
|
|
5884
|
+
"unset",
|
|
5885
|
+
"revert",
|
|
5886
|
+
"revert-layer",
|
|
5887
|
+
"none",
|
|
5888
|
+
"auto",
|
|
5889
|
+
"default",
|
|
5890
|
+
"normal",
|
|
5891
|
+
"bold",
|
|
5892
|
+
"bolder",
|
|
5893
|
+
"lighter",
|
|
5894
|
+
"italic",
|
|
5895
|
+
"oblique",
|
|
5896
|
+
"small-caps",
|
|
5897
|
+
"caption",
|
|
5898
|
+
"icon",
|
|
5899
|
+
"menu",
|
|
5900
|
+
"message-box",
|
|
5901
|
+
"small-caption",
|
|
5902
|
+
"status-bar",
|
|
5903
|
+
"swap",
|
|
5904
|
+
"block",
|
|
5905
|
+
"fallback",
|
|
5906
|
+
"optional",
|
|
5907
|
+
"true",
|
|
5908
|
+
"false",
|
|
5909
|
+
"null",
|
|
5910
|
+
"undefined"
|
|
5911
|
+
]);
|
|
5912
|
+
var FONT_FILE = /\.(?:woff2?|ttf|otf)(?:[?#].*)?$/i;
|
|
5913
|
+
var GOOGLE_CSS = /fonts\.googleapis\.com\/css2?\?([^"'`\s)<>\\]+)/gi;
|
|
5914
|
+
var GOOGLE_HOST = /fonts\.googleapis\.com\/css/i;
|
|
5915
|
+
var FAMILY_NAME = /^[A-Za-z][A-Za-z0-9 _.-]{0,63}$/;
|
|
5916
|
+
var FONT_KEY = /font|family/i;
|
|
5917
|
+
var NOT_FONT_KEY = /(?:size|weight|style|variant|kerning|stretch|scale|features?|smoothing|optical|generation|loaded|ready|px|height|colou?r|count|index|id)s?$/i;
|
|
5918
|
+
function fileFamily(path) {
|
|
5919
|
+
const name = basename4(path.split(/[?#]/)[0]);
|
|
5920
|
+
let decoded = name;
|
|
5921
|
+
try {
|
|
5922
|
+
decoded = decodeURIComponent(name);
|
|
5923
|
+
} catch {
|
|
5924
|
+
}
|
|
5925
|
+
return decoded.slice(0, decoded.length - extname(decoded).length).trim();
|
|
5926
|
+
}
|
|
5927
|
+
function familyOf(value) {
|
|
5928
|
+
const v = value.replace(/!important\s*$/i, "").trim();
|
|
5929
|
+
if (v === "" || v.length > 400 || GOOGLE_HOST.test(v)) return null;
|
|
5930
|
+
if (FONT_FILE.test(v) && !/^data:/i.test(v)) {
|
|
5931
|
+
const family = fileFamily(v);
|
|
5932
|
+
return /^[^.${}<>"'`\\][^${}<>"'`\\]{0,63}$/.test(family) ? { family, source: v } : null;
|
|
5933
|
+
}
|
|
5934
|
+
if (/^(?:[a-z][\w+.-]*:|\/\/)/i.test(v)) return null;
|
|
5935
|
+
const list = FONT_FAMILY_LIST.exec(v)?.[1] ?? v;
|
|
5936
|
+
const first = list.split(",")[0].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
5937
|
+
if (!FAMILY_NAME.test(first) || NOT_HAD.has(first.toLowerCase())) return null;
|
|
5938
|
+
return { family: first };
|
|
5939
|
+
}
|
|
5940
|
+
function googleFamilies(text2) {
|
|
5941
|
+
const out = [];
|
|
5942
|
+
for (const m of text2.matchAll(GOOGLE_CSS)) {
|
|
5943
|
+
for (const part of m[1].replace(/&/g, "&").split("&")) {
|
|
5944
|
+
const [key, value] = part.split("=");
|
|
5945
|
+
if (key !== "family" || !value) continue;
|
|
5946
|
+
let decoded = value.replace(/\+/g, " ");
|
|
5947
|
+
try {
|
|
5948
|
+
decoded = decodeURIComponent(decoded);
|
|
5949
|
+
} catch {
|
|
5950
|
+
}
|
|
5951
|
+
for (const family of decoded.split("|")) {
|
|
5952
|
+
const name = family.split(":")[0].trim();
|
|
5953
|
+
if (name) out.push(name);
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
return out;
|
|
5958
|
+
}
|
|
5959
|
+
function literalAt(text2, code, at) {
|
|
5960
|
+
const quote = text2[at];
|
|
5961
|
+
if (quote === "`") {
|
|
5962
|
+
let value = "";
|
|
5963
|
+
let dynamic = false;
|
|
5964
|
+
let i = at + 1;
|
|
5965
|
+
while (i < text2.length) {
|
|
5966
|
+
const c = text2[i];
|
|
5967
|
+
if (c === "\\") {
|
|
5968
|
+
value += text2[i + 1] ?? "";
|
|
5969
|
+
i += 2;
|
|
5970
|
+
} else if (c === "`") return { value, end: i, dynamic };
|
|
5971
|
+
else if (c === "$" && text2[i + 1] === "{") {
|
|
5972
|
+
const end2 = closing(code, i + 1);
|
|
5973
|
+
if (end2 < 0) return null;
|
|
5974
|
+
value += "0";
|
|
5975
|
+
dynamic = true;
|
|
5976
|
+
i = end2 + 1;
|
|
5977
|
+
} else {
|
|
5978
|
+
value += c;
|
|
5979
|
+
i++;
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
return null;
|
|
5983
|
+
}
|
|
5984
|
+
const end = code.indexOf(quote, at + 1);
|
|
5985
|
+
if (end < 0) return null;
|
|
5986
|
+
const raw = text2.slice(at + 1, end);
|
|
5987
|
+
return raw.includes("\n") ? null : { value: raw.replace(/\\(.)/g, "$1"), end, dynamic: false };
|
|
5988
|
+
}
|
|
5989
|
+
function objectAround(code, at) {
|
|
5990
|
+
let depth = 0;
|
|
5991
|
+
for (let i = at; i >= 0 && at - i < 4e3; i--) {
|
|
5992
|
+
if (code[i] === "}") depth++;
|
|
5993
|
+
else if (code[i] === "{") {
|
|
5994
|
+
if (depth === 0) {
|
|
5995
|
+
const end = closing(code, i);
|
|
5996
|
+
return end < 0 ? null : [i, end];
|
|
5997
|
+
}
|
|
5998
|
+
depth--;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
return null;
|
|
6002
|
+
}
|
|
6003
|
+
var SETTING = /(?<![\w$])([A-Za-z_$][\w$]*)\s*(?::|=(?![=>]))\s*(['"`])/g;
|
|
6004
|
+
var FONT_FILE_LITERAL = /(['"`])([^'"`\n]*?\.(?:woff2?|ttf|otf)(?:[?#][^'"`\n]*)?)\1/gi;
|
|
6005
|
+
var FACE_KEYS = /(?<![\w$])(?:weights?|src|url|file|style|display|unicodeRange)\s*:/;
|
|
6006
|
+
function fromScript(text2, addFont) {
|
|
6007
|
+
const code = blank(text2);
|
|
6008
|
+
const named = [];
|
|
6009
|
+
const add = (at, value, source, dynamic = false) => {
|
|
6010
|
+
if (!(dynamic && FONT_FILE.test(value))) named.push({ at, value, source });
|
|
6011
|
+
};
|
|
6012
|
+
const files = /* @__PURE__ */ new Map();
|
|
6013
|
+
for (const m of text2.matchAll(FONT_FILE_LITERAL)) if (code[m.index] === m[1]) files.set(m.index, m[2]);
|
|
6014
|
+
const taken = /* @__PURE__ */ new Set();
|
|
6015
|
+
const lists = [];
|
|
6016
|
+
for (const m of code.matchAll(/(?<![\w$])([\w$]*fonts)\s*(?::|=(?![=>]))\s*\[/gi)) {
|
|
6017
|
+
const open = m.index + m[0].length - 1;
|
|
6018
|
+
const end = closing(code, open);
|
|
6019
|
+
if (end < 0) continue;
|
|
6020
|
+
lists.push([open, end]);
|
|
6021
|
+
for (let i = open + 1; i < end; i++) {
|
|
6022
|
+
const c = code[i];
|
|
6023
|
+
if (c !== "'" && c !== '"' && c !== "`") continue;
|
|
6024
|
+
const literal = literalAt(text2, code, i);
|
|
6025
|
+
if (!literal) break;
|
|
6026
|
+
const before = code.slice(open, i).trimEnd();
|
|
6027
|
+
if ((before.endsWith("[") || before.endsWith(",")) && !files.has(i)) add(i, literal.value, void 0, literal.dynamic);
|
|
6028
|
+
i = literal.end;
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
for (const m of code.matchAll(SETTING)) {
|
|
6032
|
+
const key = m[1];
|
|
6033
|
+
if (!FONT_KEY.test(key) || NOT_FONT_KEY.test(key)) continue;
|
|
6034
|
+
const at = m.index + m[0].length - 1;
|
|
6035
|
+
const literal = literalAt(text2, code, at);
|
|
6036
|
+
if (!literal) continue;
|
|
6037
|
+
let source;
|
|
6038
|
+
if (/family/i.test(key)) {
|
|
6039
|
+
const object = objectAround(code, m.index);
|
|
6040
|
+
const inside = (pos) => object !== null && pos > object[0] && pos < object[1];
|
|
6041
|
+
const own = [...files.keys()].filter(inside);
|
|
6042
|
+
const listed = lists.some(([a, b]) => m.index > a && m.index < b);
|
|
6043
|
+
const face = own.length > 0 || object !== null && FACE_KEYS.test(code.slice(object[0], object[1]));
|
|
6044
|
+
if (/^family$/i.test(key) && !listed && !face) continue;
|
|
6045
|
+
for (const pos of own) taken.add(pos);
|
|
6046
|
+
source = own.length > 0 ? files.get(own[0]) : void 0;
|
|
6047
|
+
}
|
|
6048
|
+
if (files.has(at)) taken.add(at);
|
|
6049
|
+
add(at, literal.value, source, literal.dynamic);
|
|
6050
|
+
}
|
|
6051
|
+
for (const m of code.matchAll(/(?<![\w$])loadFont\s*\(\s*(['"`])/g)) {
|
|
6052
|
+
const at = m.index + m[0].length - 1;
|
|
6053
|
+
const literal = literalAt(text2, code, at);
|
|
6054
|
+
if (literal && !files.has(at)) add(at, literal.value, void 0, literal.dynamic);
|
|
6055
|
+
}
|
|
6056
|
+
for (const m of code.matchAll(/(?<![\w$])FontFace\s*\(\s*(['"`])/g)) {
|
|
6057
|
+
const at = m.index + m[0].length - 1;
|
|
6058
|
+
const family = literalAt(text2, code, at);
|
|
6059
|
+
if (!family) continue;
|
|
6060
|
+
const comma = /^\s*,\s*(['"`])/.exec(code.slice(family.end + 1));
|
|
6061
|
+
const src = comma ? literalAt(text2, code, family.end + comma[0].length) : null;
|
|
6062
|
+
const url = src && !src.dynamic ? /url\(\s*(['"]?)(.*?)\1\s*\)/i.exec(src.value)?.[2] ?? src.value : void 0;
|
|
6063
|
+
add(at, family.value, url && (FONT_FILE.test(url) || /^data:/i.test(url)) ? url : void 0, family.dynamic);
|
|
6064
|
+
}
|
|
6065
|
+
for (const [pos, file] of files) if (!taken.has(pos)) add(pos, file);
|
|
6066
|
+
for (const n of named.sort((a, b) => a.at - b.at)) addFont(n.value, n.source);
|
|
6067
|
+
return /(?<![\w$])overlay\s*\.\s*(?:el|mount)\s*\(|\{[^{}]*\b(?:el|mount)\b[^{}]*\}\s*=\s*[\w$.]*overlay\b/.test(code);
|
|
6068
|
+
}
|
|
6069
|
+
function fromCss(text2, add) {
|
|
6070
|
+
const css = text2.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
6071
|
+
for (const m of css.matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
|
|
6072
|
+
const family = /font-family\s*:\s*([^;]+)/i.exec(m[1])?.[1];
|
|
6073
|
+
if (!family) continue;
|
|
6074
|
+
const src = /(?:^|[;\s])src\s*:\s*([^;]+)/i.exec(m[1])?.[1] ?? "";
|
|
6075
|
+
const urls = [...src.matchAll(/url\(\s*(['"]?)(.*?)\1\s*\)/gi)].map((u) => u[2].trim());
|
|
6076
|
+
add(family, urls.find((u) => FONT_FILE.test(u) || /^data:/i.test(u)) ?? urls[0]);
|
|
6077
|
+
}
|
|
6078
|
+
const rest = css.replace(/@font-face\s*\{[^}]*\}/gi, " ");
|
|
6079
|
+
for (const m of rest.matchAll(/(?<![\w-])font-family\s*:\s*([^;{}]+)/gi)) add(m[1]);
|
|
6080
|
+
for (const m of rest.matchAll(/(?<![\w-])font\s*:\s*([^;{}]+)/gi)) add(m[1]);
|
|
6081
|
+
}
|
|
6082
|
+
function fromHtml(text2, addScript, addStyle) {
|
|
6083
|
+
let overlay = false;
|
|
6084
|
+
for (const m of text2.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)) overlay = fromScript(m[1], addScript) || overlay;
|
|
6085
|
+
for (const m of text2.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)) fromCss(m[1], addStyle);
|
|
6086
|
+
for (const m of text2.matchAll(/\sstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) fromCss(m[1] ?? m[2] ?? "", addStyle);
|
|
6087
|
+
return overlay;
|
|
6088
|
+
}
|
|
6089
|
+
function fromJson(text2, add) {
|
|
6090
|
+
let data;
|
|
6091
|
+
try {
|
|
6092
|
+
data = JSON.parse(text2);
|
|
6093
|
+
} catch {
|
|
6094
|
+
return;
|
|
6095
|
+
}
|
|
6096
|
+
let budget = 2e5;
|
|
6097
|
+
const visit = (value, depth, inFonts) => {
|
|
6098
|
+
if (budget-- <= 0 || depth > 40 || !value || typeof value !== "object") return;
|
|
6099
|
+
if (Array.isArray(value)) {
|
|
6100
|
+
for (const v of value) visit(v, depth + 1, inFonts);
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
const entries = Object.entries(value);
|
|
6104
|
+
const file = entries.find(([, v]) => typeof v === "string" && FONT_FILE.test(v))?.[1];
|
|
6105
|
+
const face = inFonts || file !== void 0 || entries.some(([k]) => /^(?:weights?|src|url|file|style|display|unicodeRange)$/.test(k));
|
|
6106
|
+
for (const [key, v] of entries) {
|
|
6107
|
+
const list = /fonts$/i.test(key);
|
|
6108
|
+
if (typeof v === "string" && FONT_KEY.test(key) && !NOT_FONT_KEY.test(key) && (face || !/^family$/i.test(key))) add(v, /family/i.test(key) ? file : void 0);
|
|
6109
|
+
else if (Array.isArray(v) && list) {
|
|
6110
|
+
for (const item of v) if (typeof item === "string") add(item);
|
|
6111
|
+
}
|
|
6112
|
+
visit(v, depth + 1, list && Array.isArray(v));
|
|
6113
|
+
}
|
|
6114
|
+
};
|
|
6115
|
+
visit(data, 0, false);
|
|
6116
|
+
}
|
|
6117
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
6118
|
+
"node_modules",
|
|
6119
|
+
"dist",
|
|
6120
|
+
"vendor",
|
|
6121
|
+
"vendors",
|
|
6122
|
+
"third_party",
|
|
6123
|
+
"third-party",
|
|
6124
|
+
"bower_components",
|
|
6125
|
+
"jspm_packages",
|
|
6126
|
+
"web_modules",
|
|
6127
|
+
"coverage",
|
|
6128
|
+
"test",
|
|
6129
|
+
"tests",
|
|
6130
|
+
"__tests__"
|
|
6131
|
+
]);
|
|
6132
|
+
var SOURCE_FILE = /\.(?:[cm]?[jt]sx?|html?|css|json)$/i;
|
|
6133
|
+
var NOT_SOURCE = /(?:\.min\.[cm]?js|\.bundle\.[cm]?js|\.d\.[cm]?ts|\.(?:test|spec)\.[cm]?[jt]sx?|^package(?:-lock)?\.json|^tsconfig\.json|^jsconfig\.json)$/i;
|
|
6134
|
+
function discoverFonts(dir) {
|
|
6135
|
+
const root = resolve6(dir);
|
|
6136
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6137
|
+
const pages = [];
|
|
6138
|
+
let overlay = false;
|
|
6139
|
+
let files = 0;
|
|
6140
|
+
const adder = (from) => (value, source) => {
|
|
6141
|
+
const found = familyOf(value);
|
|
6142
|
+
if (!found) return;
|
|
6143
|
+
const key = found.family.toLowerCase();
|
|
6144
|
+
if (key === "sora") return;
|
|
6145
|
+
let entry = seen.get(key);
|
|
6146
|
+
if (!entry) seen.set(key, entry = { ref: { family: found.family }, uses: 0, order: seen.size });
|
|
6147
|
+
entry.uses++;
|
|
6148
|
+
if (entry.ref.family === entry.ref.family.toLowerCase() && found.family !== key) entry.ref.family = found.family;
|
|
6149
|
+
const file = source ?? found.source;
|
|
6150
|
+
if (file && !entry.ref.source) {
|
|
6151
|
+
entry.ref.source = file;
|
|
6152
|
+
entry.ref.from = from;
|
|
6153
|
+
}
|
|
6154
|
+
};
|
|
6155
|
+
const walk = (folder, depth) => {
|
|
6156
|
+
if (depth > 10 || files > 4e3) return;
|
|
6157
|
+
let entries;
|
|
6158
|
+
try {
|
|
6159
|
+
entries = readdirSync3(folder, { withFileTypes: true });
|
|
6160
|
+
} catch {
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6164
|
+
if (entry.name.startsWith(".")) continue;
|
|
6165
|
+
const abs = join9(folder, entry.name);
|
|
6166
|
+
if (entry.isDirectory()) {
|
|
6167
|
+
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
6168
|
+
continue;
|
|
6169
|
+
}
|
|
6170
|
+
if (!entry.isFile() || !SOURCE_FILE.test(entry.name) || NOT_SOURCE.test(entry.name)) continue;
|
|
6171
|
+
const rel = relative3(root, abs);
|
|
6172
|
+
if (/^index\.html?$/i.test(entry.name)) pages.push(relative3(root, folder));
|
|
6173
|
+
let text2;
|
|
6174
|
+
try {
|
|
6175
|
+
if (statSync3(abs).size > 15e5) continue;
|
|
6176
|
+
text2 = readFileSync4(abs, "utf8");
|
|
6177
|
+
} catch {
|
|
6178
|
+
continue;
|
|
6179
|
+
}
|
|
6180
|
+
files++;
|
|
6181
|
+
const add = adder(rel);
|
|
6182
|
+
for (const family of googleFamilies(text2)) add(family);
|
|
6183
|
+
const ext = extname(entry.name).toLowerCase();
|
|
6184
|
+
if (ext === ".css") fromCss(text2, add);
|
|
6185
|
+
else if (ext === ".html" || ext === ".htm") overlay = fromHtml(text2, add, add) || overlay;
|
|
6186
|
+
else if (ext === ".json") fromJson(text2, add);
|
|
6187
|
+
else overlay = fromScript(text2, add) || overlay;
|
|
6188
|
+
}
|
|
6189
|
+
};
|
|
6190
|
+
walk(root, 0);
|
|
6191
|
+
const families = [...seen.values()].sort((a, b) => b.uses - a.uses || a.order - b.order).slice(0, MAX_FAMILIES).map((e) => e.ref);
|
|
6192
|
+
return { families, overlay, pages };
|
|
6193
|
+
}
|
|
6194
|
+
var NO_GAME_FONTS = { fonts: [], unavailable: [], overlay: false };
|
|
6195
|
+
var FONT_HOSTS = /* @__PURE__ */ new Set(["cdn.jsdelivr.net", "cdnjs.cloudflare.com", "unpkg.com", "esm.sh", "ga.jspm.io", "fonts.gstatic.com"]);
|
|
6196
|
+
var GOOGLE_WEIGHTS = ["800", "700", "600", "regular"];
|
|
6197
|
+
function parse(bytes) {
|
|
6198
|
+
try {
|
|
6199
|
+
return { font: readFont(bytes) };
|
|
6200
|
+
} catch (error) {
|
|
6201
|
+
return { why: /CFF/.test(String(error.message)) ? "CFF outlines, not read here" : "not a font file read here" };
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
function cacheOf(options) {
|
|
6205
|
+
const dir = options.cacheDir ?? join9(tmpdir3(), "onceworlds-fonts");
|
|
6206
|
+
try {
|
|
6207
|
+
mkdirSync2(dir, { recursive: true });
|
|
6208
|
+
return dir;
|
|
6209
|
+
} catch {
|
|
6210
|
+
return null;
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
async function download2(url, options) {
|
|
6214
|
+
const fetcher = options.fetch ?? fetch;
|
|
6215
|
+
try {
|
|
6216
|
+
const res = await fetcher(url, { signal: AbortSignal.timeout(options.timeoutMs ?? 1e4) });
|
|
6217
|
+
if (!res.ok) return { why: `${new URL(url).host} answered ${res.status}` };
|
|
6218
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
6219
|
+
return bytes.length > 0 && bytes.length < 3e7 ? bytes : { why: "an empty file" };
|
|
6220
|
+
} catch {
|
|
6221
|
+
return { why: "offline" };
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
function cached2(cache, key) {
|
|
6225
|
+
if (!cache) return null;
|
|
6226
|
+
const fonts = [];
|
|
6227
|
+
for (let i = 0; i < 8; i++) {
|
|
6228
|
+
const file = join9(cache, `${key}.${i}.font`);
|
|
6229
|
+
if (!existsSync5(file)) break;
|
|
6230
|
+
try {
|
|
6231
|
+
fonts.push(readFont(readFileSync4(file)));
|
|
6232
|
+
} catch {
|
|
6233
|
+
return null;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
return fonts.length > 0 ? fontStackOf(fonts) : null;
|
|
6237
|
+
}
|
|
6238
|
+
function keep(cache, key, files) {
|
|
6239
|
+
if (!cache) return;
|
|
6240
|
+
try {
|
|
6241
|
+
files.forEach((bytes, i) => writeFileSync3(join9(cache, `${key}.${i}.font`), bytes));
|
|
6242
|
+
} catch {
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
async function googleFont(family, options = {}) {
|
|
6246
|
+
const cache = cacheOf(options);
|
|
6247
|
+
const slug = `google-${family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
|
|
6248
|
+
for (const weight of GOOGLE_WEIGHTS) {
|
|
6249
|
+
const font = cached2(cache, `${slug}-${weight}`);
|
|
6250
|
+
if (font) return { font };
|
|
6251
|
+
}
|
|
6252
|
+
const fetcher = options.fetch ?? fetch;
|
|
6253
|
+
const name = encodeURIComponent(family).replace(/%20/g, "+");
|
|
6254
|
+
for (const weight of GOOGLE_WEIGHTS) {
|
|
6255
|
+
const url = `https://fonts.googleapis.com/css2?family=${name}${weight === "regular" ? "" : `:wght@${weight}`}`;
|
|
6256
|
+
let css;
|
|
6257
|
+
try {
|
|
6258
|
+
const res = await fetcher(url, { signal: AbortSignal.timeout(options.timeoutMs ?? 1e4) });
|
|
6259
|
+
if (res.status === 400 || res.status === 404) continue;
|
|
6260
|
+
if (!res.ok) return { why: `Google Fonts answered ${res.status}` };
|
|
6261
|
+
css = await res.text();
|
|
6262
|
+
} catch {
|
|
6263
|
+
return { why: "offline" };
|
|
6264
|
+
}
|
|
6265
|
+
const urls = [...css.matchAll(/url\(\s*(['"]?)(https:[^'")\s]+)\1\s*\)/gi)].map((m) => m[2]).slice(0, 8);
|
|
6266
|
+
if (urls.length === 0) return { why: "no file on Google Fonts" };
|
|
6267
|
+
const files = [];
|
|
6268
|
+
const fonts = [];
|
|
6269
|
+
for (const url2 of urls) {
|
|
6270
|
+
const got = await download2(url2, options);
|
|
6271
|
+
if (!(got instanceof Uint8Array)) return got;
|
|
6272
|
+
const had = parse(got);
|
|
6273
|
+
if ("font" in had) {
|
|
6274
|
+
files.push(got);
|
|
6275
|
+
fonts.push(had.font);
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
if (fonts.length === 0) return { why: "not a font file read here" };
|
|
6279
|
+
keep(cache, `${slug}-${weight}`, files);
|
|
6280
|
+
return { font: fontStackOf(fonts) };
|
|
6281
|
+
}
|
|
6282
|
+
return { why: "not on Google Fonts" };
|
|
6283
|
+
}
|
|
6284
|
+
async function remoteFont(url, options) {
|
|
6285
|
+
let host;
|
|
6286
|
+
try {
|
|
6287
|
+
host = new URL(url).host;
|
|
6288
|
+
} catch {
|
|
6289
|
+
return { why: "not an address" };
|
|
6290
|
+
}
|
|
6291
|
+
if (!FONT_HOSTS.has(host)) return { why: `${host} isn't a host the platform loads fonts from` };
|
|
6292
|
+
const cache = cacheOf(options);
|
|
6293
|
+
const key = `url-${createHash3("sha1").update(url).digest("hex").slice(0, 16)}`;
|
|
6294
|
+
const known = cached2(cache, key);
|
|
6295
|
+
if (known) return { font: known };
|
|
6296
|
+
const got = await download2(url, options);
|
|
6297
|
+
if (!(got instanceof Uint8Array)) return got;
|
|
6298
|
+
const had = parse(got);
|
|
6299
|
+
if ("font" in had) keep(cache, key, [got]);
|
|
6300
|
+
return had;
|
|
6301
|
+
}
|
|
6302
|
+
function gameFile(source, from, dir, pages) {
|
|
6303
|
+
let path = source.split(/[?#]/)[0];
|
|
6304
|
+
try {
|
|
6305
|
+
path = decodeURIComponent(path);
|
|
6306
|
+
} catch {
|
|
6307
|
+
}
|
|
6308
|
+
const rooted = path.startsWith("/");
|
|
6309
|
+
const rel = path.replace(/^\/+/, "");
|
|
6310
|
+
const bases = [...from && !rooted ? [dirname7(join9(dir, from))] : [], ...pages.map((p) => join9(dir, p)), dir];
|
|
6311
|
+
for (const base of bases) {
|
|
6312
|
+
const file = resolve6(base, rel);
|
|
6313
|
+
if (file !== dir && !file.startsWith(dir + sep)) continue;
|
|
6314
|
+
try {
|
|
6315
|
+
if (statSync3(file).isFile()) return file;
|
|
6316
|
+
} catch {
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
return null;
|
|
6320
|
+
}
|
|
6321
|
+
async function have(ref, dir, pages, options) {
|
|
6322
|
+
const source = ref.source;
|
|
6323
|
+
if (!source) {
|
|
6324
|
+
const had = await googleFont(ref.family, options);
|
|
6325
|
+
const plain3 = ref.family.replace(/\s+Variable$/i, "");
|
|
6326
|
+
return "why" in had && had.why === "not on Google Fonts" && plain3 !== ref.family ? googleFont(plain3, options) : had;
|
|
6327
|
+
}
|
|
6328
|
+
if (/^data:/i.test(source)) {
|
|
6329
|
+
const m = /^data:[^,]*?(;base64)?,(.*)$/is.exec(source);
|
|
6330
|
+
if (!m) return { why: "not a font file read here" };
|
|
6331
|
+
let bytes;
|
|
6332
|
+
try {
|
|
6333
|
+
bytes = m[1] ? Buffer.from(m[2], "base64") : Buffer.from(decodeURIComponent(m[2]), "latin1");
|
|
6334
|
+
} catch {
|
|
6335
|
+
return { why: "not a font file read here" };
|
|
6336
|
+
}
|
|
6337
|
+
return parse(bytes);
|
|
6338
|
+
}
|
|
6339
|
+
if (/^(?:https?:)?\/\//i.test(source)) return remoteFont(source.startsWith("//") ? `https:${source}` : source, options);
|
|
6340
|
+
if (/^[a-z][\w+.-]*:/i.test(source)) return { why: "not a file" };
|
|
6341
|
+
const file = gameFile(source, ref.from, resolve6(dir), pages);
|
|
6342
|
+
if (!file) return { why: `no file ${source}` };
|
|
6343
|
+
try {
|
|
6344
|
+
return parse(readFileSync4(file));
|
|
6345
|
+
} catch {
|
|
6346
|
+
return { why: `no file ${source}` };
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
async function loadGameFonts(dir, options = {}) {
|
|
6350
|
+
let found;
|
|
6351
|
+
try {
|
|
6352
|
+
found = discoverFonts(dir);
|
|
6353
|
+
} catch {
|
|
6354
|
+
return NO_GAME_FONTS;
|
|
6355
|
+
}
|
|
6356
|
+
const had = await Promise.all(found.families.map((ref) => have(ref, dir, found.pages, options).catch(() => ({ why: "not a font file read here" }))));
|
|
6357
|
+
const out = { fonts: [], unavailable: [], overlay: found.overlay };
|
|
6358
|
+
found.families.forEach((ref, i) => {
|
|
6359
|
+
const result = had[i];
|
|
6360
|
+
if ("font" in result) {
|
|
6361
|
+
const aliases = ref.source && !/^data:/i.test(ref.source) ? [.../* @__PURE__ */ new Set([ref.source, ref.source.split(/[?#]/)[0]])] : [];
|
|
6362
|
+
out.fonts.push({ family: ref.family, font: result.font, aliases });
|
|
6363
|
+
} else out.unavailable.push({ family: ref.family, why: result.why });
|
|
6364
|
+
});
|
|
6365
|
+
return out;
|
|
6366
|
+
}
|
|
6367
|
+
function fontBook(game, sora) {
|
|
6368
|
+
const families = /* @__PURE__ */ new Map();
|
|
6369
|
+
for (const { family, font, aliases } of game.fonts) {
|
|
6370
|
+
const drawn = sora ? fontStackOf([font, sora]) : font;
|
|
6371
|
+
for (const name of [family, ...aliases]) families.set(name.toLowerCase(), drawn);
|
|
6372
|
+
}
|
|
6373
|
+
return { fallback: sora, families };
|
|
6374
|
+
}
|
|
6375
|
+
function fontCoverage(game, sora) {
|
|
6376
|
+
return unionCoverage(
|
|
6377
|
+
[...game.fonts.map((f) => ({ name: f.family, coverage: f.font })), { name: "Sora", coverage: sora ?? rangeCoverage() }],
|
|
6378
|
+
game.unavailable.map((u) => u.family)
|
|
6379
|
+
);
|
|
6380
|
+
}
|
|
6381
|
+
var listOf = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
6382
|
+
function fontNotes(game) {
|
|
6383
|
+
const notes = [];
|
|
6384
|
+
const lost = game.unavailable;
|
|
6385
|
+
if (lost.length > 0) {
|
|
6386
|
+
notes.push(`${listOf(lost.map((u) => `${u.family} (${u.why})`))} could not be had: ${lost.length === 1 ? "its" : "their"} text is drawn in Sora`);
|
|
6387
|
+
}
|
|
6388
|
+
if (game.overlay) notes.push("the game's HTML overlay (ui.overlay) isn't in a headless picture: --browser draws it");
|
|
6389
|
+
return notes;
|
|
6390
|
+
}
|
|
5767
6391
|
|
|
5768
6392
|
// src/raster.ts
|
|
5769
6393
|
var NAMED = {
|
|
@@ -5836,16 +6460,20 @@ var Surface = class {
|
|
|
5836
6460
|
height;
|
|
5837
6461
|
/** Premultiplied RGBA, 0 to 1. */
|
|
5838
6462
|
data;
|
|
6463
|
+
/** What text is drawn in when the state's font names no family of `book` (the engine's font). */
|
|
5839
6464
|
font;
|
|
6465
|
+
/** The game's fonts by family: text is drawn in the first family of the state's font the book has. */
|
|
6466
|
+
book;
|
|
5840
6467
|
state;
|
|
5841
6468
|
stack = [];
|
|
5842
6469
|
paths = [];
|
|
5843
6470
|
current = null;
|
|
5844
|
-
constructor(width, height,
|
|
6471
|
+
constructor(width, height, fonts = null) {
|
|
5845
6472
|
this.width = Math.max(1, Math.min(8192, Math.round(width) || 1));
|
|
5846
6473
|
this.height = Math.max(1, Math.min(8192, Math.round(height) || 1));
|
|
5847
6474
|
this.data = new Float32Array(this.width * this.height * 4);
|
|
5848
|
-
this.
|
|
6475
|
+
this.book = fonts && "families" in fonts ? fonts : null;
|
|
6476
|
+
this.font = fonts && "families" in fonts ? fonts.fallback : fonts;
|
|
5849
6477
|
this.state = {
|
|
5850
6478
|
t: [1, 0, 0, 1, 0, 0],
|
|
5851
6479
|
fill: [0, 0, 0, 1],
|
|
@@ -6121,8 +6749,8 @@ var Surface = class {
|
|
|
6121
6749
|
const k = mask.data[j * mask.w + i];
|
|
6122
6750
|
if (k <= 0) continue;
|
|
6123
6751
|
const p = (mask.y0 + j) * this.width + mask.x0 + i;
|
|
6124
|
-
const
|
|
6125
|
-
for (let c = 0; c < 4; c++) this.data[p * 4 + c] *=
|
|
6752
|
+
const keep2 = 1 - k * (clip ? clip[p] : 1);
|
|
6753
|
+
for (let c = 0; c < 4; c++) this.data[p * 4 + c] *= keep2;
|
|
6126
6754
|
}
|
|
6127
6755
|
}
|
|
6128
6756
|
}
|
|
@@ -6255,8 +6883,12 @@ var Surface = class {
|
|
|
6255
6883
|
const weight = Number(/(?:^|\s)([1-9]00)(?:\s|$)/.exec(f)?.[1] ?? named);
|
|
6256
6884
|
return { size, weight };
|
|
6257
6885
|
}
|
|
6886
|
+
/** The font the state's font string draws in. */
|
|
6887
|
+
typeface() {
|
|
6888
|
+
return this.book ? pickFont(this.book, this.state.font) : this.font;
|
|
6889
|
+
}
|
|
6258
6890
|
measureText(text2) {
|
|
6259
|
-
return { width: measure(this.
|
|
6891
|
+
return { width: measure(this.typeface(), String(text2), this.fontOf().size) };
|
|
6260
6892
|
}
|
|
6261
6893
|
fillText(text2, x, y) {
|
|
6262
6894
|
this.text(String(text2), x, y, false);
|
|
@@ -6265,7 +6897,7 @@ var Surface = class {
|
|
|
6265
6897
|
this.text(String(text2), x, y, true);
|
|
6266
6898
|
}
|
|
6267
6899
|
text(text2, x, y, outline) {
|
|
6268
|
-
const font = this.
|
|
6900
|
+
const font = this.typeface();
|
|
6269
6901
|
const { size, weight } = this.fontOf();
|
|
6270
6902
|
if (!(size > 0) || text2 === "") return;
|
|
6271
6903
|
const width = measure(font, text2, size);
|
|
@@ -6492,7 +7124,7 @@ function circle(cx, cy, r) {
|
|
|
6492
7124
|
const n = Math.max(8, Math.min(64, Math.ceil(r * 1.5)));
|
|
6493
7125
|
return Array.from({ length: n }, (_, i) => [cx + r * Math.cos(i / n * Math.PI * 2), cy + r * Math.sin(i / n * Math.PI * 2)]);
|
|
6494
7126
|
}
|
|
6495
|
-
function strokePolygons(pts, closed, half, cap,
|
|
7127
|
+
function strokePolygons(pts, closed, half, cap, join14, miterLimit) {
|
|
6496
7128
|
const points = pts.filter((p, i) => i === 0 || Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]) > 1e-6);
|
|
6497
7129
|
if (closed && points.length > 2 && Math.hypot(points[0][0] - points[points.length - 1][0], points[0][1] - points[points.length - 1][1]) < 1e-6) points.pop();
|
|
6498
7130
|
const out = [];
|
|
@@ -6524,7 +7156,7 @@ function strokePolygons(pts, closed, half, cap, join13, miterLimit) {
|
|
|
6524
7156
|
const v = points[i];
|
|
6525
7157
|
const n1 = normals[(i - 1 + segs) % segs];
|
|
6526
7158
|
const n2 = normals[i % segs];
|
|
6527
|
-
if (
|
|
7159
|
+
if (join14 === "round") {
|
|
6528
7160
|
out.push(circle(v[0], v[1], half));
|
|
6529
7161
|
continue;
|
|
6530
7162
|
}
|
|
@@ -6532,7 +7164,7 @@ function strokePolygons(pts, closed, half, cap, join13, miterLimit) {
|
|
|
6532
7164
|
const p1 = [v[0] + n1[0] * half * side, v[1] + n1[1] * half * side];
|
|
6533
7165
|
const p2 = [v[0] + n2[0] * half * side, v[1] + n2[1] * half * side];
|
|
6534
7166
|
const poly = [v, p1, p2];
|
|
6535
|
-
if (
|
|
7167
|
+
if (join14 === "miter") {
|
|
6536
7168
|
const mx = n1[0] + n2[0];
|
|
6537
7169
|
const my = n1[1] + n2[1];
|
|
6538
7170
|
const ml = Math.hypot(mx, my);
|
|
@@ -6696,9 +7328,9 @@ function boxBlur(src, w, h, r, horizontal) {
|
|
|
6696
7328
|
|
|
6697
7329
|
// src/serve.ts
|
|
6698
7330
|
import { createServer } from "node:http";
|
|
6699
|
-
import { existsSync as
|
|
7331
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
|
|
6700
7332
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
6701
|
-
import { dirname as
|
|
7333
|
+
import { dirname as dirname8, extname as extname2, join as join10, resolve as resolve7, sep as sep2 } from "node:path";
|
|
6702
7334
|
var TYPES = {
|
|
6703
7335
|
".html": "text/html; charset=utf-8",
|
|
6704
7336
|
".js": "text/javascript",
|
|
@@ -6732,18 +7364,18 @@ var TYPES = {
|
|
|
6732
7364
|
".map": "application/json"
|
|
6733
7365
|
};
|
|
6734
7366
|
function engineFiles(dir, api2, version) {
|
|
6735
|
-
for (let here =
|
|
6736
|
-
const pkg =
|
|
6737
|
-
for (const files of [
|
|
6738
|
-
if (!
|
|
7367
|
+
for (let here = resolve7(dir); ; here = dirname8(here)) {
|
|
7368
|
+
const pkg = join10(here, "node_modules", "@onceworlds", "engine");
|
|
7369
|
+
for (const files of [join10(pkg, "dist"), pkg]) {
|
|
7370
|
+
if (!existsSync6(join10(files, "index.js")) || !existsSync6(join10(files, "modules.js"))) continue;
|
|
6739
7371
|
let installed = null;
|
|
6740
7372
|
try {
|
|
6741
|
-
installed = String(JSON.parse(
|
|
7373
|
+
installed = String(JSON.parse(readFileSync5(join10(pkg, "package.json"), "utf8")).version ?? "") || null;
|
|
6742
7374
|
} catch {
|
|
6743
7375
|
}
|
|
6744
7376
|
return { local: files, installed, remote: "" };
|
|
6745
7377
|
}
|
|
6746
|
-
if (
|
|
7378
|
+
if (dirname8(here) === here) break;
|
|
6747
7379
|
}
|
|
6748
7380
|
return { local: null, installed: null, remote: `${api2}/engine/${version}/` };
|
|
6749
7381
|
}
|
|
@@ -6756,7 +7388,7 @@ function atLeast(version, least) {
|
|
|
6756
7388
|
return true;
|
|
6757
7389
|
}
|
|
6758
7390
|
async function serveFolder(options) {
|
|
6759
|
-
const dir =
|
|
7391
|
+
const dir = resolve7(options.dir);
|
|
6760
7392
|
const engine = engineFiles(dir, options.api, options.version);
|
|
6761
7393
|
const map = engine.local ? { "@onceworlds/engine": "/__engine/index.js", "@onceworlds/engine/modules": "/__engine/modules.js" } : { "@onceworlds/engine": `${engine.remote}engine.js`, "@onceworlds/engine/modules": `${engine.remote}modules.js` };
|
|
6762
7394
|
const server = createServer(async (req, res) => {
|
|
@@ -6764,16 +7396,16 @@ async function serveFolder(options) {
|
|
|
6764
7396
|
const path = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
|
|
6765
7397
|
const fromEngine = path.startsWith("/__engine/") && engine.local !== null;
|
|
6766
7398
|
const base = fromEngine ? engine.local : dir;
|
|
6767
|
-
let file = fromEngine ?
|
|
6768
|
-
if (
|
|
6769
|
-
if (path.endsWith("/") ||
|
|
7399
|
+
let file = fromEngine ? join10(base, path.slice("/__engine/".length)) : join10(dir, path);
|
|
7400
|
+
if (resolve7(file) !== base && !resolve7(file).startsWith(base + sep2)) throw new Error("outside");
|
|
7401
|
+
if (path.endsWith("/") || existsSync6(file) && statSync4(file).isDirectory()) file = join10(file, "index.html");
|
|
6770
7402
|
let body = await readFile4(file);
|
|
6771
7403
|
if (file.endsWith(".html")) {
|
|
6772
7404
|
const tag = `<script type="importmap">${JSON.stringify({ imports: map })}</script>`;
|
|
6773
7405
|
const html = body.toString("utf8");
|
|
6774
7406
|
body = /<head[^>]*>/i.test(html) ? html.replace(/<head[^>]*>/i, (m) => `${m}${tag}`) : `${tag}${html}`;
|
|
6775
7407
|
}
|
|
6776
|
-
res.writeHead(200, { "Content-Type": TYPES[
|
|
7408
|
+
res.writeHead(200, { "Content-Type": TYPES[extname2(file).toLowerCase()] ?? "application/octet-stream", "Cache-Control": "no-store" });
|
|
6777
7409
|
res.end(body);
|
|
6778
7410
|
} catch {
|
|
6779
7411
|
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
@@ -6903,9 +7535,9 @@ async function fontFile(file, api2, fetcher, versions) {
|
|
|
6903
7535
|
return null;
|
|
6904
7536
|
}
|
|
6905
7537
|
};
|
|
6906
|
-
if (
|
|
6907
|
-
const
|
|
6908
|
-
if (
|
|
7538
|
+
if (existsSync7(join11(LOCAL_FONTS, file))) return read(readFileSync6(join11(LOCAL_FONTS, file)));
|
|
7539
|
+
const cached3 = join11(tmpdir4(), `onceworlds-${file}`);
|
|
7540
|
+
if (existsSync7(cached3)) return read(readFileSync6(cached3));
|
|
6909
7541
|
for (const version of [...versions, "1.1.0"]) {
|
|
6910
7542
|
try {
|
|
6911
7543
|
const res = await fetcher(`${api2}/engine/${version}/fonts/${file}`, { signal: AbortSignal.timeout(1e4) });
|
|
@@ -6914,7 +7546,7 @@ async function fontFile(file, api2, fetcher, versions) {
|
|
|
6914
7546
|
const font = read(bytes);
|
|
6915
7547
|
if (font) {
|
|
6916
7548
|
try {
|
|
6917
|
-
|
|
7549
|
+
writeFileSync4(cached3, bytes);
|
|
6918
7550
|
} catch {
|
|
6919
7551
|
}
|
|
6920
7552
|
return font;
|
|
@@ -6930,6 +7562,30 @@ async function engineFont(api2, fetcher = fetch, versions = []) {
|
|
|
6930
7562
|
const extended = await fontFile(FONT_FILES[1], api2, fetcher, versions);
|
|
6931
7563
|
return extended ? fontStackOf([latin, extended]) : latin;
|
|
6932
7564
|
}
|
|
7565
|
+
async function textFonts(options) {
|
|
7566
|
+
const [sora, game] = await Promise.all([
|
|
7567
|
+
engineFont(options.api, options.fetch, options.version ? [options.version] : []).catch(() => null),
|
|
7568
|
+
loadGameFonts(options.dir, { fetch: options.fetch, cacheDir: options.fontCache }).catch(() => NO_GAME_FONTS)
|
|
7569
|
+
]);
|
|
7570
|
+
return { sora, game, book: fontBook(game, sora), coverage: fontCoverage(game, sora) };
|
|
7571
|
+
}
|
|
7572
|
+
async function textCoverage(dir, api2, fetcher = fetch) {
|
|
7573
|
+
const fonts = await textFonts({ dir, api: api2, fetch: fetcher });
|
|
7574
|
+
return fonts.coverage;
|
|
7575
|
+
}
|
|
7576
|
+
function metricsTable(book) {
|
|
7577
|
+
const table = { def: metricsOf(book.fallback), fam: {}, list: [] };
|
|
7578
|
+
const index = /* @__PURE__ */ new Map();
|
|
7579
|
+
for (const [family, font] of book.families) {
|
|
7580
|
+
let at = index.get(font);
|
|
7581
|
+
if (at === void 0) {
|
|
7582
|
+
at = table.list.push(metricsOf(font)) - 1;
|
|
7583
|
+
index.set(font, at);
|
|
7584
|
+
}
|
|
7585
|
+
table.fam[family] = at;
|
|
7586
|
+
}
|
|
7587
|
+
return table;
|
|
7588
|
+
}
|
|
6933
7589
|
function metricsOf(font) {
|
|
6934
7590
|
if (!font) return { upm: 1e3, asc: 800, desc: -200, adv: {}, avg: 550 };
|
|
6935
7591
|
const adv = {};
|
|
@@ -6952,8 +7608,10 @@ function posterSize(name, width, height) {
|
|
|
6952
7608
|
function recorderScript(options) {
|
|
6953
7609
|
return `
|
|
6954
7610
|
const OUT = ${JSON.stringify(options.out)};
|
|
6955
|
-
const DIR = ${JSON.stringify(
|
|
7611
|
+
const DIR = ${JSON.stringify(resolve8(options.dir))};
|
|
6956
7612
|
const M = ${JSON.stringify(options.metrics)};
|
|
7613
|
+
// A CSS font's family list (font.ts's FONT_FAMILY_LIST): text is measured in its first family this process draws with, else in Sora.
|
|
7614
|
+
const FAMILIES = new RegExp(${JSON.stringify(FONT_FAMILY_LIST.source)}, 'i');
|
|
6957
7615
|
const MODE = ${JSON.stringify({ poster: options.poster ?? null, phase: options.phase ?? "play", story: options.story === true, at: options.at, width: options.width, height: options.height })};
|
|
6958
7616
|
const { writeFileSync, readFileSync } = await import('node:fs');
|
|
6959
7617
|
const { resolve: resolvePath, sep } = await import('node:path');
|
|
@@ -6971,11 +7629,25 @@ function record(canvas, op, args) {
|
|
|
6971
7629
|
log.push([canvas.id, seq++, op, ...args.map(round)]);
|
|
6972
7630
|
}
|
|
6973
7631
|
const sizeOf = (font) => Number(/(\\d+(?:\\.\\d+)?)px/.exec(String(font))?.[1] ?? 10);
|
|
7632
|
+
const picked = new Map();
|
|
7633
|
+
function metricsOf(font) {
|
|
7634
|
+
const css = String(font);
|
|
7635
|
+
let m = picked.get(css);
|
|
7636
|
+
if (m) return m;
|
|
7637
|
+
m = M.def;
|
|
7638
|
+
for (const part of (FAMILIES.exec(css)?.[1] ?? css).split(',')) {
|
|
7639
|
+
const name = part.trim().replace(/^(['"])(.*)\\1$/, '$2').trim().toLowerCase();
|
|
7640
|
+
if (Object.hasOwn(M.fam, name)) { m = M.list[M.fam[name]]; break; }
|
|
7641
|
+
}
|
|
7642
|
+
if (picked.size < 500) picked.set(css, m);
|
|
7643
|
+
return m;
|
|
7644
|
+
}
|
|
6974
7645
|
function measure(font, text) {
|
|
7646
|
+
const m = metricsOf(font);
|
|
6975
7647
|
const size = sizeOf(font);
|
|
6976
7648
|
let units = 0;
|
|
6977
|
-
for (const ch of String(text)) units +=
|
|
6978
|
-
return (units * size) /
|
|
7649
|
+
for (const ch of String(text)) units += m.adv[ch.codePointAt(0)] ?? m.avg;
|
|
7650
|
+
return (units * size) / m.upm;
|
|
6979
7651
|
}
|
|
6980
7652
|
class RecCanvas {
|
|
6981
7653
|
constructor(w, h) { this.id = nextId++; this._w = Math.max(0, w | 0); this._h = Math.max(0, h | 0); this.style = {}; canvases[this.id] = [this._w, this._h]; this._ctx = null; }
|
|
@@ -7017,7 +7689,8 @@ class RecContext {
|
|
|
7017
7689
|
getLineDash() { return [...this._dash]; }
|
|
7018
7690
|
measureText(text) {
|
|
7019
7691
|
const size = sizeOf(this._state.font);
|
|
7020
|
-
|
|
7692
|
+
const m = metricsOf(this._state.font);
|
|
7693
|
+
return { width: measure(this._state.font, text), actualBoundingBoxAscent: (m.asc / m.upm) * size, actualBoundingBoxDescent: (-m.desc / m.upm) * size, fontBoundingBoxAscent: (m.asc / m.upm) * size, fontBoundingBoxDescent: (-m.desc / m.upm) * size, actualBoundingBoxLeft: 0, actualBoundingBoxRight: measure(this._state.font, text) };
|
|
7021
7694
|
}
|
|
7022
7695
|
/** Everything before a call that covers the whole canvas is gone from it: kept logs stay one frame long. */
|
|
7023
7696
|
_covers(x, y, w, h) {
|
|
@@ -7285,9 +7958,13 @@ function replay(record, font) {
|
|
|
7285
7958
|
}
|
|
7286
7959
|
return { surface: surfaceOf(record.root), problems: [...problems], texts: [...texts] };
|
|
7287
7960
|
}
|
|
7288
|
-
function glyphProblems(texts,
|
|
7289
|
-
const missing =
|
|
7290
|
-
return missing.length > 0 ? [glyphNote(missing)] : [];
|
|
7961
|
+
function glyphProblems(texts, coverage2) {
|
|
7962
|
+
const missing = missingGlyphs(texts, coverage2);
|
|
7963
|
+
return missing.length > 0 ? [glyphNote(missing, coverage2)] : [];
|
|
7964
|
+
}
|
|
7965
|
+
function textNotes(fonts, texts, overlay) {
|
|
7966
|
+
const bars = fonts.sora ? [] : [`${fonts.game.fonts.length > 0 ? "text in Sora" : "text"} is drawn as bars: the engine's font could not be had`];
|
|
7967
|
+
return [...fontNotes({ ...fonts.game, overlay: overlay && fonts.game.overlay }), ...glyphProblems(texts, fonts.coverage), ...bars];
|
|
7291
7968
|
}
|
|
7292
7969
|
function applySet(s, prop, value) {
|
|
7293
7970
|
const v = value;
|
|
@@ -7303,8 +7980,8 @@ function greyBox(s, args) {
|
|
|
7303
7980
|
s.fillRect(dx, dy, dw, dh);
|
|
7304
7981
|
s.restore();
|
|
7305
7982
|
}
|
|
7306
|
-
async function shootHeadless(options,
|
|
7307
|
-
const dir =
|
|
7983
|
+
async function shootHeadless(options, fonts) {
|
|
7984
|
+
const dir = resolve8(options.dir);
|
|
7308
7985
|
const config = findConfigModule(dir);
|
|
7309
7986
|
const project = config ? null : findProject(dir);
|
|
7310
7987
|
if (!config && !project) throw new Error("No game.config.js (export const config = {...}) or project.json here: shot plays the game from its config, as check does.");
|
|
@@ -7312,56 +7989,56 @@ async function shootHeadless(options, font) {
|
|
|
7312
7989
|
const size = options.poster ? posterSize(options.poster, options.width, options.height) : { width: options.width ?? 1280, height: options.height ?? 720 };
|
|
7313
7990
|
const phase = options.poster ? void 0 : options.phase ?? "play";
|
|
7314
7991
|
const at = options.at ?? defaultAt(phase);
|
|
7315
|
-
const temp = mkdtempSync2(
|
|
7992
|
+
const temp = mkdtempSync2(join11(tmpdir4(), "onceworlds-shot-"));
|
|
7316
7993
|
try {
|
|
7317
|
-
const out =
|
|
7318
|
-
const script = recorderScript({ config: config ?? void 0, project: project ?? void 0, out, poster: options.poster, phase, at, ...size, metrics:
|
|
7994
|
+
const out = join11(temp, "record.json");
|
|
7995
|
+
const script = recorderScript({ config: config ?? void 0, project: project ?? void 0, out, poster: options.poster, phase, at, ...size, metrics: metricsTable(fonts.book), dir });
|
|
7319
7996
|
const ran = await runProgram(process.execPath, ["--input-type=module", "--eval", script], dir, 3 * 6e4);
|
|
7320
|
-
if (!
|
|
7997
|
+
if (!existsSync7(out)) {
|
|
7321
7998
|
const tail = ran.output.trimEnd().split("\n").slice(-12).join("\n");
|
|
7322
7999
|
throw new Error(ran.timedOut ? "The game did not finish its frames in 3 minutes." : `The game could not be played headless:
|
|
7323
8000
|
${tail}`);
|
|
7324
8001
|
}
|
|
7325
|
-
const record = JSON.parse(
|
|
8002
|
+
const record = JSON.parse(readFileSync6(out, "utf8"));
|
|
7326
8003
|
if (record.error && !record.three) throw new Error(`The game could not be drawn: ${record.error.split("\n").slice(0, 4).join(" ")}`);
|
|
7327
|
-
const { surface, problems, texts } = replay(record,
|
|
8004
|
+
const { surface, problems, texts } = replay(record, fonts.book);
|
|
7328
8005
|
const png = encodePng(surface.width, surface.height, surface.rgba());
|
|
7329
8006
|
const what = options.poster ? `the "${options.poster}" poster` : headlessMoment(phase ?? "play", record.seconds ?? at, record.phase);
|
|
7330
|
-
const notes = [...problems, ...
|
|
8007
|
+
const notes = [...problems, ...textNotes(fonts, texts, !options.poster), ...record.errors.map((e) => `the game reported: ${e}`)];
|
|
7331
8008
|
return { png, width: surface.width, height: surface.height, caption: [what, ...notes].join("; "), three: record.three };
|
|
7332
8009
|
} finally {
|
|
7333
8010
|
rmSync2(temp, { recursive: true, force: true });
|
|
7334
8011
|
}
|
|
7335
8012
|
}
|
|
7336
|
-
async function storyHeadless(options,
|
|
7337
|
-
const dir =
|
|
8013
|
+
async function storyHeadless(options, fonts) {
|
|
8014
|
+
const dir = resolve8(options.dir);
|
|
7338
8015
|
const config = findConfigModule(dir);
|
|
7339
8016
|
const project = config ? null : findProject(dir);
|
|
7340
8017
|
if (!config && !project) throw new Error("No game.config.js (export const config = {...}) or project.json here: shot plays the game from its config, as check does.");
|
|
7341
8018
|
if (!hasNodeModules(dir)) throw new Error('@onceworlds/engine is not installed here: run "npm install" in the game folder first.');
|
|
7342
8019
|
const size = { width: options.width ?? 960, height: options.height ?? 540 };
|
|
7343
|
-
const temp = mkdtempSync2(
|
|
8020
|
+
const temp = mkdtempSync2(join11(tmpdir4(), "onceworlds-story-"));
|
|
7344
8021
|
try {
|
|
7345
|
-
const out =
|
|
7346
|
-
const script = recorderScript({ config: config ?? void 0, project: project ?? void 0, out, story: true, at: options.at ?? STORY_SECONDS, ...size, metrics:
|
|
8022
|
+
const out = join11(temp, "record.json");
|
|
8023
|
+
const script = recorderScript({ config: config ?? void 0, project: project ?? void 0, out, story: true, at: options.at ?? STORY_SECONDS, ...size, metrics: metricsTable(fonts.book), dir });
|
|
7347
8024
|
const ran = await runProgram(process.execPath, ["--input-type=module", "--eval", script], dir, 5 * 6e4);
|
|
7348
|
-
if (!
|
|
8025
|
+
if (!existsSync7(out)) {
|
|
7349
8026
|
const tail = ran.output.trimEnd().split("\n").slice(-12).join("\n");
|
|
7350
8027
|
throw new Error(ran.timedOut ? "The game did not finish its first minute in 5 minutes." : `The game could not be played headless:
|
|
7351
8028
|
${tail}`);
|
|
7352
8029
|
}
|
|
7353
|
-
const record = JSON.parse(
|
|
8030
|
+
const record = JSON.parse(readFileSync6(out, "utf8"));
|
|
7354
8031
|
if (record.three) return { frames: [], notes: [], three: true };
|
|
7355
8032
|
if (record.error) throw new Error(`The game could not be drawn: ${record.error.split("\n").slice(0, 4).join(" ")}`);
|
|
7356
8033
|
const problems = /* @__PURE__ */ new Set();
|
|
7357
8034
|
const texts = /* @__PURE__ */ new Set();
|
|
7358
8035
|
const frames = (record.frames ?? []).map((f) => {
|
|
7359
|
-
const { surface, problems: p, texts: drawn } = replay({ ops: f.ops, canvases: f.canvases, root: record.root, images: record.images },
|
|
8036
|
+
const { surface, problems: p, texts: drawn } = replay({ ops: f.ops, canvases: f.canvases, root: record.root, images: record.images }, fonts.book);
|
|
7360
8037
|
for (const x of p) problems.add(x);
|
|
7361
8038
|
for (const t of drawn) texts.add(t);
|
|
7362
8039
|
return { label: f.label, seconds: f.seconds, image: surface };
|
|
7363
8040
|
});
|
|
7364
|
-
const notes = [...problems, ...
|
|
8041
|
+
const notes = [...problems, ...textNotes(fonts, texts, true), ...record.errors.map((e) => `the game reported: ${e}`)];
|
|
7365
8042
|
return { frames, notes, three: false };
|
|
7366
8043
|
} finally {
|
|
7367
8044
|
rmSync2(temp, { recursive: true, force: true });
|
|
@@ -7529,15 +8206,16 @@ async function storyBrowser(options, version) {
|
|
|
7529
8206
|
}
|
|
7530
8207
|
async function shootStory(options) {
|
|
7531
8208
|
const version = options.version ?? ENGINE_VERSION;
|
|
7532
|
-
const
|
|
8209
|
+
const fonts = options.browser ? null : await textFonts(options);
|
|
7533
8210
|
let story;
|
|
7534
|
-
if (
|
|
8211
|
+
if (!fonts) story = await storyBrowser(options, version);
|
|
7535
8212
|
else {
|
|
7536
|
-
const headless = await storyHeadless(options,
|
|
8213
|
+
const headless = await storyHeadless(options, fonts);
|
|
7537
8214
|
story = headless.three ? await storyBrowser(options, version) : headless;
|
|
7538
8215
|
}
|
|
7539
8216
|
if (story.frames.length === 0) throw new Error("The game drew nothing to look at in its first minute.");
|
|
7540
|
-
const
|
|
8217
|
+
const sora = fonts ? fonts.sora : await engineFont(options.api, options.fetch, options.version ? [options.version] : []).catch(() => null);
|
|
8218
|
+
const sheet = contactSheet(story.frames, sora);
|
|
7541
8219
|
const frames = story.frames.map((f, i) => {
|
|
7542
8220
|
const surface = f.image instanceof Surface ? f.image : null;
|
|
7543
8221
|
const png = surface ? encodePng(surface.width, surface.height, surface.rgba()) : encodePng(f.image.width, f.image.height, toRgba(f.image.data));
|
|
@@ -7560,16 +8238,15 @@ function toRgba(data) {
|
|
|
7560
8238
|
async function shoot(options) {
|
|
7561
8239
|
const version = options.version ?? ENGINE_VERSION;
|
|
7562
8240
|
if (options.browser) return shootBrowser(options, version);
|
|
7563
|
-
const
|
|
7564
|
-
const headless = await shootHeadless(options, font);
|
|
8241
|
+
const headless = await shootHeadless(options, await textFonts(options));
|
|
7565
8242
|
if (!headless.three) return headless;
|
|
7566
8243
|
return shootBrowser(options, version);
|
|
7567
8244
|
}
|
|
7568
8245
|
|
|
7569
8246
|
// src/assets.ts
|
|
7570
|
-
import { mkdtempSync as mkdtempSync3, readFileSync as
|
|
7571
|
-
import { tmpdir as
|
|
7572
|
-
import { join as
|
|
8247
|
+
import { mkdtempSync as mkdtempSync3, readFileSync as readFileSync7, rmSync as rmSync3, existsSync as existsSync8 } from "node:fs";
|
|
8248
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
8249
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
7573
8250
|
var LIBRARY_EXPORTS = ["assetLibrary", "library", "Library", "assetIndex", "libraryIndex", "ASSET_LIBRARY", "searchAssets"];
|
|
7574
8251
|
function normalizeAsset(raw) {
|
|
7575
8252
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -7627,14 +8304,14 @@ writeFileSync(${JSON.stringify(out)}, JSON.stringify(result, (k, v) => (typeof v
|
|
|
7627
8304
|
`;
|
|
7628
8305
|
}
|
|
7629
8306
|
async function searchAssets(dir, api2, query, options = {}, fetcher = fetch) {
|
|
7630
|
-
const folder =
|
|
8307
|
+
const folder = resolve9(dir);
|
|
7631
8308
|
if (hasNodeModules(folder)) {
|
|
7632
|
-
const temp = mkdtempSync3(
|
|
8309
|
+
const temp = mkdtempSync3(join12(tmpdir5(), "onceworlds-assets-"));
|
|
7633
8310
|
try {
|
|
7634
|
-
const out =
|
|
8311
|
+
const out = join12(temp, "catalog.json");
|
|
7635
8312
|
await runProgram(process.execPath, ["--input-type=module", "--eval", catalogScript(out, query, options)], folder, 6e4);
|
|
7636
|
-
if (
|
|
7637
|
-
const result = JSON.parse(
|
|
8313
|
+
if (existsSync8(out)) {
|
|
8314
|
+
const result = JSON.parse(readFileSync7(out, "utf8"));
|
|
7638
8315
|
if (Array.isArray(result.searched)) return result.searched.map(normalizeAsset).filter((a) => a !== null).slice(0, options.limit ?? 8);
|
|
7639
8316
|
if (Array.isArray(result.entries)) return rankAssets(result.entries.map(normalizeAsset).filter((a) => a !== null), query, options);
|
|
7640
8317
|
}
|
|
@@ -7746,7 +8423,7 @@ function checkProjectConfig(config) {
|
|
|
7746
8423
|
}
|
|
7747
8424
|
|
|
7748
8425
|
// src/index.ts
|
|
7749
|
-
var CLI_VERSION = "0.9.
|
|
8426
|
+
var CLI_VERSION = "0.9.5";
|
|
7750
8427
|
var PRODUCTION_API = "https://onceworlds.com";
|
|
7751
8428
|
var LOCAL_API = "http://localhost:8787";
|
|
7752
8429
|
var USAGE = `Usage:
|
|
@@ -7873,7 +8550,7 @@ async function main() {
|
|
|
7873
8550
|
console.log(USAGE);
|
|
7874
8551
|
return;
|
|
7875
8552
|
}
|
|
7876
|
-
const projectDir =
|
|
8553
|
+
const projectDir = resolve10(projectArg ?? ".");
|
|
7877
8554
|
const api2 = (values.api ?? process.env.ONCEWORLDS_API ?? (command === "dev" ? LOCAL_API : PRODUCTION_API)).replace(/\/+$/, "");
|
|
7878
8555
|
if (command === "api" || command === "recipe" || command === "guide") {
|
|
7879
8556
|
const words3 = positionals.slice(1).join(" ").trim();
|
|
@@ -7995,7 +8672,7 @@ async function dev(projectDir, api2, tokenArg, players = 1) {
|
|
|
7995
8672
|
for (let n = 2; n <= players; n++) console.log(` Player ${n}: ${first.playUrl}?player=${n}&watch`);
|
|
7996
8673
|
console.log(` Watching ${root}. Press Ctrl+C to stop.
|
|
7997
8674
|
`);
|
|
7998
|
-
const projectSignature = async () => signature((await collectFiles(root)).files) + await readFile5(
|
|
8675
|
+
const projectSignature = async () => signature((await collectFiles(root)).files) + await readFile5(join13(projectDir, "onceworlds.json"), "utf8").catch(() => "");
|
|
7999
8676
|
let lastSignature = await projectSignature();
|
|
8000
8677
|
let timer;
|
|
8001
8678
|
let running = false;
|
|
@@ -8028,17 +8705,17 @@ async function dev(projectDir, api2, tokenArg, players = 1) {
|
|
|
8028
8705
|
timer = setTimeout(run, 250);
|
|
8029
8706
|
};
|
|
8030
8707
|
watch(root, { recursive: true }, (_event, filename) => {
|
|
8031
|
-
if (filename && filename.split(
|
|
8708
|
+
if (filename && filename.split(sep3).some((part) => part.startsWith(".") || part === "node_modules")) return;
|
|
8032
8709
|
trigger();
|
|
8033
8710
|
});
|
|
8034
8711
|
await new Promise(() => {
|
|
8035
8712
|
});
|
|
8036
8713
|
}
|
|
8037
8714
|
async function designDoc(projectDir) {
|
|
8038
|
-
return readFile5(
|
|
8715
|
+
return readFile5(join13(projectDir, "DESIGN.md"), "utf8").catch(() => null);
|
|
8039
8716
|
}
|
|
8040
8717
|
async function kitPrintOf(projectDir) {
|
|
8041
|
-
return readKitPrint(await readFile5(
|
|
8718
|
+
return readKitPrint(await readFile5(join13(projectDir, KIT_PRINT_PATH), "utf8").catch(() => null));
|
|
8042
8719
|
}
|
|
8043
8720
|
async function readSources(files) {
|
|
8044
8721
|
const sources = [];
|
|
@@ -8130,7 +8807,7 @@ async function writeControls(projectDir, config, hints) {
|
|
|
8130
8807
|
} catch (err) {
|
|
8131
8808
|
throw new CliError(`The action map gives controls the platform won't take: ${err.message}.`);
|
|
8132
8809
|
}
|
|
8133
|
-
const file =
|
|
8810
|
+
const file = join13(projectDir, "onceworlds.json");
|
|
8134
8811
|
const written = JSON.parse(await readFile5(file, "utf8"));
|
|
8135
8812
|
written.controls = controls;
|
|
8136
8813
|
await writeFile4(file, `${JSON.stringify(written, null, 2)}
|
|
@@ -8145,7 +8822,7 @@ async function check(projectDir, options) {
|
|
|
8145
8822
|
const sources = await readSources(files);
|
|
8146
8823
|
const engine = await isEngineGame(projectDir, config);
|
|
8147
8824
|
if (engine) console.log("Playing the game headless (its tests, then simulated matches with chaos)...");
|
|
8148
|
-
const result = engine ? await checkEngineGame({ dir: projectDir, configFile: options.config, font: await
|
|
8825
|
+
const result = engine ? await checkEngineGame({ dir: projectDir, configFile: options.config, font: await textCoverage(projectDir, options.api).catch(() => null) }) : null;
|
|
8149
8826
|
const findings = projectFindings(config, sources, result?.controls, await assetsOf(files), await configProblems(projectDir, config), await designDoc(projectDir), await kitPrintOf(projectDir));
|
|
8150
8827
|
console.log(`
|
|
8151
8828
|
Checked ${sources.length} file${sources.length === 1 ? "" : "s"} in ${root}.`);
|
|
@@ -8186,8 +8863,8 @@ ${formatBuildTemplates(scenes)}
|
|
|
8186
8863
|
if (values.space !== void 0 && !SPACES.includes(values.space)) throw new CliError(`--space is ${SPACES.join(" or ")}.`);
|
|
8187
8864
|
const folder = dirArg ?? values.slug;
|
|
8188
8865
|
if (!folder) throw new CliError("Say which folder to make: onceworlds new <folder> (blank), or add --kit <name> to start from a kit (--list shows them).");
|
|
8189
|
-
const dir =
|
|
8190
|
-
const source =
|
|
8866
|
+
const dir = resolve10(folder);
|
|
8867
|
+
const source = basename5(dir);
|
|
8191
8868
|
const slug = values.slug ?? slugFrom(source);
|
|
8192
8869
|
if (!slug) throw new CliError(`"${source}" can't be a game's link: pass --slug (lowercase letters, digits and dashes).`);
|
|
8193
8870
|
const made = await newGame({ api: api2, dir, slug, name: values.name ?? titleFrom(source), description: values.description, kit: values.kit, template: values.template, space: values.space });
|
|
@@ -8215,14 +8892,14 @@ Next:
|
|
|
8215
8892
|
Read AGENTS.md: the guide for you and your AI tool. Look the engine up with onceworlds api <name> and onceworlds recipe.`);
|
|
8216
8893
|
}
|
|
8217
8894
|
function publishRoot(projectDir, config) {
|
|
8218
|
-
const root =
|
|
8219
|
-
if (root !== projectDir && !root.startsWith(projectDir +
|
|
8895
|
+
const root = resolve10(projectDir, config.dir ?? ".");
|
|
8896
|
+
if (root !== projectDir && !root.startsWith(projectDir + sep3)) {
|
|
8220
8897
|
throw new CliError(`"dir" in onceworlds.json (${config.dir}) must be a folder inside the project.`);
|
|
8221
8898
|
}
|
|
8222
8899
|
return root;
|
|
8223
8900
|
}
|
|
8224
8901
|
async function loadConfig(projectDir, options = {}) {
|
|
8225
|
-
const file =
|
|
8902
|
+
const file = join13(projectDir, "onceworlds.json");
|
|
8226
8903
|
let config;
|
|
8227
8904
|
try {
|
|
8228
8905
|
config = JSON.parse(await readFile5(file, "utf8"));
|
|
@@ -8263,8 +8940,8 @@ function storeImages(config) {
|
|
|
8263
8940
|
}
|
|
8264
8941
|
async function readStoreImage(projectDir, value, slot, field2) {
|
|
8265
8942
|
if (typeof value !== "string" || !value.trim()) return { problem: `"${field2}" in onceworlds.json must be the path of an image, like "store/icon.png".` };
|
|
8266
|
-
const abs =
|
|
8267
|
-
if (!abs.startsWith(projectDir +
|
|
8943
|
+
const abs = resolve10(projectDir, value);
|
|
8944
|
+
if (!abs.startsWith(projectDir + sep3)) return { problem: `"${field2}" in onceworlds.json (${value}) must be inside the project folder.` };
|
|
8268
8945
|
let data;
|
|
8269
8946
|
try {
|
|
8270
8947
|
data = await readFile5(abs);
|
|
@@ -8276,7 +8953,7 @@ async function readStoreImage(projectDir, value, slot, field2) {
|
|
|
8276
8953
|
if (!info) return { problem: `${value} ("${field2}") must be a PNG, JPEG, WebP or GIF.` };
|
|
8277
8954
|
const problem = imageSizeProblem(slot, info.width, info.height);
|
|
8278
8955
|
if (problem) return { problem: `${value} ("${field2}") is ${info.width}\xD7${info.height}. ${problem}` };
|
|
8279
|
-
return { file: { path: value, abs, hash:
|
|
8956
|
+
return { file: { path: value, abs, hash: createHash4("sha256").update(data).digest("hex"), size: data.byteLength } };
|
|
8280
8957
|
}
|
|
8281
8958
|
async function loadStore(projectDir, config) {
|
|
8282
8959
|
const images = /* @__PURE__ */ new Map();
|
|
@@ -8319,7 +8996,7 @@ async function collectFiles(root) {
|
|
|
8319
8996
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8320
8997
|
if (entry.name.startsWith(".") || QUIET_SKIPS.has(entry.name) || TEST_FILE.test(entry.name)) continue;
|
|
8321
8998
|
if (entry.isDirectory() && (prefix === "" ? DEV_FOLDERS.has(entry.name) : entry.name === "__tests__")) continue;
|
|
8322
|
-
const abs =
|
|
8999
|
+
const abs = join13(dir, entry.name);
|
|
8323
9000
|
const path = prefix + entry.name;
|
|
8324
9001
|
if (entry.isDirectory()) {
|
|
8325
9002
|
await walk(abs, `${path}/`);
|
|
@@ -8332,7 +9009,7 @@ async function collectFiles(root) {
|
|
|
8332
9009
|
if (data.byteLength > LIMITS.deployFileBytes) {
|
|
8333
9010
|
throw new CliError(`${path} is ${formatBytes(data.byteLength)}; files can be at most ${formatBytes(LIMITS.deployFileBytes)}.`);
|
|
8334
9011
|
}
|
|
8335
|
-
files.push({ path, abs, hash:
|
|
9012
|
+
files.push({ path, abs, hash: createHash4("sha256").update(data).digest("hex"), size: data.byteLength });
|
|
8336
9013
|
}
|
|
8337
9014
|
}
|
|
8338
9015
|
}
|
|
@@ -8388,7 +9065,7 @@ async function whoami(api2) {
|
|
|
8388
9065
|
}
|
|
8389
9066
|
async function runPlaytest(projectDir, api2, options) {
|
|
8390
9067
|
const { url, draft, chrome } = await draftToPlay(projectDir, api2, "Playtesting");
|
|
8391
|
-
const pictures = options.pictures ?
|
|
9068
|
+
const pictures = options.pictures ? join13(projectDir, "store") : void 0;
|
|
8392
9069
|
const result = await playtest({
|
|
8393
9070
|
chrome,
|
|
8394
9071
|
url,
|
|
@@ -8417,12 +9094,12 @@ async function runPosters(projectDir, api2, only) {
|
|
|
8417
9094
|
const result = await capturePosters({
|
|
8418
9095
|
chrome,
|
|
8419
9096
|
url,
|
|
8420
|
-
dir:
|
|
9097
|
+
dir: join13(projectDir, "store"),
|
|
8421
9098
|
only: only ? only.split(",").map((name) => name.trim()).filter(Boolean) : void 0,
|
|
8422
9099
|
log: (name) => console.error(` posters: ${name}`)
|
|
8423
9100
|
});
|
|
8424
9101
|
const lines = [formatPosters(result)];
|
|
8425
|
-
const file =
|
|
9102
|
+
const file = join13(projectDir, "onceworlds.json");
|
|
8426
9103
|
const config = JSON.parse(await readFile5(file, "utf8"));
|
|
8427
9104
|
const named = listingFrom(result.saved, config);
|
|
8428
9105
|
if (named.icon || named.thumbnails) {
|
|
@@ -8449,11 +9126,11 @@ async function runShot(projectDir, api2, values) {
|
|
|
8449
9126
|
const story = await shootStory({ dir: projectDir, api: api2, at, ...sizeArg(values.size), browser: values.browser === true }).catch((err) => {
|
|
8450
9127
|
throw new CliError(err.message);
|
|
8451
9128
|
});
|
|
8452
|
-
const folder =
|
|
9129
|
+
const folder = resolve10(projectDir, "store", "story");
|
|
8453
9130
|
await mkdir4(folder, { recursive: true });
|
|
8454
|
-
for (const frame of story.frames) await writeFile4(
|
|
8455
|
-
const sheet =
|
|
8456
|
-
await mkdir4(
|
|
9131
|
+
for (const frame of story.frames) await writeFile4(join13(folder, frame.file), frame.png);
|
|
9132
|
+
const sheet = resolve10(projectDir, values.out ?? join13("store", "story.png"));
|
|
9133
|
+
await mkdir4(dirname9(sheet), { recursive: true });
|
|
8457
9134
|
await writeFile4(sheet, story.sheet.png);
|
|
8458
9135
|
console.log(`Saved ${sheet} (${story.sheet.width}x${story.sheet.height}) and ${story.frames.length} frames in ${folder}: ${story.sheet.caption}.`);
|
|
8459
9136
|
console.log(`
|
|
@@ -8468,13 +9145,13 @@ ${STORY_CHECKLIST.map((q) => ` - ${q}`).join("\n")}`);
|
|
|
8468
9145
|
const shot = await shoot({ dir: projectDir, api: api2, poster: values.poster, phase, at, ...sizeArg(values.size), browser: values.browser === true }).catch((err) => {
|
|
8469
9146
|
throw new CliError(err.message);
|
|
8470
9147
|
});
|
|
8471
|
-
const file =
|
|
8472
|
-
await mkdir4(
|
|
9148
|
+
const file = resolve10(projectDir, values.out ?? join13("store", values.poster ? `shot-${values.poster}.png` : phase && phase !== "play" ? `shot-${phase}.png` : "shot.png"));
|
|
9149
|
+
await mkdir4(dirname9(file), { recursive: true });
|
|
8473
9150
|
await writeFile4(file, shot.png);
|
|
8474
9151
|
console.log(`Saved ${file} (${shot.width}x${shot.height}): ${shot.caption}.`);
|
|
8475
9152
|
}
|
|
8476
9153
|
async function servedRoot(projectDir) {
|
|
8477
|
-
if (!
|
|
9154
|
+
if (!existsSync9(join13(projectDir, "onceworlds.json"))) return { root: projectDir };
|
|
8478
9155
|
const config = await loadConfig(projectDir, { strict: false });
|
|
8479
9156
|
return { root: publishRoot(projectDir, config), name: config.name };
|
|
8480
9157
|
}
|
|
@@ -8489,7 +9166,7 @@ async function runServe(projectDir, api2, values) {
|
|
|
8489
9166
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("--port is a port number (0 for any free one).");
|
|
8490
9167
|
if (!(await stat2(projectDir).catch(() => null))?.isDirectory()) throw new CliError(`${projectDir} isn't a folder.`);
|
|
8491
9168
|
const { root, name } = await servedRoot(projectDir);
|
|
8492
|
-
if (!
|
|
9169
|
+
if (!existsSync9(join13(root, "index.html"))) throw new CliError(`No index.html in ${root}: serve plays the page a game starts from.`);
|
|
8493
9170
|
const served = await serveFolder({ dir: root, api: api2, version: ENGINE_VERSION, port });
|
|
8494
9171
|
const { lines } = servedNotes(served, { bots, name });
|
|
8495
9172
|
console.log(`${lines.join("\n")}
|
|
@@ -8498,7 +9175,7 @@ async function runServe(projectDir, api2, values) {
|
|
|
8498
9175
|
});
|
|
8499
9176
|
}
|
|
8500
9177
|
async function namePictures(projectDir) {
|
|
8501
|
-
const file =
|
|
9178
|
+
const file = join13(projectDir, "onceworlds.json");
|
|
8502
9179
|
const config = JSON.parse(await readFile5(file, "utf8"));
|
|
8503
9180
|
let changed = false;
|
|
8504
9181
|
if (!config.icon) {
|
|
@@ -8525,7 +9202,7 @@ function mcpHost(api2) {
|
|
|
8525
9202
|
},
|
|
8526
9203
|
engineCheck: async (dir) => {
|
|
8527
9204
|
const config = await loadConfig(dir);
|
|
8528
|
-
return await isEngineGame(dir, config) ? checkEngineGame({ dir, font: await
|
|
9205
|
+
return await isEngineGame(dir, config) ? checkEngineGame({ dir, font: await textCoverage(dir, api2).catch(() => null) }) : null;
|
|
8529
9206
|
},
|
|
8530
9207
|
playtest: (dir, options) => runPlaytest(dir, api2, options),
|
|
8531
9208
|
index: async (dir) => (await loadIndex({ dir, api: api2 })).index,
|
|
@@ -8538,7 +9215,7 @@ function mcpHost(api2) {
|
|
|
8538
9215
|
searchAssets: (dir, query, options) => searchAssets(dir, api2, query, options),
|
|
8539
9216
|
serve: async (dir, options) => {
|
|
8540
9217
|
const { root, name } = await servedRoot(dir);
|
|
8541
|
-
if (!
|
|
9218
|
+
if (!existsSync9(join13(root, "index.html"))) throw new CliError(`No index.html in ${root}: serve plays the page a game starts from.`);
|
|
8542
9219
|
let served = serving.get(root);
|
|
8543
9220
|
if (!served) {
|
|
8544
9221
|
served = await serveFolder({ dir: root, api: api2, version: ENGINE_VERSION, port: 0 });
|