dotframe 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ The rest of round 7, which missed the 0.1.8 merge.
6
+
7
+ - `dotframe/src/probe`: `createProbe()` publishes `globalThis.__dotframe` for `play --online`, and `createMasher(seed, random)` gives scripted inputs, so games do not write either by hand. Templates use them.
8
+ - `play --online --seeds a,b` picks each peer's mash seed.
9
+ - `doctor` fails on the template's placeholder iOS bundle id (`com.example.*`), which Apple will not register.
10
+
11
+ ## 0.1.8
12
+
13
+ From dogfood round 7 (Craft Ones on 0.1.7).
14
+
15
+ - `doctor` sim:math skips files marked `// dotframe-allow-math-file`, for render-only files the sim reaches because it builds the renderer.
16
+ - `deploy init` pins images: bun from `packageManager` (game or repo root, else the local bun) and the relay's dotframe from the version installed in the game.
17
+ - The vendor fix (doctor and native builds) names `DOTFRAME_VENDOR` for reusing an existing vendor dir.
18
+
3
19
  ## 0.1.7
4
20
 
5
21
  From dogfood rounds 5 and 6 (Craft Ones on iOS, online, and on the VPS).
@@ -14,14 +14,38 @@ async function gitRoot(ctx: Ctx, root: string): Promise<string> {
14
14
  return r.output.trim();
15
15
  }
16
16
 
17
+ interface Versions {
18
+ bun: string;
19
+ dotframe: string;
20
+ }
21
+
22
+ // Pinned versions, so an image built tomorrow is the image built today: bun from packageManager (the game's
23
+ // package.json, then the repo root's), else the bun running this CLI; dotframe as installed in the game.
24
+ function versions(gameRoot: string, repo: string): Versions {
25
+ let bun = Bun.version;
26
+ for (const dir of [gameRoot, repo]) {
27
+ const file = join(dir, "package.json");
28
+ if (!existsSync(file)) continue;
29
+ const manager = (JSON.parse(readFileSync(file, "utf8")) as { packageManager?: string }).packageManager ?? "";
30
+ const match = manager.match(/^bun@(\d+\.\d+\.\d+)/);
31
+ if (match) {
32
+ bun = match[1];
33
+ break;
34
+ }
35
+ }
36
+ const installed = join(gameRoot, "node_modules", "dotframe", "package.json");
37
+ const dotframe = existsSync(installed) ? (JSON.parse(readFileSync(installed, "utf8")) as { version: string }).version : pkg.version;
38
+ return { bun, dotframe };
39
+ }
40
+
17
41
  // game: the game's path inside the repo ("" at the root); context: the repo root, relative to deploy/.
