@onceworlds/cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +228 -83
  2. package/dist/emulator.js +1066 -151
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // src/index.ts
4
4
  import { execFileSync as execFileSync2 } from "node:child_process";
5
5
  import { createHash as createHash5 } from "node:crypto";
6
- import { existsSync as existsSync12, watch } from "node:fs";
7
- import { mkdir as mkdir4, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
6
+ import { createReadStream, existsSync as existsSync12, watch } from "node:fs";
7
+ import { mkdir as mkdir4, open, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
8
8
  import { basename as basename6, dirname as dirname10, join as join17, resolve as resolve12, sep as sep5 } from "node:path";
9
9
  import { parseArgs } from "node:util";
10
10
 
@@ -861,10 +861,10 @@ function pageScripts(html) {
861
861
  comment = html.indexOf("<!--", at);
862
862
  continue;
863
863
  }
864
- const open = html.indexOf(">", script);
865
- const close = open === -1 ? -1 : find(closing2, open);
864
+ const open2 = html.indexOf(">", script);
865
+ const close = open2 === -1 ? -1 : find(closing2, open2);
866
866
  if (close === -1) return scripts;
867
- scripts.push({ tag: html.slice(script, open + 1), body: html.slice(open + 1, close), at: open + 1 });
867
+ scripts.push({ tag: html.slice(script, open2 + 1), body: html.slice(open2 + 1, close), at: open2 + 1 });
868
868
  at = close + 8;
869
869
  if (comment !== -1 && comment < at) comment = html.indexOf("<!--", at);
870
870
  }
@@ -1610,17 +1610,17 @@ function keyName(key) {
1610
1610
  return key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
1611
1611
  }
1612
1612
  var PRESET_KEYS = { wasd: ["W", "A", "S", "D"], ijkl: ["I", "J", "K", "L"], arrows: ["Arrows"] };
1613
- function objectAt(text2, open) {
1613
+ function objectAt(text2, open2) {
1614
1614
  let depth = 0;
1615
1615
  let quote = "";
1616
- for (let i = open; i < text2.length; i++) {
1616
+ for (let i = open2; i < text2.length; i++) {
1617
1617
  const c = text2[i];
1618
1618
  if (quote) {
1619
1619
  if (c === "\\") i++;
1620
1620
  else if (c === quote) quote = "";
1621
1621
  } else if (c === '"' || c === "'" || c === "`") quote = c;
1622
1622
  else if (c === "{") depth++;
1623
- else if (c === "}" && --depth === 0) return text2.slice(open, i + 1);
1623
+ else if (c === "}" && --depth === 0) return text2.slice(open2, i + 1);
1624
1624
  }
1625
1625
  return null;
1626
1626
  }
@@ -2324,9 +2324,9 @@ var SCREENS2 = [
2324
2324
  ];
2325
2325
  var DEFAULT_FONT = "sora";
2326
2326
  var lineAt = (text2, index) => text2.slice(0, index).split("\n").length;
2327
- function objectAt2(file, code, open) {
2328
- const end = closing(code, open);
2329
- return end < 0 ? null : { text: file.text.slice(open, end + 1), code: code.slice(open, end + 1), from: open };
2327
+ function objectAt2(file, code, open2) {
2328
+ const end = closing(code, open2);
2329
+ return end < 0 ? null : { text: file.text.slice(open2, end + 1), code: code.slice(open2, end + 1), from: open2 };
2330
2330
  }
2331
2331
  function constObject(files, name) {
2332
2332
  const re = new RegExp(`(?:const|let|var)\\s+${name.replace(/\$/g, "\\$")}\\s*=\\s*\\{`);
@@ -2414,8 +2414,8 @@ function designFindings(input) {
2414
2414
  if (f && !flow) flow = { file, code, at: f.index, shape: f[1] };
2415
2415
  for (const m of code.matchAll(/(?<![\w$.])UI\s*\(\s*(\{)?/g)) {
2416
2416
  if (ui) break;
2417
- const open = (m.index ?? 0) + m[0].length - 1;
2418
- ui = { file, code, at: m.index ?? 0, options: m[1] ? objectAt2(file, code, open) : null };
2417
+ const open2 = (m.index ?? 0) + m[0].length - 1;
2418
+ ui = { file, code, at: m.index ?? 0, options: m[1] ? objectAt2(file, code, open2) : null };
2419
2419
  }
2420
2420
  }
2421
2421
  if (!flow || !ui) return found;
@@ -2490,10 +2490,10 @@ function designFindings(input) {
2490
2490
  const start = (m.index ?? 0) + m[0].length;
2491
2491
  const name = file.text.slice(start, file.text.indexOf(m[1], start));
2492
2492
  if (name === "flow.countdown") continue;
2493
- const open = (m.index ?? 0) + m[0].length - m[1].length - 1;
2493
+ const open2 = (m.index ?? 0) + m[0].length - m[1].length - 1;
2494
2494
  const end = closing(code, code.indexOf("(", m.index ?? 0));
2495
2495
  if (end < 0) continue;
2496
- const span = file.text.slice(open, end);
2496
+ const span = file.text.slice(open2, end);
2497
2497
  const countdown = /phase\s*[!=]==?\s*['"]countdown['"]|['"]countdown['"]\s*===?\s*[\w$.?]*phase|\bflow\??\.countdown\b/.test(span);
2498
2498
  const digits = /Math\s*\.\s*ceil\s*\(|['"`]GO!?['"`]/.test(span);
2499
2499
  if (countdown && digits) {
@@ -2634,10 +2634,10 @@ function blank(text2) {
2634
2634
  }
2635
2635
  return out.join("");
2636
2636
  }
2637
- function closing(code, open) {
2637
+ function closing(code, open2) {
2638
2638
  const pairs = { "(": ")", "[": "]", "{": "}" };
2639
2639
  const stack = [];
2640
- for (let i = open; i < code.length; i++) {
2640
+ for (let i = open2; i < code.length; i++) {
2641
2641
  const c = code[i];
2642
2642
  if (pairs[c]) stack.push(pairs[c]);
2643
2643
  else if (c === ")" || c === "]" || c === "}") {
@@ -2717,30 +2717,30 @@ function functionBody(code, from, to, name) {
2717
2717
  function regions(text2, code) {
2718
2718
  const out = [];
2719
2719
  for (const m of code.matchAll(/\b(defineSystem|defineBotSystem|script|Flow\s*\.\s*(?:rounds|turns|coop|dropIn))\s*\(\s*\{/g)) {
2720
- const open = (m.index ?? 0) + m[0].length - 1;
2721
- const end = closing(code, open);
2720
+ const open2 = (m.index ?? 0) + m[0].length - 1;
2721
+ const end = closing(code, open2);
2722
2722
  if (end < 0) continue;
2723
2723
  const which = m[1].replace(/\s/g, "");
2724
- const name = stringProp(text2, code, open, end, "name") ?? which;
2724
+ const name = stringProp(text2, code, open2, end, "name") ?? which;
2725
2725
  if (which === "defineSystem" || which === "defineBotSystem") {
2726
- const stage = stringProp(text2, code, open, end, "stage") ?? (which === "defineBotSystem" ? "fixed" : "update");
2727
- const host = which === "defineBotSystem" || stringProp(text2, code, open, end, "authority") === "host";
2726
+ const stage = stringProp(text2, code, open2, end, "stage") ?? (which === "defineBotSystem" ? "fixed" : "update");
2727
+ const host = which === "defineBotSystem" || stringProp(text2, code, open2, end, "authority") === "host";
2728
2728
  for (const fn of which === "defineBotSystem" ? ["think", "act"] : ["run"]) {
2729
- const body = functionBody(code, open, end, fn);
2730
- if (body) out.push({ from: body[0], to: body[1], kind: "system", name, stage, host, defFrom: open, defTo: end });
2729
+ const body = functionBody(code, open2, end, fn);
2730
+ if (body) out.push({ from: body[0], to: body[1], kind: "system", name, stage, host, defFrom: open2, defTo: end });
2731
2731
  }
2732
2732
  } else if (which === "script") {
2733
2733
  for (const hook of ["fixed", "update", "ready"]) {
2734
- const body = functionBody(code, open, end, hook);
2735
- if (body) out.push({ from: body[0], to: body[1], kind: "script", name: hook, stage: hook === "fixed" ? "fixed" : "update", host: stringProp(text2, code, open, end, "authority") === "host", defFrom: open, defTo: end });
2734
+ const body = functionBody(code, open2, end, hook);
2735
+ if (body) out.push({ from: body[0], to: body[1], kind: "script", name: hook, stage: hook === "fixed" ? "fixed" : "update", host: stringProp(text2, code, open2, end, "authority") === "host", defFrom: open2, defTo: end });
2736
2736
  }
2737
2737
  } else {
2738
2738
  for (const rule of ["setup", "populate", "spawn", "isOver", "rank", "end", "begin", "bot"]) {
2739
2739
  const re = new RegExp(`(?<![\\w$.])${rule}\\s*(\\(|:)`, "g");
2740
- re.lastIndex = open;
2740
+ re.lastIndex = open2;
2741
2741
  for (let r = re.exec(code); r && r.index < end; r = re.exec(code)) {
2742
- const body = functionBody(code, Math.max(open, enclosing(code, r.index)), end, rule);
2743
- if (body) out.push({ from: body[0], to: body[1], kind: "rules", name: rule, stage: "fixed", host: rule === "populate" || rule === "isOver" || rule === "rank" || rule === "end", defFrom: open, defTo: end });
2742
+ const body = functionBody(code, Math.max(open2, enclosing(code, r.index)), end, rule);
2743
+ if (body) out.push({ from: body[0], to: body[1], kind: "rules", name: rule, stage: "fixed", host: rule === "populate" || rule === "isOver" || rule === "rank" || rule === "end", defFrom: open2, defTo: end });
2744
2744
  }
2745
2745
  }
2746
2746
  }
@@ -2787,10 +2787,10 @@ function lintGame(input) {
2787
2787
  for (const file of scripts) {
2788
2788
  const code = blank(file.text);
2789
2789
  for (const m of code.matchAll(/(?:const|let|var)\s+([\w$]+)\s*=\s*defineComponent\s*\(/g)) {
2790
- const open = code.indexOf("(", m.index ?? 0);
2791
- const end = closing(code, open);
2790
+ const open2 = code.indexOf("(", m.index ?? 0);
2791
+ const end = closing(code, open2);
2792
2792
  if (end < 0) continue;
2793
- if (/\bowner\s*:\s*['"]host['"]/.test(file.text.slice(open, end))) hostComponents.add(m[1]);
2793
+ if (/\bowner\s*:\s*['"]host['"]/.test(file.text.slice(open2, end))) hostComponents.add(m[1]);
2794
2794
  }
2795
2795
  }
2796
2796
  for (const file of scripts) {
@@ -2847,16 +2847,16 @@ function lintGame(input) {
2847
2847
  }
2848
2848
  }
2849
2849
  for (const m of code.matchAll(/\bwhile\s*\(\s*(?:true|1)\s*\)|\bfor\s*\(\s*;\s*;\s*\)/g)) {
2850
- const open = code.indexOf("{", m.index ?? 0);
2851
- const end = open < 0 ? -1 : closing(code, open);
2852
- const body = end < 0 ? "" : code.slice(open, end);
2850
+ const open2 = code.indexOf("{", m.index ?? 0);
2851
+ const end = open2 < 0 ? -1 : closing(code, open2);
2852
+ const body = end < 0 ? "" : code.slice(open2, end);
2853
2853
  if (!/\b(break|return|throw)\b/.test(body)) add(m.index ?? 0, "warn", "unbounded-spawn", "A loop with no break or return: it hangs the page. Give it an end, or do the work a step at a time in a system.");
2854
2854
  }
2855
2855
  const actionsFound = [];
2856
2856
  for (const m of code.matchAll(/(?<![\w$])([\w$]+)\s*:\s*Input\s*\.\s*(button|axis2d|pointer)\s*\(\s*(\{)?/g)) {
2857
- const open = (m.index ?? 0) + m[0].length - 1;
2858
- const end = m[3] ? closing(code, open) : open;
2859
- const options = m[3] && end > 0 ? code.slice(open, end) : "";
2857
+ const open2 = (m.index ?? 0) + m[0].length - 1;
2858
+ const end = m[3] ? closing(code, open2) : open2;
2859
+ const options = m[3] && end > 0 ? code.slice(open2, end) : "";
2860
2860
  const touch = m[2] === "pointer" || (m[2] === "button" ? /(?<![\w$])touch\s*:/.test(options) : /(?<![\w$])stick\s*:\s*(?!false)/.test(options));
2861
2861
  actionsFound.push({ name: m[1], kind: m[2], touch, at: m.index ?? 0 });
2862
2862
  }
@@ -5387,6 +5387,106 @@ import { tmpdir as tmpdir2 } from "node:os";
5387
5387
  import { dirname as dirname6, join as join8, resolve as resolve4 } from "node:path";
5388
5388
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
5389
5389
 
5390
+ // ../protocol/src/about.ts
5391
+ var ABOUT_LIMITS = {
5392
+ sourceLength: 2e4,
5393
+ blocks: 300,
5394
+ images: 40,
5395
+ videos: 6,
5396
+ youtube: 6,
5397
+ buttons: 12,
5398
+ links: 60,
5399
+ headings: 40,
5400
+ /** Pictures and videos uploaded for one game's About. */
5401
+ files: 40,
5402
+ fileNameLength: 40,
5403
+ headingLength: 100,
5404
+ captionLength: 200,
5405
+ titleLength: 80,
5406
+ buttonLabelLength: 40,
5407
+ urlLength: 500,
5408
+ columnsMin: 2,
5409
+ columnsMax: 3,
5410
+ galleryMin: 2,
5411
+ galleryMax: 12,
5412
+ listItems: 40,
5413
+ tableRows: 30,
5414
+ tableColumns: 5,
5415
+ imageMinSide: 32,
5416
+ imageMaxSide: 4096
5417
+ };
5418
+ var BLOCK_COMMANDS = /* @__PURE__ */ new Set(["section", "subsection", "image", "video", "youtube", "button", "divider", "begin", "end", "item", "column", "head", "row"]);
5419
+ var INLINE_WRAPS = /* @__PURE__ */ new Set(["b", "i", "spoiler"]);
5420
+ var KNOWN = [...BLOCK_COMMANDS, ...INLINE_WRAPS, "link"];
5421
+ var ABOUT_COMMANDS = [
5422
+ { group: "Text", syntax: "A blank line", does: "Starts a new paragraph." },
5423
+ { group: "Text", syntax: "\\section{Title}", does: "A heading.", tool: { label: "Heading", insert: "\\section{$Title$}\n" } },
5424
+ { group: "Text", syntax: "\\subsection{Title}", does: "A smaller heading." },
5425
+ { group: "Text", syntax: "\\b{bold} \\i{italic}", does: "Bold and italic.", tool: { label: "Bold", insert: "\\b{$text$}" } },
5426
+ { group: "Text", syntax: "\\link{text}{https://...}", does: "A link. Also /games/name or /users/name.", tool: { label: "Link", insert: "\\link{$text$}{https://}" } },
5427
+ { group: "Text", syntax: "\\begin{itemize} \\item One \\end{itemize}", does: "A list. enumerate numbers it.", tool: { label: "List", insert: "\\begin{itemize}\n\\item $One$\n\\item Two\n\\end{itemize}\n" } },
5428
+ { group: "Text", syntax: "\\begin{quote}[Who] ... \\end{quote}", does: "A quote. Who said it is optional." },
5429
+ { group: "Text", syntax: "\\spoiler{text}", does: "Hidden until hovered or focused." },
5430
+ { group: "Text", syntax: "\\\\", does: "A line break." },
5431
+ { group: "Text", syntax: "% note", does: "A comment, at the start of a line: not shown." },
5432
+ { group: "Text", syntax: "\\{ \\} \\% \\[ \\]", does: "The character itself." },
5433
+ { group: "Media", syntax: "\\image{name}{caption}", does: "A picture you uploaded, at its own size up to the page\u2019s width. The caption is optional.", tool: { label: "Image", insert: "\\image{$name$}\n" } },
5434
+ { group: "Media", syntax: "\\video{name}{caption}", does: "A video you uploaded. It plays when pressed.", tool: { label: "Video", insert: "\\video{$name$}\n" } },
5435
+ { group: "Media", syntax: "\\video[loop]{name}", does: "Plays muted on a loop while in view, like a GIF." },
5436
+ { group: "Media", syntax: "\\youtube{address}", does: "A YouTube video. \\youtube[name]{address} shows your picture until it\u2019s played.", tool: { label: "YouTube", insert: "\\youtube{$https://youtu.be/$}\n" } },
5437
+ { group: "Media", syntax: "\\begin{gallery} \\image{a} \\image{b} \\end{gallery}", does: "Pictures side by side." },
5438
+ { group: "Layout", syntax: "\\button{Label}{https://...}", does: "A button. Buttons in a row sit together.", tool: { label: "Button", insert: "\\button{$Label$}{https://}\n" } },
5439
+ { group: "Layout", syntax: "\\button{Play now}{play}", does: "Plays this game." },
5440
+ { group: "Layout", syntax: "\\begin{columns} \\column ... \\column ... \\end{columns}", does: "2 or 3 columns, stacked on phones." },
5441
+ { group: "Layout", syntax: "\\begin{table} \\head{A}{B} \\row{1}{2} \\end{table}", does: "A table, up to 5 columns." },
5442
+ { group: "Layout", syntax: "\\begin{fold}{Title} ... \\end{fold}", does: "Closed until opened." },
5443
+ { group: "Layout", syntax: "\\divider", does: "A line across." }
5444
+ ];
5445
+ var ABOUT_EXAMPLE = `Hold the castle until dawn. Up to eight friends, one wall, and a night that gets longer every time.
5446
+
5447
+ \\image{castle}
5448
+
5449
+ \\section{Features}
5450
+
5451
+ \\begin{itemize}
5452
+ \\item \\b{Build between nights.} Walls, traps and towers, rebuilt however you like.
5453
+ \\item \\b{Twelve bosses.} Each has a weak spot, and none of them tell you where.
5454
+ \\item \\b{Raid your friends.} Your castle is a level they can attack.
5455
+ \\end{itemize}
5456
+
5457
+ \\video[loop]{night-raid}
5458
+
5459
+ \\section{Credits}
5460
+
5461
+ Made by two people. Music by a very kind friend.
5462
+
5463
+ \\button{Join the Discord}{https://discord.gg/example}
5464
+ `;
5465
+ function aboutGuide() {
5466
+ const groups = ["Text", "Media", "Layout"];
5467
+ const L = ABOUT_LIMITS;
5468
+ return [
5469
+ "# About (OnceTeX)",
5470
+ "",
5471
+ "A game page has a short description and, above How to play, an About section the creator writes in OnceTeX, a small LaTeX-like language.",
5472
+ "Write it in the dashboard: the game's About tab, with a live preview. There is no HTML or CSS: the commands below are all there is. Every game's text looks the same, so the pictures do the designing.",
5473
+ "",
5474
+ ...groups.flatMap((group) => [`## ${group}`, "", ...ABOUT_COMMANDS.filter((c) => c.group === group).map((c) => `- \`${c.syntax}\`: ${c.does}`), ""]),
5475
+ "## Rules",
5476
+ "",
5477
+ `- Pictures and videos are the game's own uploads, up to ${L.files}, used by name (\`castle.png\` is \`castle\`). Outside pictures can't be shown.`,
5478
+ `- Limits: ${L.sourceLength.toLocaleString("en")} characters, ${L.images} pictures, ${L.videos} videos, ${L.youtube} YouTube videos, ${L.buttons} buttons, ${L.links} links, ${L.headings} headings.`,
5479
+ "- Links go in `\\link` and `\\button` only: an address, an email or a handle in plain text is refused.",
5480
+ "- Columns hold text, media, buttons, lists and quotes.",
5481
+ "",
5482
+ "## Example",
5483
+ "",
5484
+ "```latex",
5485
+ ABOUT_EXAMPLE.trimEnd(),
5486
+ "```"
5487
+ ].join("\n");
5488
+ }
5489
+
5390
5490
  // ../protocol/src/listing.ts
5391
5491
  function badgeIdMaxLength() {
5392
5492
  let n = 1;
@@ -5444,7 +5544,13 @@ function listingTopics() {
5444
5544
  "The creator can also add badges on the dashboard."
5445
5545
  ].join("\n")
5446
5546
  };
5447
- return [genres, badges];
5547
+ const about = {
5548
+ name: "about",
5549
+ title: "About",
5550
+ summary: "The long section of a game page, with pictures, videos, links and buttons: written in OnceTeX in the dashboard.",
5551
+ text: aboutGuide()
5552
+ };
5553
+ return [genres, badges, about];
5448
5554
  }
5449
5555
 
5450
5556
  // src/lookup.ts
@@ -7539,8 +7645,8 @@ var LocalRoom = class {
7539
7645
  }
7540
7646
  hostOpen(ws, me, msg, config) {
7541
7647
  if (!this.isHost(me.id)) return this.error(ws, "not_host", "Only the host can open or close the server");
7542
- const open = Boolean(msg.open);
7543
- if (config.pool && !open && this.mrec.phase === "lobby") {
7648
+ const open2 = Boolean(msg.open);
7649
+ if (config.pool && !open2 && this.mrec.phase === "lobby") {
7544
7650
  if (!this.takeHostAction(me.id, "host")) return this.error(ws, "slow_down", "Too many changes at once.");
7545
7651
  const start = closingStart(this.lobbyView(config));
7546
7652
  if ("code" in start) return this.error(ws, start.code, start.message);
@@ -7550,11 +7656,11 @@ var LocalRoom = class {
7550
7656
  this.stepMatch();
7551
7657
  return;
7552
7658
  }
7553
- if (config.pool && open && this.mrec.byOpen && this.mrec.phase !== "lobby") return this.hostEnd(ws, me, config);
7554
- if (config.open === open) return;
7659
+ if (config.pool && open2 && this.mrec.byOpen && this.mrec.phase !== "lobby") return this.hostEnd(ws, me, config);
7660
+ if (config.open === open2) return;
7555
7661
  if (!this.takeHostAction(me.id, "host")) return this.error(ws, "slow_down", "Too many changes at once.");
7556
- config.open = open;
7557
- this.broadcast({ t: "open", open });
7662
+ config.open = open2;
7663
+ this.broadcast({ t: "open", open: open2 });
7558
7664
  this.hub.changed();
7559
7665
  }
7560
7666
  hostEnd(ws, me, config) {
@@ -9538,16 +9644,16 @@ function fromScript(text2, addFont) {
9538
9644
  const taken = /* @__PURE__ */ new Set();
9539
9645
  const lists = [];
9540
9646
  for (const m of code.matchAll(/(?<![\w$])([\w$]*fonts)\s*(?::|=(?![=>]))\s*\[/gi)) {
9541
- const open = m.index + m[0].length - 1;
9542
- const end = closing(code, open);
9647
+ const open2 = m.index + m[0].length - 1;
9648
+ const end = closing(code, open2);
9543
9649
  if (end < 0) continue;
9544
- lists.push([open, end]);
9545
- for (let i = open + 1; i < end; i++) {
9650
+ lists.push([open2, end]);
9651
+ for (let i = open2 + 1; i < end; i++) {
9546
9652
  const c = code[i];
9547
9653
  if (c !== "'" && c !== '"' && c !== "`") continue;
9548
9654
  const literal = literalAt(text2, code, i);
9549
9655
  if (!literal) break;
9550
- const before = code.slice(open, i).trimEnd();
9656
+ const before = code.slice(open2, i).trimEnd();
9551
9657
  if ((before.endsWith("[") || before.endsWith(",")) && !files.has(i)) add(i, literal.value, void 0, literal.dynamic);
9552
9658
  i = literal.end;
9553
9659
  }
@@ -12287,15 +12393,41 @@ function statementEnd(code, from) {
12287
12393
  return Math.min(code.length, from + 2e3);
12288
12394
  }
12289
12395
 
12396
+ // ../protocol/src/deploy-files.ts
12397
+ var MB = 1024 * 1024;
12398
+ var GB = 1024 * MB;
12399
+ var DEPLOY_FREE_LIMITS = {
12400
+ deployFileBytes: 500 * MB,
12401
+ deployTotalBytes: GB,
12402
+ deployBytesPerOwnerPerDay: 10 * GB
12403
+ };
12404
+ var DEPLOY_REQUEST_BYTES = 95 * MB;
12405
+ var DEPLOY_PART_BYTES = 50 * MB;
12406
+ var NOT_A_WEB_GAME = [
12407
+ { pattern: "(^|/)project\\.godot$", text: "This is a Godot project, not its web export. In Godot: Project > Export > Add > Web, with Thread Support off, and name the file index.html." },
12408
+ { pattern: "\\.(rpy|rpyc|rpa)$", text: "This is a Ren'Py game, not its web build. In the Ren'Py launcher: Web, then Build Web Application." },
12409
+ { pattern: "(^|/)(ProjectSettings|Assets)/|(^|/)UnityPlayer\\.dll$", text: "This is a Unity project, not a web build. Build for WebGL with Compression Format: Disabled." },
12410
+ { pattern: "\\.py$", text: "Python games need a web export, such as pygbag." },
12411
+ { pattern: "\\.(exe|apk|aab|ipa|dmg|msi|deb|rpm|appimage|x86_64)$|\\.app/", text: "This is a program to install. Games here run in the browser: export a web version with an index.html." }
12412
+ ];
12413
+ function notAWebGame(paths) {
12414
+ for (const { pattern: pattern2, text: text2 } of NOT_A_WEB_GAME) {
12415
+ const test = new RegExp(pattern2, "i");
12416
+ if (paths.some((path) => test.test(path))) return text2;
12417
+ }
12418
+ return null;
12419
+ }
12420
+
12290
12421
  // ../protocol/src/billing.ts
12291
12422
  var PRO_LIMITS = {
12292
12423
  gamesPerOwner: 250,
12293
12424
  gamesCreatedPerCreatorPerDay: 20,
12294
12425
  deployFiles: 1e4,
12295
- deployTotalBytes: 500 * 1024 * 1024,
12426
+ deployFileBytes: 2 * 1024 * 1024 * 1024,
12427
+ deployTotalBytes: 5 * 1024 * 1024 * 1024,
12296
12428
  deploysPerOwnerPerDay: 1e3,
12297
12429
  draftsPerOwnerPerDay: 5e3,
12298
- deployBytesPerOwnerPerDay: 10 * 1024 * 1024 * 1024,
12430
+ deployBytesPerOwnerPerDay: 100 * 1024 * 1024 * 1024,
12299
12431
  deployEntriesPerOwnerPerDay: 5e5,
12300
12432
  deploymentsKeptPerGame: 50,
12301
12433
  mediaUploadsPerDay: 1e3,
@@ -12315,8 +12447,9 @@ var MARKET_FREE_LIMITS = {
12315
12447
  itemFiles: 50,
12316
12448
  itemUploadBytesPerDay: 10 * 1024 * 1024 * 1024
12317
12449
  };
12450
+ var FREE_OUTSIDE_LIMITS = { ...MARKET_FREE_LIMITS, ...DEPLOY_FREE_LIMITS };
12318
12451
  var FREE_LIMITS = Object.fromEntries(
12319
- Object.keys(PRO_LIMITS).map((key) => [key, key in MARKET_FREE_LIMITS ? MARKET_FREE_LIMITS[key] : LIMITS[key]])
12452
+ Object.keys(PRO_LIMITS).map((key) => [key, FREE_OUTSIDE_LIMITS[key] ?? LIMITS[key]])
12320
12453
  );
12321
12454
 
12322
12455
  // ../protocol/src/config.ts
@@ -12413,29 +12546,8 @@ function checkProjectConfig(config) {
12413
12546
  return out;
12414
12547
  }
12415
12548
 
12416
- // ../protocol/src/deploy-files.ts
12417
- var LARGE_FILE_EXTENSIONS = ["wasm", "pck", "data", "zip"];
12418
- var DEPLOY_LARGE_FILE_BYTES = 95 * 1024 * 1024;
12419
- function deployFileLimit(path) {
12420
- return LARGE_FILE_EXTENSIONS.includes(extensionOf(path)) ? DEPLOY_LARGE_FILE_BYTES : LIMITS.deployFileBytes;
12421
- }
12422
- var NOT_A_WEB_GAME = [
12423
- { pattern: "(^|/)project\\.godot$", text: "This is a Godot project, not its web export. In Godot: Project > Export > Add > Web, with Thread Support off, and name the file index.html." },
12424
- { pattern: "\\.(rpy|rpyc|rpa)$", text: "This is a Ren'Py game, not its web build. In the Ren'Py launcher: Web, then Build Web Application." },
12425
- { pattern: "(^|/)(ProjectSettings|Assets)/|(^|/)UnityPlayer\\.dll$", text: "This is a Unity project, not a web build. Build for WebGL with Compression Format: Disabled." },
12426
- { pattern: "\\.py$", text: "Python games need a web export, such as pygbag." },
12427
- { pattern: "\\.(exe|apk|aab|ipa|dmg|msi|deb|rpm|appimage|x86_64)$|\\.app/", text: "This is a program to install. Games here run in the browser: export a web version with an index.html." }
12428
- ];
12429
- function notAWebGame(paths) {
12430
- for (const { pattern: pattern2, text: text2 } of NOT_A_WEB_GAME) {
12431
- const test = new RegExp(pattern2, "i");
12432
- if (paths.some((path) => test.test(path))) return text2;
12433
- }
12434
- return null;
12435
- }
12436
-
12437
12549
  // src/index.ts
12438
- var CLI_VERSION = "0.13.0";
12550
+ var CLI_VERSION = "0.14.0";
12439
12551
  var PRODUCTION_API = "https://onceworlds.com";
12440
12552
  var LOCAL_API = "http://localhost:8787";
12441
12553
  var USAGE = `Usage:
@@ -12678,8 +12790,9 @@ async function deploy(projectDir, api2, tokenArg, quiet = false, dev2 = false) {
12678
12790
  console.log(` \u2191 uploading ${uploads.length} changed file(s), ${formatBytes(uploads.reduce((s, f) => s + f.size, 0))}`);
12679
12791
  }
12680
12792
  await runPool(uploads, 6, async (file) => {
12681
- const body = await readFile9(file.abs);
12682
- await send("PUT", `${api2}/api/deploy/${start.deploymentId}/blobs/${file.hash}`, start.uploadToken, body);
12793
+ const url = `${api2}/api/deploy/${start.deploymentId}/blobs/${file.hash}`;
12794
+ if (file.size > DEPLOY_REQUEST_BYTES) return sendInParts(url, start.uploadToken, file);
12795
+ await send("PUT", url, start.uploadToken, await readFile9(file.abs));
12683
12796
  });
12684
12797
  const result = await send("POST", `${api2}/api/deploy/${start.deploymentId}/finalize`, start.uploadToken, {});
12685
12798
  if (!quiet) {
@@ -13077,6 +13190,11 @@ async function loadStore(projectDir, config) {
13077
13190
  if (guide.help) store.help = guide.help;
13078
13191
  return { store, images: [...images.values()] };
13079
13192
  }
13193
+ async function hashFile(abs) {
13194
+ const hash2 = createHash5("sha256");
13195
+ for await (const chunk2 of createReadStream(abs)) hash2.update(chunk2);
13196
+ return hash2.digest("hex");
13197
+ }
13080
13198
  async function collectFiles(root) {
13081
13199
  const files = [];
13082
13200
  const skipped = [];
@@ -13099,11 +13217,11 @@ async function collectFiles(root) {
13099
13217
  if (!QUIET_EXTENSIONS.has(extensionOf(path))) skipped.push(path);
13100
13218
  continue;
13101
13219
  }
13102
- const data = await readFile9(abs);
13103
- if (data.byteLength > deployFileLimit(path)) {
13104
- throw new CliError(`${path} is ${formatBytes(data.byteLength)}; files like it can be at most ${formatBytes(deployFileLimit(path))}.`);
13220
+ const { size } = await stat2(abs);
13221
+ if (size > PRO_LIMITS.deployFileBytes) {
13222
+ throw new CliError(`${path} is ${formatBytes(size)}; a file can be at most ${formatBytes(PRO_LIMITS.deployFileBytes)}.`);
13105
13223
  }
13106
- files.push({ path, abs, hash: createHash5("sha256").update(data).digest("hex"), size: data.byteLength });
13224
+ files.push({ path, abs, hash: await hashFile(abs), size });
13107
13225
  }
13108
13226
  }
13109
13227
  }
@@ -13350,7 +13468,33 @@ function mcpHost(api2) {
13350
13468
  token: async () => (await readCredential(api2))?.token ?? null
13351
13469
  };
13352
13470
  }
13353
- async function send(method, url, token, body) {
13471
+ async function sendInParts(url, token, file) {
13472
+ const { uploadId, partBytes, parts } = await send("POST", `${url}/parts`, token, void 0);
13473
+ const etags = new Array(parts);
13474
+ const handle = await open(file.abs);
13475
+ try {
13476
+ await runPool(Array.from({ length: parts }, (_, i) => i), 3, async (i) => {
13477
+ const data = Buffer.alloc(Math.min(partBytes, file.size - i * partBytes));
13478
+ await handle.read(data, 0, data.byteLength, i * partBytes);
13479
+ const stored = await retrying(() => send("PUT", `${url}/parts/${i + 1}`, token, data, { "X-Upload-Id": uploadId }));
13480
+ etags[i] = stored.etag;
13481
+ });
13482
+ } finally {
13483
+ await handle.close();
13484
+ }
13485
+ await retrying(() => send("POST", `${url}/complete`, token, { uploadId, etags }));
13486
+ }
13487
+ async function retrying(request) {
13488
+ for (let attempt = 1; ; attempt++) {
13489
+ try {
13490
+ return await request();
13491
+ } catch (err) {
13492
+ if (attempt === 3) throw err;
13493
+ await new Promise((done) => setTimeout(done, attempt * 2e3));
13494
+ }
13495
+ }
13496
+ }
13497
+ async function send(method, url, token, body, headers = {}) {
13354
13498
  const binary = body instanceof Uint8Array;
13355
13499
  let res;
13356
13500
  try {
@@ -13358,7 +13502,8 @@ async function send(method, url, token, body) {
13358
13502
  method,
13359
13503
  headers: {
13360
13504
  Authorization: `Bearer ${token}`,
13361
- ...body === void 0 ? {} : { "Content-Type": binary ? "application/octet-stream" : "application/json" }
13505
+ ...body === void 0 ? {} : { "Content-Type": binary ? "application/octet-stream" : "application/json" },
13506
+ ...headers
13362
13507
  },
13363
13508
  body: body === void 0 ? void 0 : binary ? body : JSON.stringify(body)
13364
13509
  });