ugly-game 0.5.20 → 0.5.22
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loop/gameLoop.js +11 -1
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +95 -0
- package/package.json +10 -9
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/loop/gameLoop.js
CHANGED
|
@@ -27,6 +27,7 @@ export function runGameLoop(opts) {
|
|
|
27
27
|
const maxCatchUp = sim.maxCatchUpFrames * fixedDt; // seconds
|
|
28
28
|
let handle = 0;
|
|
29
29
|
let last = -1; // ms of the previous rAF callback
|
|
30
|
+
let lastRender = -1; // ms of the previous ACTUAL render (not every rAF fires one)
|
|
30
31
|
let acc = 0; // sim accumulator (seconds)
|
|
31
32
|
let renderAcc = 0; // render accumulator (seconds)
|
|
32
33
|
let frame = 0;
|
|
@@ -34,6 +35,7 @@ export function runGameLoop(opts) {
|
|
|
34
35
|
page.onResumeFromHidden((hiddenMs) => {
|
|
35
36
|
// Reset frame timing so the first frame after resume carries no dt/step spike.
|
|
36
37
|
last = -1;
|
|
38
|
+
lastRender = -1;
|
|
37
39
|
acc = 0;
|
|
38
40
|
renderAcc = 0;
|
|
39
41
|
const gapTicks = Math.round(hiddenMs / (1000 * fixedDt));
|
|
@@ -76,7 +78,15 @@ export function runGameLoop(opts) {
|
|
|
76
78
|
renderAcc -= period;
|
|
77
79
|
if (renderAcc > period)
|
|
78
80
|
renderAcc = 0; // shed backlog; never render twice per frame
|
|
79
|
-
|
|
81
|
+
// dt is time since the previous RENDER, not the previous rAF. On a display faster than the render cap (a
|
|
82
|
+
// 120Hz panel with a 60fps cap) rAF fires more often than we render, so `wallDt` (inter-rAF) is a fraction of
|
|
83
|
+
// the real inter-render time. A game that steps a fixed-step sim inside render off `info.dt` would then gain
|
|
84
|
+
// only that fraction of wall time and run slow (30 UPS on 120Hz). Report the true inter-render delta so the
|
|
85
|
+
// in-render accumulator advances real time. (A worker/`tick`-driven sim is immune either way — this keeps the
|
|
86
|
+
// main-thread render-driven path correct too.)
|
|
87
|
+
const renderDt = lastRender < 0 ? wallDt : (t - lastRender) / 1000;
|
|
88
|
+
lastRender = t;
|
|
89
|
+
opts.render({ tier, frame, dt: renderDt, t });
|
|
80
90
|
frame++;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A registered provider: returns a JSON-able snapshot of some subsystem's state, evaluated lazily at report time. */
|
|
2
|
+
export type SnapshotProvider = () => unknown;
|
|
3
|
+
/** Register a lazy snapshot provider under `key`. Returns an unregister fn (useEffect-friendly). The provider is
|
|
4
|
+
* only invoked when a report is actually assembled, so an expensive full-state serialize costs nothing otherwise. */
|
|
5
|
+
export declare function registerSnapshotProvider(key: string, fn: SnapshotProvider): () => void;
|
|
6
|
+
/** Run every registered provider (best-effort — a throwing provider is recorded as `{_error}`, not fatal). */
|
|
7
|
+
export declare function collectSnapshots(): Record<string, unknown>;
|
|
8
|
+
/** A compact, storable reference to a packed snapshot — goes into the report's `context` JSON. Either the gzip is
|
|
9
|
+
* inlined (`gzB64`) or it was uploaded and only the `url` is kept. `bytes`/`gzBytes` let a viewer show the size. */
|
|
10
|
+
export interface PackedSnapshot {
|
|
11
|
+
key: string;
|
|
12
|
+
gzip: true;
|
|
13
|
+
bytes: number;
|
|
14
|
+
gzBytes: number;
|
|
15
|
+
gzB64?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
truncated?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface PackOpts {
|
|
20
|
+
/** gzip size (bytes) at/below which we INLINE as base64; above it we upload. Default 48000 (headroom under the
|
|
21
|
+
* 60KB keepalive-body cap once base64's ~1.37× expansion is applied). */
|
|
22
|
+
maxInlineBytes?: number;
|
|
23
|
+
/** Uploads the gzip blob to object storage and resolves to its URL. If absent, oversize payloads inline anyway. */
|
|
24
|
+
upload?: (gz: Uint8Array, meta: {
|
|
25
|
+
key: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
gzBytes: number;
|
|
28
|
+
}) => Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/** Serialize → gzip → inline if small (base64) else upload. Returns the compact reference to store in the report. */
|
|
31
|
+
export declare function packSnapshot(key: string, snap: unknown, opts?: PackOpts): Promise<PackedSnapshot>;
|
|
32
|
+
/** Inverse of packSnapshot for tooling: recover the original object from a PackedSnapshot. Inline payloads decode
|
|
33
|
+
* locally; uploaded ones are fetched (pass a `fetchUrl` for non-browser contexts). Enables local-sim repro. */
|
|
34
|
+
export declare function unpackSnapshot(ref: PackedSnapshot, fetchUrl?: (url: string) => Promise<Uint8Array>): Promise<unknown>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// ugly-game TELEMETRY ENRICH — attach a game's structured IR snapshot to a feedback / error report so a bug can be
|
|
3
|
+
// LOCALLY REPRODUCED, not just described. Generic + engine-side: the app supplies the snapshot (typically an
|
|
4
|
+
// AgentDebuggable.snapshot() carrying the full deterministic sim state) and, for big payloads, an uploader; this
|
|
5
|
+
// module handles JSON serialization, gzip compression (CompressionStream — browser + Node ≥18), the
|
|
6
|
+
// inline-vs-upload SIZE decision, and the compact reference you store in the report's `context`.
|
|
7
|
+
//
|
|
8
|
+
// Why gzip + upload: a full-state snapshot (edit overlays, entity lists, CA fields) routinely blows past the 60KB
|
|
9
|
+
// keepalive-body line and bloats the single D1 report row. Gzip alone usually gets it back under the line (inline);
|
|
10
|
+
// genuinely huge snapshots upload to object storage (R2) and the report keeps only the URL.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
const providers = new Map();
|
|
13
|
+
/** Register a lazy snapshot provider under `key`. Returns an unregister fn (useEffect-friendly). The provider is
|
|
14
|
+
* only invoked when a report is actually assembled, so an expensive full-state serialize costs nothing otherwise. */
|
|
15
|
+
export function registerSnapshotProvider(key, fn) {
|
|
16
|
+
providers.set(key, fn);
|
|
17
|
+
return () => {
|
|
18
|
+
if (providers.get(key) === fn)
|
|
19
|
+
providers.delete(key);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Run every registered provider (best-effort — a throwing provider is recorded as `{_error}`, not fatal). */
|
|
23
|
+
export function collectSnapshots() {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, fn] of providers) {
|
|
26
|
+
try {
|
|
27
|
+
out[k] = fn();
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
out[k] = { _error: String(e) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
async function gzipBytes(bytes) {
|
|
36
|
+
const cs = new CompressionStream('gzip');
|
|
37
|
+
const w = cs.writable.getWriter();
|
|
38
|
+
void w.write(bytes);
|
|
39
|
+
void w.close();
|
|
40
|
+
return new Uint8Array(await new Response(cs.readable).arrayBuffer());
|
|
41
|
+
}
|
|
42
|
+
async function gunzipBytes(bytes) {
|
|
43
|
+
const ds = new DecompressionStream('gzip');
|
|
44
|
+
const w = ds.writable.getWriter();
|
|
45
|
+
void w.write(bytes);
|
|
46
|
+
void w.close();
|
|
47
|
+
return new Uint8Array(await new Response(ds.readable).arrayBuffer());
|
|
48
|
+
}
|
|
49
|
+
function toB64(bytes) {
|
|
50
|
+
let s = '';
|
|
51
|
+
for (const b of bytes)
|
|
52
|
+
s += String.fromCharCode(b);
|
|
53
|
+
return btoa(s);
|
|
54
|
+
}
|
|
55
|
+
function fromB64(b64) {
|
|
56
|
+
const bin = atob(b64);
|
|
57
|
+
const out = new Uint8Array(bin.length);
|
|
58
|
+
for (let i = 0; i < bin.length; i++)
|
|
59
|
+
out[i] = bin.charCodeAt(i);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** Serialize → gzip → inline if small (base64) else upload. Returns the compact reference to store in the report. */
|
|
63
|
+
export async function packSnapshot(key, snap, opts = {}) {
|
|
64
|
+
const maxInline = opts.maxInlineBytes ?? 48_000;
|
|
65
|
+
const json = JSON.stringify(snap);
|
|
66
|
+
const raw = new TextEncoder().encode(json);
|
|
67
|
+
const gz = await gzipBytes(raw);
|
|
68
|
+
const truncated = !!snap && typeof snap === 'object' && '_truncated' in snap
|
|
69
|
+
? true
|
|
70
|
+
: undefined;
|
|
71
|
+
const ref = {
|
|
72
|
+
key,
|
|
73
|
+
gzip: true,
|
|
74
|
+
bytes: raw.length,
|
|
75
|
+
gzBytes: gz.length,
|
|
76
|
+
truncated,
|
|
77
|
+
};
|
|
78
|
+
if (gz.length <= maxInline || !opts.upload)
|
|
79
|
+
return { ...ref, gzB64: toB64(gz) };
|
|
80
|
+
return {
|
|
81
|
+
...ref,
|
|
82
|
+
url: await opts.upload(gz, { key, bytes: raw.length, gzBytes: gz.length }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Inverse of packSnapshot for tooling: recover the original object from a PackedSnapshot. Inline payloads decode
|
|
86
|
+
* locally; uploaded ones are fetched (pass a `fetchUrl` for non-browser contexts). Enables local-sim repro. */
|
|
87
|
+
export async function unpackSnapshot(ref, fetchUrl = async (url) => new Uint8Array(await (await fetch(url)).arrayBuffer())) {
|
|
88
|
+
const gz = ref.gzB64
|
|
89
|
+
? fromB64(ref.gzB64)
|
|
90
|
+
: ref.url
|
|
91
|
+
? await fetchUrl(ref.url)
|
|
92
|
+
: new Uint8Array();
|
|
93
|
+
const raw = await gunzipBytes(gz);
|
|
94
|
+
return JSON.parse(new TextDecoder().decode(raw));
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-game",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"preinstall": "npx only-allow pnpm",
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"lint": "ugly-app lint",
|
|
22
|
+
"lint:fix": "ugly-app lint --fix",
|
|
23
|
+
"pretty": "prettier --write .",
|
|
24
|
+
"prepublishOnly": "pnpm run pretty && ugly-app lint && tsc -p tsconfig.json"
|
|
25
|
+
},
|
|
18
26
|
"dependencies": {
|
|
19
27
|
"meshoptimizer": "^1.2.0"
|
|
20
28
|
},
|
|
@@ -27,12 +35,5 @@
|
|
|
27
35
|
"@webgpu/types": "^0.1.40",
|
|
28
36
|
"prettier": "^3.9.5",
|
|
29
37
|
"ugly-app": "^0.1.841"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"preinstall": "npx only-allow pnpm",
|
|
33
|
-
"build": "tsc -p tsconfig.json",
|
|
34
|
-
"lint": "ugly-app lint",
|
|
35
|
-
"lint:fix": "ugly-app lint --fix",
|
|
36
|
-
"pretty": "prettier --write ."
|
|
37
38
|
}
|
|
38
|
-
}
|
|
39
|
+
}
|