@jgengine/node 0.8.0 → 0.9.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.
- package/CHANGELOG.md +16 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/persistence.js +45 -13
- package/dist/webHandler.d.ts +1 -1
- package/dist/webHandler.js +16 -3
- package/llms.txt +140 -641
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,22 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
11
11
|
same data as typed values, so an updater can diff its installed version against
|
|
12
12
|
the latest and surface the migration steps.
|
|
13
13
|
|
|
14
|
+
## Unreleased
|
|
15
|
+
|
|
16
|
+
## 0.9.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
|
|
21
|
+
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
|
|
22
|
+
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
|
|
23
|
+
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
|
|
24
|
+
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).
|
|
29
|
+
|
|
14
30
|
## 0.8.0
|
|
15
31
|
|
|
16
32
|
Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/persistence.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { applyLeaderboardRows, leaderboardRowKey, profileLeaderboardStats, topLeaderboardRows, trimFeedEntries, } from "@jgengine/core/runtime/hostPersistence";
|
|
4
5
|
export { memoryPersistence } from "@jgengine/ws/host";
|
|
6
|
+
function isMissing(error) {
|
|
7
|
+
return error?.code === "ENOENT";
|
|
8
|
+
}
|
|
5
9
|
async function readJson(path) {
|
|
10
|
+
let text;
|
|
6
11
|
try {
|
|
7
|
-
|
|
12
|
+
text = await readFile(path, "utf8");
|
|
8
13
|
}
|
|
9
|
-
catch {
|
|
10
|
-
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissing(error))
|
|
16
|
+
return null;
|
|
17
|
+
throw error;
|
|
11
18
|
}
|
|
19
|
+
return JSON.parse(text);
|
|
12
20
|
}
|
|
13
|
-
async function
|
|
14
|
-
const temp = `${path}.tmp`;
|
|
21
|
+
async function stageJson(path, value) {
|
|
22
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
15
23
|
await writeFile(temp, JSON.stringify(value), "utf8");
|
|
16
|
-
|
|
24
|
+
return temp;
|
|
25
|
+
}
|
|
26
|
+
async function writeJson(path, value) {
|
|
27
|
+
await rename(await stageJson(path, value), path);
|
|
17
28
|
}
|
|
18
29
|
const enc = encodeURIComponent;
|
|
19
30
|
export function filePersistence(dir, now = Date.now) {
|
|
@@ -32,6 +43,7 @@ export function filePersistence(dir, now = Date.now) {
|
|
|
32
43
|
return new Map(rows.map((row) => [leaderboardRowKey(row), row]));
|
|
33
44
|
};
|
|
34
45
|
let leaderboardLock = Promise.resolve();
|
|
46
|
+
const feedLocks = new Map();
|
|
35
47
|
return {
|
|
36
48
|
async loadServer(serverId) {
|
|
37
49
|
await ensureDirs();
|
|
@@ -86,9 +98,24 @@ export function filePersistence(dir, now = Date.now) {
|
|
|
86
98
|
await ensureDirs();
|
|
87
99
|
const serverDir = join(chunksDir, enc(serverId));
|
|
88
100
|
await mkdir(serverDir, { recursive: true });
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
const staged = await Promise.all(records.map(async (record) => {
|
|
102
|
+
const final = join(serverDir, `${enc(record.chunkKey)}.json`);
|
|
103
|
+
return { temp: await stageJson(final, record), final };
|
|
104
|
+
}));
|
|
105
|
+
await Promise.all(staged.map(({ temp, final }) => rename(temp, final)));
|
|
106
|
+
},
|
|
107
|
+
async deleteChunks(serverId, chunkKeys) {
|
|
108
|
+
await ensureDirs();
|
|
109
|
+
const serverDir = join(chunksDir, enc(serverId));
|
|
110
|
+
await Promise.all(chunkKeys.map(async (chunkKey) => {
|
|
111
|
+
try {
|
|
112
|
+
await unlink(join(serverDir, `${enc(chunkKey)}.json`));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!isMissing(error))
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
92
119
|
},
|
|
93
120
|
async loadFeed({ serverId, action }) {
|
|
94
121
|
await ensureDirs();
|
|
@@ -97,9 +124,14 @@ export function filePersistence(dir, now = Date.now) {
|
|
|
97
124
|
async appendFeed({ serverId, action, entry }) {
|
|
98
125
|
await ensureDirs();
|
|
99
126
|
const path = join(feedsDir, `${enc(serverId)}__${enc(action)}.json`);
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
const prev = feedLocks.get(path) ?? Promise.resolve();
|
|
128
|
+
const run = prev.then(async () => {
|
|
129
|
+
const entries = trimFeedEntries([...((await readJson(path)) ?? []), entry]);
|
|
130
|
+
await writeJson(path, entries);
|
|
131
|
+
return entries;
|
|
132
|
+
});
|
|
133
|
+
feedLocks.set(path, run.catch(() => undefined));
|
|
134
|
+
return run;
|
|
103
135
|
},
|
|
104
136
|
async applyLeaderboardIncrements(gameId, entries) {
|
|
105
137
|
const run = leaderboardLock.then(async () => {
|
package/dist/webHandler.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
export type WebHandler = (request: Request) => Promise<Response>;
|
|
3
3
|
export type NodeHandler = (req: IncomingMessage, res: ServerResponse) => void;
|
|
4
|
-
export declare function toWebRequest(req: IncomingMessage): Request
|
|
4
|
+
export declare function toWebRequest(req: IncomingMessage): Promise<Request>;
|
|
5
5
|
export declare function toNodeHandler(handler: WebHandler): NodeHandler;
|
package/dist/webHandler.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function toWebRequest(req) {
|
|
1
|
+
export async function toWebRequest(req) {
|
|
2
2
|
const host = req.headers.host ?? "localhost";
|
|
3
3
|
const headers = new Headers();
|
|
4
4
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -8,11 +8,24 @@ export function toWebRequest(req) {
|
|
|
8
8
|
for (const entry of value)
|
|
9
9
|
headers.append(key, entry);
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
const method = req.method ?? "GET";
|
|
12
|
+
let body;
|
|
13
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of req)
|
|
16
|
+
chunks.push(chunk);
|
|
17
|
+
if (chunks.length > 0) {
|
|
18
|
+
const buffer = Buffer.concat(chunks);
|
|
19
|
+
if (buffer.byteLength > 0)
|
|
20
|
+
body = Uint8Array.from(buffer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return new Request(`http://${host}${req.url ?? "/"}`, { method, headers, body });
|
|
12
24
|
}
|
|
13
25
|
export function toNodeHandler(handler) {
|
|
14
26
|
return (req, res) => {
|
|
15
|
-
void
|
|
27
|
+
void toWebRequest(req)
|
|
28
|
+
.then(handler)
|
|
16
29
|
.then(async (response) => {
|
|
17
30
|
res.statusCode = response.status;
|
|
18
31
|
response.headers.forEach((value, key) => res.setHeader(key, value));
|