18
- function files(config: Config, game: string, out: string, context: string): Record<string, string> {
42
+ function files(config: Config, game: string, out: string, context: string, v: Versions): Record<string, string> {
19
43
  const requires = (config.requires ?? []).filter((t: string): boolean => t !== "bun");
20
44
  const apt = requires.length > 0 ? `RUN apt-get update && apt-get install -y --no-install-recommends ${requires.join(" ")} && rm -rf /var/lib/apt/lists/*\n` : "";
21
45
  const at = game === "" ? "." : game;
22
46
  return {
23
47
  "Dockerfile.web": `# Static web build of ${config.name}, served by nginx. Generated by dotframe deploy init; edit freely.
24
- FROM oven/bun:1 AS build
48
+ FROM oven/bun:${v.bun} AS build
25
49
  ${apt}WORKDIR /app
26
50
  COPY . .
27
51
  RUN cd ${at} && bun install --frozen-lockfile && bun node_modules/dotframe/cli/main.ts build web
@@ -32,9 +56,10 @@ COPY --from=build ${join("/app", at, out)} /usr/share/nginx/html
32
56
  EXPOSE 80
33
57
  `,
34
58
  "Dockerfile.relay": `# Netplay relay for ${config.name}: dotframe relay serve. Generated by dotframe deploy init.
35
- FROM oven/bun:1-alpine
59
+ FROM oven/bun:${v.bun}-alpine
36
60
  WORKDIR /app
37
- RUN bun add dotframe@${pkg.version}
61
+ # The dotframe version the game has installed; bump it together with the game's.
62
+ RUN bun add dotframe@${v.dotframe}
38
63
  ENV PORT=8787
39
64
  EXPOSE 8787
40
65
  CMD ["bun", "node_modules/dotframe/cli/main.ts", "relay", "serve"]
@@ -86,7 +111,7 @@ export async function deployInit(ctx: Ctx, name: string | undefined, provider: s
86
111
  const repo = await gitRoot(ctx, config.root);
87
112
  const game = relative(repo, config.root);
88
113
  // compose.yaml sits in <game>/deploy; the web build context is the repo root, so workspace code is visible.
89
- const generated = files(config, game, t.out, relative(join(config.root, DIR), repo) || ".");
114
+ const generated = files(config, game, t.out, relative(join(config.root, DIR), repo) || ".", versions(config.root, repo));
90
115
  const dir = join(config.root, DIR);
91
116
  const written = Object.keys(generated).map((f: string): string => join(DIR, f));
92
117
  const existing = written.filter((f: string): boolean => existsSync(join(config.root, f)));
@@ -47,6 +47,9 @@ export async function doctor(ctx: Ctx, fix: boolean, docker = false): Promise<vo
47
47
  }
48
48
  }
49
49
  for (const [name, t] of Object.entries(config.targets)) {
50
+ if (t.native?.platform === "ios" && t.native.bundleId.startsWith("com.example.")) {
51
+ checks.push({ check: `ios:${name}:bundleId`, ok: false, detail: `bundle id ${t.native.bundleId} is the template placeholder; Apple will not register it`, fix: `dotframe config set targets.${name}.native.bundleId '"<reverse domain you own>.<game>"'`, skill: "ios" });
52
+ }
50
53
  if (t.native?.platform === "ios" && t.native.team === "YOUR_TEAM_ID") {
51
54
  checks.push({ check: `ios:${name}`, ok: false, detail: "the iOS team is still the template placeholder", fix: `dotframe config set targets.${name}.native.team '"<Apple team id>"'`, skill: "ios" });
52
55
  }
@@ -147,8 +150,9 @@ async function renderCheck(sim: Sim, root: string): Promise<Check> {
147
150
  }
148
151
 
149
152
  // Math.sin and friends differ in the last bits between engines and OSes, so simulation code that calls them drifts
150
- // between netplay peers and breaks replays on another machine. Render code may use them; mark a line with
151
- // `dotframe-allow-math` to silence it.
153
+ // between netplay peers and breaks replays on another machine. Render code may use them: mark a line with
154
+ // `dotframe-allow-math`, or a whole render-only file with `// dotframe-allow-math-file`. (The sim module builds the
155
+ // renderer for desync's purity check, so render files are reachable and a static scan cannot tell them apart.)
152
156
  const NONDETERMINISTIC = /\bMath\.(sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|exp|expm1|log|log1p|log2|log10|pow|cbrt|hypot)\b|[\w)\]]\s*\*\*\s*[\w(]/;
153
157
 
154
158
  // The engine's own files (draw2d's arcs, raster2d) are render code, however the game imports them: a vendored
@@ -173,7 +177,9 @@ function mathCheck(simFile: string): Check {
173
177
  const engine = new Map<string, boolean>();
174
178
  for (const file of importGraph(simFile).files) {
175
179
  if (!/\.tsx?$/.test(file) || file.includes(`${sep}node_modules${sep}`) || engineFile(file, engine)) continue;
176
- readFileSync(file, "utf8")
180
+ const text = readFileSync(file, "utf8");
181
+ if (text.includes("dotframe-allow-math-file")) continue;
182
+ text
177
183
  .split("\n")
178
184
  .forEach((line: string, i: number): void => {
179
185
  const code = line.replace(/\/\/.*$/, "");
@@ -187,6 +193,6 @@ function mathCheck(simFile: string): Check {
187
193
  ok: false,
188
194
  warn: true,
189
195
  detail: `${hits.length} platform-dependent math call(s) in code the sim reaches: ${shown}`,
190
- fix: "use dotframe/src/detmath (dsin, dcos, datan2, dexp, dpow, ...) in simulation code; mark render-only lines with // dotframe-allow-math",
196
+ fix: "use dotframe/src/detmath (dsin, dcos, datan2, dexp, dpow, ...) in simulation code; mark render-only lines with // dotframe-allow-math, or a render-only file with // dotframe-allow-math-file",
191
197
  };
192
198
  }
@@ -13,13 +13,17 @@ interface Probe {
13
13
  // Two browsers play one online match through a local relay, each with a scripted masher, and their checksums at
14
14
  // every 30th confirmed frame must agree. The game's web entry honors ?room=, ?relay= and ?mash=, and publishes
15
15
  // globalThis.__dotframe = { frame, confirmed, status, sums } (the templates do; see the netplay skill).
16
- export async function playOnline(ctx: Ctx, room: string | undefined, args: { frames?: string; out?: string }): Promise<void> {
16
+ export async function playOnline(ctx: Ctx, room: string | undefined, args: { frames?: string; out?: string; seeds?: string }): Promise<void> {
17
17
  const skill = "netplay";
18
18
  const config = loadConfig();
19
19
  const t = target(config, "web");
20
20
  if (!t.out) throw new CliError("NO_OUT", "the web target needs an out dir", "", "export-web");
21
21
  if (!which("agent-browser")) throw new CliError("TOOL_MISSING", "agent-browser not found (play drives two browsers)", "npm i -g agent-browser && agent-browser install", skill);
22
22
  const frames = num("frames", args.frames, 600, 60);
23
+ // One mash seed per peer; different seeds make the peers press different inputs, which is what exercises rollback.
24
+ const seedParts = (args.seeds ?? "1,2").split(",");
25
+ if (seedParts.length !== 2) throw new CliError("BAD_ARG", `--seeds needs two comma-separated seeds, got "${args.seeds}"`, "--seeds 7,42", skill);
26
+ const seeds = seedParts.map((part: string): number => num("seeds", part.trim(), 1));
23
27
  const name = room ?? `play-${Date.now().toString(36)}`;
24
28
  const outDir = resolve(process.cwd(), args.out ?? ".dotframe/play");
25
29
  await runSteps({ ...ctx, json: true }, config.root, t.steps ?? [], "export-web");
@@ -46,7 +50,7 @@ export async function playOnline(ctx: Ctx, room: string | undefined, args: { fra
46
50
  };
47
51
  try {
48
52
  for (const [i, session] of sessions.entries()) {
49
- const url = `http://localhost:${server.port}/?room=${encodeURIComponent(name)}&relay=${encodeURIComponent(`ws://localhost:${relay.port}`)}&mash=${i + 1}`;
53
+ const url = `http://localhost:${server.port}/?room=${encodeURIComponent(name)}&relay=${encodeURIComponent(`ws://localhost:${relay.port}`)}&mash=${seeds[i]}`;
50
54
  const opened = await ab(session, "open", url);
51
55
  if (opened.code !== 0) throw new CliError("BROWSER_FAILED", opened.tail, "agent-browser install", skill);
52
56
  }
@@ -78,7 +82,7 @@ export async function playOnline(ctx: Ctx, room: string | undefined, args: { fra
78
82
  if (shot.code !== 0 || !existsSync(shots[i])) throw new CliError("BROWSER_FAILED", `screenshot of peer ${i} failed: ${shot.tail}`, "", skill);
79
83
  }
80
84
  const ok = mismatch === undefined && compared.length > 0;
81
- print(ctx, { ok, room: name, frames: Math.min(a.confirmed, b.confirmed), compared: compared.length, firstMismatch: mismatch ?? null, seconds: Math.round((Date.now() - started) / 1000), screenshots: shots }, (): string =>
85
+ print(ctx, { ok, room: name, seeds, frames: Math.min(a.confirmed, b.confirmed), compared: compared.length, firstMismatch: mismatch ?? null, seconds: Math.round((Date.now() - started) / 1000), screenshots: shots }, (): string =>
82
86
  [
83
87
  ok ? `in sync: ${compared.length} checksums compared over ${Math.min(a.confirmed, b.confirmed)} confirmed frames` : mismatch !== undefined ? `DESYNC at frame ${mismatch}` : "no checksums to compare",
84
88
  `screenshots: ${shots.join(", ")} (open them and look)`,
package/cli/main.ts CHANGED
@@ -21,7 +21,7 @@ Play (headless, deterministic)
21
21
  sim [--inputs f.jsonl | --mash <seed>] [--frames 600] [--seed 1] [--options json] [--every n]
22
22
  snap --frame <n> [--out frame.png] [--inputs f | --mash <seed>] [--seed 1] [--options json]
23
23
  replay record <file> | verify <file...>
24
- play --online [room] [--frames 600] two browsers, local relay, scripted match
24
+ play --online [room] [--frames 600] [--seeds 1,2] two browsers, local relay, scripted match
25
25
  desync [--latency 100ms] [--jitter 0ms] [--delay 2] [--frames 1800] [--renders 3]
26
26
 
27
27
  Build and ship
@@ -74,11 +74,13 @@ Compares checksums every frame and the full inspect() state every --every frames
74
74
  Warns when rollbacks exceed the sim's rollbackWindow.
75
75
 
76
76
  dotframe desync --latency 474ms --jitter 40ms --mash 42 --json`,
77
- play: `dotframe play --online [room] [--frames 600] [--out .dotframe/play] [--json]
77
+ play: `dotframe play --online [room] [--frames 600] [--seeds 1,2] [--out .dotframe/play] [--json]
78
78
 
79
- Builds the web target, starts a local relay, and opens two agent-browser sessions on ?room=<room>&relay=...&mash=1|2.
79
+ Builds the web target, starts a local relay, and opens two agent-browser sessions on ?room=<room>&relay=...&mash=<seed>
80
+ (one seed per peer from --seeds).
80
81
  Waits until both confirm --frames frames, compares their checksums every 30 frames, and saves a screenshot of each.
81
- The web entry must honor ?room=, ?relay=, ?mash= and publish globalThis.__dotframe (templates do).`,
82
+ The web entry must honor ?room=, ?relay=, ?mash= and publish globalThis.__dotframe: createProbe() and createMasher()
83
+ from dotframe/src/probe do both (templates show it).`,
82
84
  build: `dotframe build <target> [--release] [--dry-run] [--json]
83
85
 
84
86
  Runs targets.<target>.steps from dotframe.json, or for a native target ({"native": {"platform": "macos",
@@ -168,6 +170,7 @@ const { values, positionals } = parseArgs({
168
170
  "through-over": { type: "boolean" },
169
171
  docker: { type: "boolean" },
170
172
  online: { type: "boolean" },
173
+ seeds: { type: "string" },
171
174
  provider: { type: "string" },
172
175
  compose: { type: "string" },
173
176
  },
@@ -181,7 +184,7 @@ try {
181
184
  if (values.version) console.log(pkg.version);
182
185
  else if (command && values.help && COMMAND_HELP[command]) console.log(COMMAND_HELP[command]);
183
186
  else if (!command || values.help) console.log(HELP);
184
- else if (command === "play" && values.online === true) await playOnline(ctx, rest[0], { frames: v.frames, out: v.out });
187
+ else if (command === "play" && values.online === true) await playOnline(ctx, rest[0], { frames: v.frames, out: v.out, seeds: v.seeds });
185
188
  else if (command === "sim") await sim(ctx, v);
186
189
  else if (command === "snap") await snap(ctx, v);
187
190
  else if (command === "replay" && rest[0] === "record") await record(ctx, rest[1], v);
package/cli/native.ts CHANGED
@@ -46,7 +46,7 @@ export function sdlLibrary(platform: NativePlatform): string {
46
46
  }
47
47
 
48
48
  export function vendorFix(platform: NativePlatform): string {
49
- return `dotframe vendor ${platform}`;
49
+ return `dotframe vendor ${platform} (builds into ~/.dotframe/vendor), or point DOTFRAME_VENDOR at an existing vendor dir such as a dotframe checkout's vendor/`;
50
50
  }
51
51
 
52
52
  const SPECIFIER = /(?:from\s+|import\s*\(\s*|import\s+)(["'])([^"']+)\1/g;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotframe",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "TS-first game engine and agent-first CLI: one codebase for web, Discord, iOS and native binaries",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,6 +29,7 @@ dotframe config set targets.web.deploy.compose '"<compose id>"' # vps compose
29
29
  dotframe deploy web --dry-run && dotframe deploy web --yes
30
30
  ```
31
31
 
32
+ - Images are pinned: bun from `packageManager` (game or repo root, else the local bun) and the relay's dotframe from the version installed in the game. Rerun `deploy init --yes` after bumping either.
32
33
  - The web image installs the tools in `requires` (dotframe.json, for example `["ffmpeg"]`) and builds with dotframe; the relay image runs `dotframe relay serve`. The build context is the repo root, so monorepo ports see workspace code.
33
34
  - In Dokploy, route the domain's `/` to service `web` (port 80) and `/relay` to service `relay` (port 8787). For Discord, map `/` and `/relay` to that host.
34
35
  - Dokploy builds the pushed branch, not your working tree: deploy warns about unpushed or uncommitted work. On failure it returns the build log (through `vps compose logs`).
@@ -6,6 +6,7 @@ description: Build and install the iOS target of a dotframe game. Use when build
6
6
 
7
7
  ```sh
8
8
  dotframe vendor ios # once per machine: SDL3 (Xcode generator) and wgpu-native for iOS
9
+ # or reuse an existing vendor (a dotframe checkout's vendor/): export DOTFRAME_VENDOR=<path>
9
10
  dotframe doctor # vendor:ios, ios-runtime (scriptc pack matching the compiler), team placeholder
10
11
  dotframe build ios --json # dist/ios/<Scheme>.app
11
12
  dotframe device install ios --dry-run
@@ -15,7 +15,7 @@ dotframe relay serve # local relay on :8787, same protocol as p
15
15
  dotframe play --online --frames 900 # two browsers, scripted match, checksums compared, screenshots saved
16
16
  ```
17
17
 
18
- `play --online` needs the web entry to honor `?room=`, `?relay=` and `?mash=<seed>` and to publish `globalThis.__dotframe = { frame, confirmed, status, sums }` (sums: checksum per 30th confirmed frame). The templates show it.
18
+ `play --online` needs the web entry to honor `?room=`, `?relay=` and `?mash=<seed>` and to publish `globalThis.__dotframe = { frame, confirmed, status, sums }` (sums: checksum per 30th confirmed frame). `dotframe/src/probe` does both: `createProbe()` publishes it (call `probe.update(rollback, link.status())` each step, or `probe.update(null, "local", frame)` offline), and `createMasher(seed, randomInput)` returns a function giving the scripted input for each step. `--seeds 7,42` picks each peer's seed. The templates show the wiring.
19
19
 
20
20
  dotframe online play is rollback netcode: each peer predicts the remote input (repeat the last one), simulates ahead, and when the real input arrives and differs, restores a snapshot and resimulates. It only works if every peer computes bit-identical state from the same inputs.
21
21
 
@@ -43,7 +43,7 @@ Run several seeds and a high latency (474 ms is what a transatlantic relay measu
43
43
 
44
44
  ## Rules
45
45
 
46
- - Simulation code uses `dotframe/src/detmath` (dsin, dcos, datan2, dexp, dlog, dpow, ...), never `Math.sin` and friends or `**`: those differ in the last bits between engines and operating systems, so peers and replays drift (Craft Ones' replays diverged between macOS and Linux at frame 120). `dotframe doctor` warns about them (`sim:math`); mark render-only lines with `// dotframe-allow-math`. A replay passing on one machine proves nothing about another: run CI on a second OS.
46
+ - Simulation code uses `dotframe/src/detmath` (dsin, dcos, datan2, dexp, dlog, dpow, ...), never `Math.sin` and friends or `**`: those differ in the last bits between engines and operating systems, so peers and replays drift (Craft Ones' replays diverged between macOS and Linux at frame 120). `dotframe doctor` warns about them (`sim:math`); mark render-only lines with `// dotframe-allow-math`, or a whole render-only file with `// dotframe-allow-math-file` (the sim builds the renderer for desync, so render files count as reachable). A replay passing on one machine proves nothing about another: run CI on a second OS.
47
47
  - `desync` reports `restoreMs` early vs late; it warns when late restores cost more than twice the early ones (a heuristic for restore that replays from an earlier frame, which passes desync but stalls real rollbacks late in a match).
48
48
  - Build checksums from numbers in a fixed order, never from `JSON.stringify`: native builds can order keys differently from JavaScript, so a web peer and a native peer would report false desyncs.
49
49
 
package/src/probe.ts ADDED
@@ -0,0 +1,53 @@
1
+ // The state `dotframe play --online` reads from each browser: frame, confirmed frame, link status, and this peer's
2
+ // checksum at every 30th confirmed frame. Call update() once per fixed step; publish() exposes it as
3
+ // globalThis.__dotframe.
4
+ import type { Rollback } from "./netplay";
5
+
6
+ export interface ProbeState {
7
+ frame: number;
8
+ confirmed: number;
9
+ status: string;
10
+ sums: Record<number, number>;
11
+ }
12
+
13
+ export interface Probe {
14
+ state: ProbeState;
15
+ // Online: pass the rollback and the link status. Local: pass null and the frame just simulated.
16
+ update: (rollback: Rollback | null, status: string, localFrame?: number) => void;
17
+ }
18
+
19
+ const EVERY = 30;
20
+
21
+ export function createProbe(): Probe {
22
+ const state: ProbeState = { frame: 0, confirmed: 0, status: "local", sums: {} };
23
+ (globalThis as { __dotframe?: ProbeState }).__dotframe = state;
24
+ return {
25
+ state,
26
+ update: (rollback: Rollback | null, status: string, localFrame?: number): void => {
27
+ state.status = status;
28
+ if (!rollback) {
29
+ if (localFrame !== undefined) state.frame = localFrame;
30
+ return;
31
+ }
32
+ state.frame = rollback.stats().frame;
33
+ state.confirmed = rollback.confirmedFrame();
34
+ for (let f = EVERY; f < state.confirmed; f += EVERY) if (state.sums[f] === undefined) state.sums[f] = rollback.sumAt(f) ?? 0;
35
+ },
36
+ };
37
+ }
38
+
39
+ // A scripted masher for tests: a new random input every 6 frames from a seed, like the CLI's --mash.
40
+ export function createMasher(seed: number, random: (next: () => number) => number): () => number {
41
+ let s = seed >>> 0;
42
+ const next = (): number => {
43
+ s = (Math.imul(s, 1103515245) + 12345) >>> 0;
44
+ return s / 4294967296;
45
+ };
46
+ let frame = 0;
47
+ let input = 0;
48
+ return (): number => {
49
+ if (frame % 6 === 0) input = random(next);
50
+ frame += 1;
51
+ return input;
52
+ };
53
+ }
@@ -6,6 +6,7 @@ import type { Frame, RenderGpu } from "dotframe/src/gpu";
6
6
  import type { Input } from "dotframe/src/input";
7
7
  import { Key, MouseButton } from "dotframe/src/input";
8
8
  import { createRollback, type RelayLink, type Rollback } from "dotframe/src/netplay";
9
+ import { createMasher, createProbe } from "dotframe/src/probe";
9
10
  import { checksum, createGame, type Game, keyInput, PLAYERS, POINTER_BIT, randomInput, render, restore, snapshot, step, WINDOW } from "./game";
10
11
 
11
12
  export interface SetupOptions {
@@ -15,13 +16,6 @@ export interface SetupOptions {
15
16
  mash: number | null;
16
17
  }
17
18
 
18
- // What `dotframe play --online` reads from each browser.
19
- interface Probe {
20
- frame: number;
21
- confirmed: number;
22
- status: string;
23
- sums: Record<number, number>;
24
- }
25
19
 
26
20
  const STEP = 1 / 60;
27
21
  // A full charge takes one second of holding.
@@ -47,15 +41,10 @@ export function createSetup(options: SetupOptions): (platform: SetupPlatform) =>
47
41
  // Online peers must start from the same state, so the seed is fixed there.
48
42
  const game: Game = createGame(options.link ? 1 : Math.floor(Math.random() * 1e9));
49
43
  let rollback: Rollback | null = null;
50
- let mashState = options.mash ?? 0;
51
- const nextRandom = (): number => {
52
- mashState = (Math.imul(mashState, 1103515245) + 12345) >>> 0;
53
- return mashState / 4294967296;
54
- };
55
- let mashed = 0;
56
- let mashInput = 0;
57
- const probe: Probe = { frame: 0, confirmed: 0, status: options.link ? "connecting" : "local", sums: {} };
58
- (globalThis as { __dotframe?: Probe }).__dotframe = probe;
44
+ const mash = options.mash === null ? null : createMasher(options.mash, randomInput);
45
+ // What dotframe play --online reads from each browser.
46
+ const probe = createProbe();
47
+ let localFrame = 0;
59
48
  let simulated = -1;
60
49
  // Hold to charge, release to act: the pattern most mouse and touch games need.
61
50
  let charge = 0;
@@ -77,15 +66,9 @@ export function createSetup(options: SetupOptions): (platform: SetupPlatform) =>
77
66
  wasDown = down;
78
67
  const keys = [keyInput(input, [Key.A, Key.D, Key.W, Key.J]), keyInput(input, [Key.Left, Key.Right, Key.Up, Key.L])];
79
68
  if (released) keys[0] = keys[0] | POINTER_BIT;
80
- if (options.mash !== null) {
81
- // A new random input every 6 frames, like a mashing player.
82
- if (mashed % 6 === 0) mashInput = randomInput(nextRandom);
83
- keys[0] = mashInput;
84
- mashed += 1;
85
- }
69
+ if (mash) keys[0] = mash();
86
70
  const link = options.link;
87
71
  if (link) {
88
- probe.status = link.status();
89
72
  if (!rollback && link.status() === "paired") {
90
73
  rollback = createRollback({
91
74
  sim: {
@@ -102,15 +85,12 @@ export function createSetup(options: SetupOptions): (platform: SetupPlatform) =>
102
85
  });
103
86
  }
104
87
  // Online, each browser controls its own player with player 1's keys.
105
- if (rollback) {
106
- rollback.tick(keys[0]);
107
- probe.frame = rollback.stats().frame;
108
- probe.confirmed = rollback.confirmedFrame();
109
- for (let f = 30; f < probe.confirmed; f += 30) if (probe.sums[f] === undefined) probe.sums[f] = rollback.sumAt(f) ?? 0;
110
- }
88
+ if (rollback) rollback.tick(keys[0]);
89
+ probe.update(rollback, link.status());
111
90
  } else {
112
91
  step(game, keys.slice(0, PLAYERS));
113
- probe.frame += 1;
92
+ localFrame += 1;
93
+ probe.update(null, "local", localFrame);
114
94
  }
115
95
  simulated += STEP;
116
96
  }