@downdraft/engine 0.3.0 → 0.3.1
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/app/src/renderer/bootstrap.ts +0 -6
- package/app/src/renderer/feature-log.ts +1 -9
- package/app/src/renderer/mcp-harness.ts +8 -11
- package/app/src/renderer/standard-automation-tools.ts +8 -145
- package/core/src/module/host.ts +3 -6
- package/core/src/module/index.ts +1 -1
- package/core/src/module/module.ts +4 -47
- package/core/src/physics/interface.ts +5 -0
- package/core/src/platform/render-surface.ts +13 -0
- package/core/src/render/canvas-resize-watcher.ts +1 -10
- package/core/src/render/game-renderer.ts +40 -18
- package/core/src/render/index.ts +0 -2
- package/core/src/render/input-manager.ts +7 -7
- package/core/src/render/renderer-module-host.ts +2 -4
- package/core/src/ui-font-scale.ts +5 -38
- package/core/src/worker/rpc.ts +65 -0
- package/dist/.dd-manifest.json +859 -884
- package/dist/app/src/renderer/bootstrap.js +0 -4
- package/dist/app/src/renderer/feature-log.js +1 -10
- package/dist/app/src/renderer/mcp-harness.js +8 -11
- package/dist/app/src/renderer/standard-automation-tools.js +6 -138
- package/dist/core/src/module/host.js +3 -6
- package/dist/core/src/render/canvas-resize-watcher.js +1 -10
- package/dist/core/src/render/game-renderer.js +44 -18
- package/dist/core/src/render/index.js +0 -1
- package/dist/core/src/render/input-manager.js +7 -7
- package/dist/core/src/render/renderer-module-host.js +2 -4
- package/dist/core/src/ui-font-scale.js +5 -38
- package/dist/core/src/worker/rpc.js +59 -0
- package/dist/libraries/blitz-ui/src/native-module.js +8 -1
- package/dist/libraries/devtools/src/native-providers.js +0 -2
- package/dist/libraries/entities/src/model-renderer.js +87 -27
- package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
- package/dist/libraries/networking/src/index.js +1 -0
- package/dist/libraries/networking/src/ws-server.js +387 -0
- package/dist/libraries/physics-rapier/src/api.js +15 -0
- package/dist/libraries/physics-rapier/src/backend.js +15 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1 -3
- package/dist/libraries/sand/src/materials.js +2 -32
- package/dist/libraries/sand/src/sand-world.js +0 -3
- package/dist/modules/devtools/src/api.js +8 -23
- package/dist/modules/devtools/src/data-bridge.js +0 -11
- package/dist/modules/devtools/src/index.js +0 -6
- package/dist/modules/devtools/src/init.js +8 -42
- package/dist/modules/devtools/src/scene-inspector.js +1 -4
- package/dist/modules/devtools/src/worker-sync.js +3 -11
- package/dist/test/src/vision-test.js +1 -3
- package/libraries/blitz-ui/src/native-module.ts +8 -1
- package/libraries/devtools/src/native-providers.ts +0 -2
- package/libraries/entities/src/model-renderer.ts +88 -27
- package/libraries/entities/src/shaders/model.wgsl +13 -4
- package/libraries/networking/src/index.ts +4 -0
- package/libraries/networking/src/ws-server.ts +397 -0
- package/libraries/physics-rapier/src/api.ts +15 -0
- package/libraries/physics-rapier/src/backend.ts +15 -0
- package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
- package/libraries/postfx/src/post-process-stack.ts +1 -2
- package/libraries/sand/src/materials.ts +2 -32
- package/libraries/sand/src/sand-world.ts +0 -4
- package/modules/devtools/README.md +1 -3
- package/modules/devtools/src/api.ts +10 -34
- package/modules/devtools/src/data-bridge.ts +0 -16
- package/modules/devtools/src/index.ts +1 -11
- package/modules/devtools/src/init.ts +8 -51
- package/modules/devtools/src/scene-inspector.ts +2 -6
- package/modules/devtools/src/types.ts +5 -90
- package/modules/devtools/src/worker-sync.ts +3 -13
- package/package.json +4 -5
- package/test/src/vision-test.ts +2 -4
- package/core/src/render/sab-canvas-overlay.ts +0 -122
- package/dist/core/src/render/sab-canvas-overlay.js +0 -83
- package/dist/modules/devtools/src/debug-overlay.js +0 -181
- package/dist/modules/devtools/src/doctor-panel.js +0 -163
- package/dist/modules/devtools/src/label-overlay.js +0 -129
- package/dist/modules/devtools/src/material-stats-panel.js +0 -171
- package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
- package/modules/devtools/src/debug-overlay.ts +0 -238
- package/modules/devtools/src/doctor-panel.ts +0 -182
- package/modules/devtools/src/label-overlay.ts +0 -154
- package/modules/devtools/src/material-stats-panel.ts +0 -188
- package/modules/devtools/src/sim-stats-panel.ts +0 -272
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ws-server.ts — minimal tri-runtime WebSocket server for host-authoritative
|
|
3
|
+
// LAN/WAN sessions. The networking library ships client transports
|
|
4
|
+
// (WebSocketTransport, WebRTCTransport) but had no listener — this fills the
|
|
5
|
+
// host side so a game instance can accept direct connections without any
|
|
6
|
+
// external service.
|
|
7
|
+
//
|
|
8
|
+
// Runtime selection is capability-based (see platform/runtime.ts):
|
|
9
|
+
// Bun → Bun.serve({ fetch, websocket })
|
|
10
|
+
// Deno → Deno.serve + Deno.upgradeWebSocket
|
|
11
|
+
// Node → node:http + a hand-rolled RFC6455 handshake/framer (pure JS,
|
|
12
|
+
// no native deps — also the fallback for any runtime with node:http)
|
|
13
|
+
//
|
|
14
|
+
// Semantics intentionally mirror the browser WebSocket surface: text frames
|
|
15
|
+
// arrive as `string`, binary frames as `Uint8Array`. Ping/pong/close and
|
|
16
|
+
// fragmented-frame reassembly are handled internally. Message cap matches
|
|
17
|
+
// WebSocketTransport's 16 MiB.
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
import { createLogger } from "@downdraft/engine/util/logger";
|
|
21
|
+
|
|
22
|
+
const log = createLogger();
|
|
23
|
+
|
|
24
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
25
|
+
const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
/** One accepted client connection on a {@link WsServer}. */
|
|
28
|
+
export interface WsServerConnection {
|
|
29
|
+
/** Server-assigned connection id (monotonic per server). */
|
|
30
|
+
readonly id: number;
|
|
31
|
+
/** Remote peer "ip:port" or best-effort address string. */
|
|
32
|
+
readonly remoteAddress: string;
|
|
33
|
+
/** Send a text (string) or binary (Uint8Array/ArrayBuffer) frame. */
|
|
34
|
+
send(data: string | Uint8Array | ArrayBuffer): void;
|
|
35
|
+
/** Initiate the close handshake (default code 1000). */
|
|
36
|
+
close(code?: number, reason?: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WebSocketServerHandlers {
|
|
40
|
+
/** A client completed the WS handshake. */
|
|
41
|
+
onConnection?(conn: WsServerConnection): void;
|
|
42
|
+
/** A complete (reassembled) message arrived. */
|
|
43
|
+
onMessage?(conn: WsServerConnection, data: Uint8Array | string): void;
|
|
44
|
+
/** The connection closed (cleanly or via error/timeout). */
|
|
45
|
+
onClose?(conn: WsServerConnection, code: number, reason: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WebSocketServerOptions extends WebSocketServerHandlers {
|
|
49
|
+
/** TCP port to listen on. 0 = ephemeral (read `server.port` after start). */
|
|
50
|
+
port: number;
|
|
51
|
+
/** Bind address — default "0.0.0.0" (all interfaces, for LAN/WAN). */
|
|
52
|
+
host?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WsServer {
|
|
56
|
+
/** The port actually bound (differs from opts.port when 0 was passed). */
|
|
57
|
+
readonly port: number;
|
|
58
|
+
/** The bound hostname. */
|
|
59
|
+
readonly host: string;
|
|
60
|
+
/** All currently-open connections. */
|
|
61
|
+
connections(): WsServerConnection[];
|
|
62
|
+
/** Stop listening and close every connection. */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Shared plumbing
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
interface InternalConn extends WsServerConnection {
|
|
71
|
+
/** Runtime-specific raw socket/sendable handle. */
|
|
72
|
+
raw: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeServerBase(handlers: WebSocketServerHandlers) {
|
|
76
|
+
const conns = new Set<InternalConn>();
|
|
77
|
+
let nextId = 1;
|
|
78
|
+
|
|
79
|
+
const deliver = (conn: InternalConn, data: Uint8Array | string) => {
|
|
80
|
+
const size = typeof data === "string" ? data.length : data.byteLength;
|
|
81
|
+
if (size > MAX_MESSAGE_SIZE) { conn.close(1009, "message too large"); return; }
|
|
82
|
+
try { handlers.onMessage?.(conn, data); } catch (e) { log.error("WsServer", `onMessage threw: ${e}`); }
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const drop = (conn: InternalConn, code: number, reason: string) => {
|
|
86
|
+
if (!conns.delete(conn)) return;
|
|
87
|
+
try { handlers.onClose?.(conn, code, reason); } catch (e) { log.error("WsServer", `onClose threw: ${e}`); }
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return { conns, nextId: () => nextId++, deliver, drop };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Bun — Bun.serve with the `websocket` handler map
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function startBunServer(opts: WebSocketServerOptions): Promise<WsServer> {
|
|
98
|
+
const Bun = (globalThis as any).Bun;
|
|
99
|
+
const base = makeServerBase(opts);
|
|
100
|
+
|
|
101
|
+
const server = (Bun.serve as (o: unknown) => { port: number; stop(force?: boolean): Promise<void> })({
|
|
102
|
+
port: opts.port,
|
|
103
|
+
hostname: opts.host ?? "0.0.0.0",
|
|
104
|
+
fetch(req: Request, srv: any) {
|
|
105
|
+
const ip = srv.requestIP(req);
|
|
106
|
+
const conn: InternalConn = {
|
|
107
|
+
id: base.nextId(),
|
|
108
|
+
remoteAddress: `${ip?.address ?? "?"}:${ip?.port ?? "?"}`,
|
|
109
|
+
raw: null,
|
|
110
|
+
send(data: string | Uint8Array | ArrayBuffer) { if (this.raw) (this.raw as any).send(data instanceof ArrayBuffer ? new Uint8Array(data) : data as never); },
|
|
111
|
+
close(code = 1000, reason = "") { if (this.raw) (this.raw as any).close(code, reason); },
|
|
112
|
+
};
|
|
113
|
+
return srv.upgrade(req, { data: { conn } }) ? undefined : new Response("WebSocket upgrade required", { status: 426 });
|
|
114
|
+
},
|
|
115
|
+
websocket: {
|
|
116
|
+
open(ws: any) {
|
|
117
|
+
const conn = ws.data.conn as InternalConn;
|
|
118
|
+
conn.raw = ws;
|
|
119
|
+
base.conns.add(conn);
|
|
120
|
+
try { opts.onConnection?.(conn); } catch (e) { log.error("WsServer", `onConnection threw: ${e}`); }
|
|
121
|
+
},
|
|
122
|
+
message(ws: any, msg: string | ArrayBuffer | Uint8Array) {
|
|
123
|
+
const conn = ws.data.conn as InternalConn;
|
|
124
|
+
base.deliver(conn, typeof msg === "string" ? msg : new Uint8Array(msg as ArrayBufferLike));
|
|
125
|
+
},
|
|
126
|
+
close(ws: any, code: number, reason: string) {
|
|
127
|
+
base.drop(ws.data.conn as InternalConn, code, reason);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return Promise.resolve({
|
|
133
|
+
port: server.port as number,
|
|
134
|
+
host: opts.host ?? "0.0.0.0",
|
|
135
|
+
connections: () => [...base.conns],
|
|
136
|
+
async close() {
|
|
137
|
+
[...base.conns].forEach((c) => c.close(1001, "server shutdown"));
|
|
138
|
+
await server.stop(true);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Deno — Deno.serve + Deno.upgradeWebSocket
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function startDenoServer(opts: WebSocketServerOptions): Promise<WsServer> {
|
|
148
|
+
const Deno = (globalThis as any).Deno;
|
|
149
|
+
const base = makeServerBase(opts);
|
|
150
|
+
|
|
151
|
+
const server = Deno.serve(
|
|
152
|
+
{ port: opts.port, hostname: opts.host ?? "0.0.0.0" },
|
|
153
|
+
(req: Request, info: { remoteAddr?: { hostname: string; port: number } }) => {
|
|
154
|
+
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
155
|
+
return new Response("WebSocket upgrade required", { status: 426 });
|
|
156
|
+
}
|
|
157
|
+
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
158
|
+
const conn: InternalConn = {
|
|
159
|
+
id: base.nextId(),
|
|
160
|
+
remoteAddress: info?.remoteAddr ? `${info.remoteAddr.hostname}:${info.remoteAddr.port}` : "?",
|
|
161
|
+
raw: socket,
|
|
162
|
+
send(data: string | Uint8Array | ArrayBuffer) { try { socket.send(data as never); } catch { /* closing */ } },
|
|
163
|
+
close(code = 1000, reason = "") { try { socket.close(code, reason); } catch { /* already closed */ } },
|
|
164
|
+
};
|
|
165
|
+
socket.binaryType = "arraybuffer";
|
|
166
|
+
socket.onopen = () => {
|
|
167
|
+
base.conns.add(conn);
|
|
168
|
+
try { opts.onConnection?.(conn); } catch (e) { log.error("WsServer", `onConnection threw: ${e}`); }
|
|
169
|
+
};
|
|
170
|
+
socket.onmessage = (e: MessageEvent) => {
|
|
171
|
+
base.deliver(conn, typeof e.data === "string" ? e.data : new Uint8Array(e.data as ArrayBuffer));
|
|
172
|
+
};
|
|
173
|
+
socket.onclose = (e: CloseEvent) => base.drop(conn, e.code, e.reason);
|
|
174
|
+
socket.onerror = () => base.drop(conn, 1006, "socket error");
|
|
175
|
+
return response;
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const bound = server.addr as { port: number; hostname: string };
|
|
180
|
+
return Promise.resolve({
|
|
181
|
+
port: bound.port,
|
|
182
|
+
host: bound.hostname,
|
|
183
|
+
connections: () => [...base.conns],
|
|
184
|
+
async close() {
|
|
185
|
+
[...base.conns].forEach((c) => c.close(1001, "server shutdown"));
|
|
186
|
+
await server.shutdown();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Node (and generic fallback) — node:http + hand-rolled RFC6455
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/** Encode one server→client frame. Server frames are never masked. */
|
|
196
|
+
function encodeWsFrame(opcode: number, payload: Uint8Array): Uint8Array {
|
|
197
|
+
const len = payload.byteLength;
|
|
198
|
+
let header: Uint8Array;
|
|
199
|
+
if (len < 126) {
|
|
200
|
+
header = new Uint8Array(2);
|
|
201
|
+
header[1] = len;
|
|
202
|
+
} else if (len <= 0xffff) {
|
|
203
|
+
header = new Uint8Array(4);
|
|
204
|
+
header[1] = 126;
|
|
205
|
+
header[2] = len >>> 8; header[3] = len & 0xff;
|
|
206
|
+
} else {
|
|
207
|
+
header = new Uint8Array(10);
|
|
208
|
+
header[1] = 127;
|
|
209
|
+
const dv = new DataView(header.buffer);
|
|
210
|
+
dv.setUint32(2, Math.floor(len / 0x100000000));
|
|
211
|
+
dv.setUint32(6, len >>> 0);
|
|
212
|
+
}
|
|
213
|
+
header[0] = 0x80 | (opcode & 0x0f);
|
|
214
|
+
const out = new Uint8Array(header.length + len);
|
|
215
|
+
out.set(header, 0);
|
|
216
|
+
out.set(payload, header.length);
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const OP_CONT = 0x0, OP_TEXT = 0x1, OP_BIN = 0x2, OP_CLOSE = 0x8, OP_PING = 0x9, OP_PONG = 0xa;
|
|
221
|
+
|
|
222
|
+
async function startNodeServer(opts: WebSocketServerOptions): Promise<WsServer> {
|
|
223
|
+
const http = await import("node:http");
|
|
224
|
+
const crypto = await import("node:crypto");
|
|
225
|
+
const base = makeServerBase(opts);
|
|
226
|
+
|
|
227
|
+
const httpServer = http.createServer((_req, res) => {
|
|
228
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
229
|
+
res.end("WebSocket upgrade required");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
httpServer.on("upgrade", (req: import("node:http").IncomingMessage, socket: import("node:net").Socket) => {
|
|
233
|
+
socket.setNoDelay(true);
|
|
234
|
+
const key = req.headers["sec-websocket-key"];
|
|
235
|
+
const upgrade = req.headers.upgrade?.toLowerCase();
|
|
236
|
+
if (upgrade !== "websocket" || typeof key !== "string") {
|
|
237
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
238
|
+
socket.destroy();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const accept = crypto.createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
242
|
+
socket.write(
|
|
243
|
+
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
244
|
+
"Upgrade: websocket\r\n" +
|
|
245
|
+
"Connection: Upgrade\r\n" +
|
|
246
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
let closed = false;
|
|
250
|
+
const conn: InternalConn = {
|
|
251
|
+
id: base.nextId(),
|
|
252
|
+
remoteAddress: `${socket.remoteAddress ?? "?"}:${socket.remotePort ?? "?"}`,
|
|
253
|
+
raw: socket,
|
|
254
|
+
send(data: string | Uint8Array | ArrayBuffer) {
|
|
255
|
+
if (closed || socket.destroyed) return;
|
|
256
|
+
if (typeof data === "string") {
|
|
257
|
+
socket.write(Buffer.from(encodeWsFrame(OP_TEXT, Buffer.from(data, "utf8"))));
|
|
258
|
+
} else {
|
|
259
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
260
|
+
socket.write(Buffer.from(encodeWsFrame(OP_BIN, bytes)));
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
close(code = 1000, reason = "") {
|
|
264
|
+
if (closed) return;
|
|
265
|
+
closed = true;
|
|
266
|
+
const payload = new Uint8Array(2 + reason.length);
|
|
267
|
+
payload[0] = code >>> 8; payload[1] = code & 0xff;
|
|
268
|
+
payload.set(new TextEncoder().encode(reason), 2);
|
|
269
|
+
try {
|
|
270
|
+
socket.write(Buffer.from(encodeWsFrame(OP_CLOSE, payload)));
|
|
271
|
+
socket.end();
|
|
272
|
+
} catch { /* socket already gone */ }
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// ── Incremental frame parser ──
|
|
277
|
+
let buf: Buffer = Buffer.alloc(0) as Buffer;
|
|
278
|
+
let fragOp = -1;
|
|
279
|
+
const frags: Buffer[] = [];
|
|
280
|
+
|
|
281
|
+
const finishMessage = (op: number, payload: Buffer) => {
|
|
282
|
+
if (payload.byteLength > MAX_MESSAGE_SIZE) { conn.close(1009, "message too large"); return; }
|
|
283
|
+
if (op === OP_TEXT) base.deliver(conn, payload.toString("utf8"));
|
|
284
|
+
else base.deliver(conn, new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength));
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const handleFrame = (fin: boolean, op: number, payload: Buffer) => {
|
|
288
|
+
if (op === OP_CLOSE) {
|
|
289
|
+
const code = payload.byteLength >= 2 ? payload.readUInt16BE(0) : 1000;
|
|
290
|
+
const reason = payload.byteLength > 2 ? payload.subarray(2).toString("utf8") : "";
|
|
291
|
+
conn.close(code, reason);
|
|
292
|
+
base.drop(conn, code, reason);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (op === OP_PING) { if (!closed && !socket.destroyed) socket.write(Buffer.from(encodeWsFrame(OP_PONG, payload))); return; }
|
|
296
|
+
if (op === OP_PONG) return;
|
|
297
|
+
if (op === OP_CONT && fragOp < 0) { conn.close(1002, "unexpected continuation"); return; }
|
|
298
|
+
if ((op === OP_TEXT || op === OP_BIN) && fragOp >= 0) { conn.close(1002, "interleaved frame"); return; }
|
|
299
|
+
|
|
300
|
+
if (op !== OP_CONT) fragOp = op;
|
|
301
|
+
frags.push(payload);
|
|
302
|
+
if (!fin) return;
|
|
303
|
+
const full = Buffer.concat(frags);
|
|
304
|
+
frags.length = 0;
|
|
305
|
+
const finalOp = fragOp;
|
|
306
|
+
fragOp = -1;
|
|
307
|
+
finishMessage(finalOp, full);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
socket.on("data", (chunk: Buffer) => {
|
|
311
|
+
buf = buf.byteLength ? Buffer.concat([buf, chunk]) : chunk;
|
|
312
|
+
// Parse as many complete frames as the buffer holds.
|
|
313
|
+
for (;;) {
|
|
314
|
+
if (buf.byteLength < 2) return;
|
|
315
|
+
const b0 = buf[0], b1 = buf[1];
|
|
316
|
+
const fin = (b0 & 0x80) !== 0;
|
|
317
|
+
const op = b0 & 0x0f;
|
|
318
|
+
const masked = (b1 & 0x80) !== 0;
|
|
319
|
+
let len = b1 & 0x7f;
|
|
320
|
+
let off = 2;
|
|
321
|
+
if (len === 126) {
|
|
322
|
+
if (buf.byteLength < off + 2) return;
|
|
323
|
+
len = buf.readUInt16BE(off); off += 2;
|
|
324
|
+
} else if (len === 127) {
|
|
325
|
+
if (buf.byteLength < off + 8) return;
|
|
326
|
+
const hi = buf.readUInt32BE(off), lo = buf.readUInt32BE(off + 4); off += 8;
|
|
327
|
+
if (hi > 0 || lo > MAX_MESSAGE_SIZE) { conn.close(1009, "message too large"); return; }
|
|
328
|
+
len = lo;
|
|
329
|
+
}
|
|
330
|
+
const maskLen = masked ? 4 : 0;
|
|
331
|
+
if (buf.byteLength < off + maskLen + len) return;
|
|
332
|
+
let payload = buf.subarray(off + maskLen, off + maskLen + len);
|
|
333
|
+
if (masked) {
|
|
334
|
+
const mask = buf.subarray(off, off + 4);
|
|
335
|
+
const unmasked = Buffer.allocUnsafe(payload.byteLength);
|
|
336
|
+
for (let i = 0; i < payload.byteLength; i++) unmasked[i] = payload[i] ^ mask[i & 3];
|
|
337
|
+
payload = unmasked;
|
|
338
|
+
} else {
|
|
339
|
+
payload = Buffer.from(payload); // detach — subarray aliases buf
|
|
340
|
+
}
|
|
341
|
+
buf = buf.subarray(off + maskLen + len);
|
|
342
|
+
handleFrame(fin, op, payload);
|
|
343
|
+
if (closed) return;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const onSocketEnd = () => base.drop(conn, 1006, "socket closed");
|
|
348
|
+
socket.on("close", onSocketEnd);
|
|
349
|
+
socket.on("error", onSocketEnd);
|
|
350
|
+
|
|
351
|
+
base.conns.add(conn);
|
|
352
|
+
try { opts.onConnection?.(conn); } catch (e) { log.error("WsServer", `onConnection threw: ${e}`); }
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await new Promise<void>((resolve, reject) => {
|
|
356
|
+
httpServer.once("error", reject);
|
|
357
|
+
httpServer.listen(opts.port, opts.host ?? "0.0.0.0", () => {
|
|
358
|
+
httpServer.removeListener("error", reject);
|
|
359
|
+
resolve();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const addr = httpServer.address();
|
|
364
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
port: boundPort,
|
|
368
|
+
host: opts.host ?? "0.0.0.0",
|
|
369
|
+
connections: () => [...base.conns],
|
|
370
|
+
async close() {
|
|
371
|
+
[...base.conns].forEach((c) => c.close(1001, "server shutdown"));
|
|
372
|
+
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
|
|
373
|
+
// httpServer.close waits for open sockets; force-destroy stragglers.
|
|
374
|
+
[...base.conns].forEach((c) => (c.raw as import("node:net").Socket).destroy());
|
|
375
|
+
base.conns.clear();
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Entry point
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Start a WebSocket server. Resolves once the socket is bound and listening.
|
|
386
|
+
* Runtime is picked by capability: Bun.serve → Deno.serve → node:http.
|
|
387
|
+
*/
|
|
388
|
+
export function createWebSocketServer(opts: WebSocketServerOptions): Promise<WsServer> {
|
|
389
|
+
const g = globalThis as Record<string, unknown>;
|
|
390
|
+
const bun = g.Bun as { serve?: unknown } | undefined;
|
|
391
|
+
if (typeof bun?.serve === "function") return startBunServer(opts);
|
|
392
|
+
const deno = g.Deno as { serve?: unknown; upgradeWebSocket?: unknown } | undefined;
|
|
393
|
+
if (typeof deno?.serve === "function" && typeof deno.upgradeWebSocket === "function") {
|
|
394
|
+
return startDenoServer(opts);
|
|
395
|
+
}
|
|
396
|
+
return startNodeServer(opts);
|
|
397
|
+
}
|
|
@@ -298,6 +298,21 @@ export class UniversalPhysicsAPI {
|
|
|
298
298
|
this.backend.setIntegrationDt(realmId, dt);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Change gravity across all realms (applies to near/mid/far). Best-effort:
|
|
303
|
+
* backends that can't mutate realm gravity omit `setRealmGravity`, in which
|
|
304
|
+
* case callers should fall back to per-body `gravityScale` in BodyDesc.
|
|
305
|
+
*/
|
|
306
|
+
setGravity(gravity: [number, number, number]): void {
|
|
307
|
+
[RealmTier.Near, RealmTier.Mid, RealmTier.Far].forEach((tier) => {
|
|
308
|
+
try {
|
|
309
|
+
const realm = this.realmManager.getRealm(tier);
|
|
310
|
+
this.backend.setRealmGravity?.(realm.id, gravity);
|
|
311
|
+
} catch { /* realm tier not initialized */ }
|
|
312
|
+
});
|
|
313
|
+
this.config.gravity = gravity;
|
|
314
|
+
}
|
|
315
|
+
|
|
301
316
|
// --- Realm ---
|
|
302
317
|
|
|
303
318
|
getRealmTier(body: PhysicsBody): RealmTier {
|
|
@@ -472,6 +472,21 @@ export class RapierPhysicsBackend implements PhysicsBackend {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Change a live realm's gravity. Updates the stored realm config (the
|
|
477
|
+
* no-lib JS fallback stepper reads it) and forwards to the lib when it
|
|
478
|
+
* supports runtime gravity (the WASM Rapier world.gravity is mutable; the
|
|
479
|
+
* native FFI lib may not implement it — callers should treat this as
|
|
480
|
+
* best-effort).
|
|
481
|
+
*/
|
|
482
|
+
setRealmGravity(realmId: number, gravity: [number, number, number]): void {
|
|
483
|
+
const realm = this.realms.get(realmId);
|
|
484
|
+
if (realm) realm.config.gravity = gravity;
|
|
485
|
+
if (this.lib && this.lib.setRealmGravity) {
|
|
486
|
+
this.lib.setRealmGravity(realmId, gravity);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
475
490
|
setSleepThresholds(realmId: number, linearThreshold: number, angularThreshold: number): void {
|
|
476
491
|
if (this.lib) {
|
|
477
492
|
this.lib.setSleepThresholds(realmId, linearThreshold, angularThreshold);
|
|
@@ -7,6 +7,8 @@ const log = createLogger();
|
|
|
7
7
|
export interface PhysicsLib {
|
|
8
8
|
createRealm(id: number, gravity: [number, number, number]): void;
|
|
9
9
|
destroyRealm(id: number): void;
|
|
10
|
+
/** Change a live realm's gravity (Rapier `world.gravity` is mutable). */
|
|
11
|
+
setRealmGravity?(realmId: number, gravity: [number, number, number]): void;
|
|
10
12
|
createBody(realmId: number, bodyId: number, desc: BodyDesc, entity: Entity): void;
|
|
11
13
|
destroyBody(realmId: number, bodyId: number): void;
|
|
12
14
|
setBodyType(realmId: number, bodyId: number, type: BodyType): void;
|
|
@@ -251,6 +253,10 @@ async function doLoadPhysicsLib(): Promise<PhysicsLib> {
|
|
|
251
253
|
entityByBodyHandle.delete(id);
|
|
252
254
|
bodyIdByHandle.delete(id);
|
|
253
255
|
},
|
|
256
|
+
setRealmGravity(id, gravity) {
|
|
257
|
+
const world = realms.get(id);
|
|
258
|
+
if (world) world.gravity = { x: gravity[0], y: gravity[1], z: gravity[2] };
|
|
259
|
+
},
|
|
254
260
|
createBody(realmId, bodyId, desc, entity) {
|
|
255
261
|
const world = realms.get(realmId);
|
|
256
262
|
if (!world) return;
|
|
@@ -546,8 +546,7 @@ export class PostProcessStack {
|
|
|
546
546
|
private createGlyphAtlas(): void {
|
|
547
547
|
const chars = " .:-=+*#%@";
|
|
548
548
|
const gw = 16, gh = 16, cols = chars.length;
|
|
549
|
-
const canvas =
|
|
550
|
-
canvas.width = cols * gw; canvas.height = gh;
|
|
549
|
+
const canvas = new OffscreenCanvas(cols * gw, gh);
|
|
551
550
|
const ctx = canvas.getContext("2d")!;
|
|
552
551
|
ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
553
552
|
ctx.fillStyle = "white"; ctx.font = `bold ${gh - 2}px monospace`;
|
|
@@ -71,32 +71,18 @@ export enum Material {
|
|
|
71
71
|
// --- Ingredients ---
|
|
72
72
|
// Ice (frozen water — produced by cooling)
|
|
73
73
|
Ice = 64,
|
|
74
|
+
// (IDs 67–68, 71–73, 76–77, 79–81, 83–88 were inert alchemy materials —
|
|
75
|
+
// retired; keep the gaps so old saves don't remap onto new materials.)
|
|
74
76
|
// Liquids
|
|
75
77
|
Ether = 65, // ethereal solvent — light, volatile, glowing
|
|
76
78
|
Blood = 66, // viscous, necrotic-tinted
|
|
77
|
-
Syrup = 67, // thick, dense, sweet (binding agent)
|
|
78
|
-
NightshadeExtract = 68,// toxic liquid
|
|
79
79
|
TrollBlood = 69, // viscous, healing + necrotic
|
|
80
80
|
LiquidShadow = 70, // dense, light-absorbing
|
|
81
|
-
LoveEssence = 71, // psychic liquid
|
|
82
|
-
HateEssence = 72, // psychic liquid, corrosive
|
|
83
|
-
DreamMist = 73, // ethereal + psychic, gas-like liquid
|
|
84
81
|
VoidEssence = 74, // dense, necrotic, reality-bending
|
|
85
82
|
// Powders (solid, fall)
|
|
86
83
|
Sulfur = 75, // yellow powder, flammable, corrosive
|
|
87
|
-
GroundEyeOfNewt = 76, // psychic powder
|
|
88
|
-
GroundBatWing = 77, // ethereal powder
|
|
89
84
|
BoneDust = 78, // necrotic powder
|
|
90
|
-
IronFilings = 79, // kinetic + dense powder, magnetic
|
|
91
|
-
MoonstoneDust = 80, // luminous + ethereal powder
|
|
92
|
-
CrystalDust = 81, // luminous + kinetic powder
|
|
93
85
|
MushroomSpores = 82, // toxic + psychic powder, grows
|
|
94
|
-
DragonScale = 83, // thermal + kinetic, very dense
|
|
95
|
-
PhoenixFeather = 84, // thermal + healing, light
|
|
96
|
-
UnicornHorn = 85, // healing + luminous, rare
|
|
97
|
-
MandrakeRoot = 86, // toxic + psychic, solid root
|
|
98
|
-
SpiderSilk = 87, // kinetic + ethereal, very light
|
|
99
|
-
GraveDust = 88, // necrotic + dense
|
|
100
86
|
StarShard = 89, // luminous + ethereal + kinetic, glowing crystal
|
|
101
87
|
TimeSand = 90, // kinetic + ethereal, reality-bending
|
|
102
88
|
// Gases
|
|
@@ -283,30 +269,14 @@ export const MATERIALS: Record<number, MaterialDef> = {
|
|
|
283
269
|
// --- Liquids ---
|
|
284
270
|
[Material.Ether]: def(65, "Ether", [0.55, 0.45, 0.85, 0.8], { gravity: 1.5, gravityDir: 1, density: 0.7, liquid: true, flammable: true, burnTime: 40, albedo: 0.2, reflectivity: 0.4, brightness: 1.1 }),
|
|
285
271
|
[Material.Blood]: def(66, "Blood", [0.7, 0.05, 0.05, 0.95], { gravity: 2, gravityDir: 1, density: 1.06, liquid: true, albedo: 0.3, reflectivity: 0.2, brightness: 0.8 }),
|
|
286
|
-
[Material.Syrup]: def(67, "Syrup", [0.6, 0.4, 0.15, 0.95], { gravity: 1, gravityDir: 1, density: 1.4, liquid: true, albedo: 0.3, reflectivity: 0.3, brightness: 0.9 }),
|
|
287
|
-
[Material.NightshadeExtract]: def(68, "Nightshade Extract", [0.3, 0.05, 0.35, 0.95], { gravity: 2, gravityDir: 1, density: 1.1, liquid: true, albedo: 0.2, reflectivity: 0.1, brightness: 0.7 }),
|
|
288
272
|
[Material.TrollBlood]: def(69, "Troll Blood", [0.4, 0.2, 0.15, 0.95], { gravity: 2, gravityDir: 1, density: 1.2, liquid: true, albedo: 0.25, reflectivity: 0.15, brightness: 0.8 }),
|
|
289
273
|
[Material.LiquidShadow]: def(70, "Liquid Shadow", [0.05, 0.05, 0.12, 0.98], { gravity: 3, gravityDir: 1, density: 2.0, liquid: true, albedo: 0.0, reflectivity: 0.0, brightness: 0.3 }),
|
|
290
|
-
[Material.LoveEssence]: def(71, "Love Essence", [0.95, 0.4, 0.6, 0.9], { gravity: 1.5, gravityDir: 1, density: 0.9, liquid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.0 }),
|
|
291
|
-
[Material.HateEssence]: def(72, "Hate Essence", [0.2, 0.1, 0.15, 0.9], { gravity: 2, gravityDir: 1, density: 1.3, liquid: true, albedo: 0.2, reflectivity: 0.2, brightness: 0.7 }),
|
|
292
|
-
[Material.DreamMist]: def(73, "Dream Mist", [0.6, 0.7, 0.95, 0.6], { gravity: 1, gravityDir: -1, density: 0.3, liquid: true, albedo: 0.2, reflectivity: 0.5, brightness: 1.2 }),
|
|
293
274
|
[Material.VoidEssence]: def(74, "Void Essence", [0.1, 0.0, 0.2, 0.95], { gravity: 4, gravityDir: 1, density: 3.0, liquid: true, albedo: 0.0, reflectivity: 0.1, brightness: 0.5 }),
|
|
294
275
|
|
|
295
276
|
// --- Alchemy game: powders (solid, fall) ---
|
|
296
277
|
[Material.Sulfur]: def(75, "Sulfur", [0.9, 0.85, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, flammable: true, burnTime: 60, albedo: 0.5, reflectivity: 0.1, brightness: 0.9 }),
|
|
297
|
-
[Material.GroundEyeOfNewt]: def(76, "Ground Eye of Newt", [0.5, 0.35, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 1.2, solid: true, albedo: 0.4, brightness: 0.8 }),
|
|
298
|
-
[Material.GroundBatWing]: def(77, "Ground Bat Wing", [0.3, 0.25, 0.35, 1.0], { gravity: 0.8, gravityDir: 1, density: 0.6, solid: true, albedo: 0.3, brightness: 0.7 }),
|
|
299
278
|
[Material.BoneDust]: def(78, "Bone Dust", [0.85, 0.82, 0.75, 1.0], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.5, brightness: 0.8 }),
|
|
300
|
-
[Material.IronFilings]: def(79, "Iron Filings", [0.55, 0.55, 0.6, 1.0], { gravity: 1.5, gravityDir: 1, density: 7.8, solid: true, magnetic: true, albedo: 0.5, reflectivity: 0.3 }),
|
|
301
|
-
[Material.MoonstoneDust]: def(80, "Moonstone Dust", [0.8, 0.85, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 1.0, solid: true, albedo: 0.4, reflectivity: 0.5, brightness: 1.3 }),
|
|
302
|
-
[Material.CrystalDust]: def(81, "Crystal Dust", [0.6, 0.9, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 2.5, solid: true, albedo: 0.3, reflectivity: 0.6, brightness: 1.2 }),
|
|
303
279
|
[Material.MushroomSpores]: def(82, "Mushroom Spores", [0.6, 0.5, 0.4, 0.9], { gravity: 0.5, gravityDir: 1, density: 0.4, solid: true, albedo: 0.3, brightness: 0.8 }),
|
|
304
|
-
[Material.DragonScale]: def(83, "Dragon Scale", [0.5, 0.15, 0.1, 1.0], { gravity: 1.5, gravityDir: 1, density: 5.0, solid: true, albedo: 0.4, reflectivity: 0.3, brightness: 1.0 }),
|
|
305
|
-
[Material.PhoenixFeather]: def(84, "Phoenix Feather", [0.95, 0.5, 0.15, 0.9], { gravity: 0.3, gravityDir: 1, density: 0.3, solid: true, flammable: true, burnTime: 200, albedo: 0.4, brightness: 1.4 }),
|
|
306
|
-
[Material.UnicornHorn]: def(85, "Unicorn Horn", [0.95, 0.9, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.5, reflectivity: 0.5, brightness: 1.5 }),
|
|
307
|
-
[Material.MandrakeRoot]: def(86, "Mandrake Root", [0.5, 0.4, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.3, solid: true, albedo: 0.4, brightness: 0.7 }),
|
|
308
|
-
[Material.SpiderSilk]: def(87, "Spider Silk", [0.9, 0.9, 0.85, 0.8], { gravity: 0.2, gravityDir: 1, density: 0.2, solid: true, albedo: 0.4, reflectivity: 0.2, brightness: 0.9 }),
|
|
309
|
-
[Material.GraveDust]: def(88, "Grave Dust", [0.3, 0.28, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.3, brightness: 0.6 }),
|
|
310
280
|
[Material.StarShard]: def(89, "Star Shard", [0.7, 0.8, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, albedo: 0.3, reflectivity: 0.7, brightness: 1.6 }),
|
|
311
281
|
[Material.TimeSand]: def(90, "Time Sand", [0.4, 0.3, 0.6, 0.9], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.2 }),
|
|
312
282
|
|
|
@@ -2130,10 +2130,6 @@ export class SandWorld {
|
|
|
2130
2130
|
}
|
|
2131
2131
|
}
|
|
2132
2132
|
|
|
2133
|
-
// --- PhoenixFeather near fire/lava: glows brighter, resists burning ---
|
|
2134
|
-
// (flammable but long burnTime already set; here we just let it shimmer)
|
|
2135
|
-
// --- DragonScale: very dense, sinks through liquids (handled by density) ---
|
|
2136
|
-
|
|
2137
2133
|
// --- LiquidShadow + light source (fire/plasma) → dissipates to smoke ---
|
|
2138
2134
|
if (mat === Material.LiquidShadow) {
|
|
2139
2135
|
let lightNi = -1;
|
|
@@ -14,12 +14,10 @@ bun add @downdraft/engine/modules/devtools
|
|
|
14
14
|
- `BaseSceneInspector`
|
|
15
15
|
- `DEVTOOLS_REALM`
|
|
16
16
|
- `DEVTOOLS_SAB_MAX_FEEDS`
|
|
17
|
-
- `DebugOverlay`
|
|
18
17
|
- `DebugRaycast`
|
|
19
18
|
- `DevToolsDataBridge`
|
|
20
19
|
- `GridRenderer`
|
|
21
20
|
- `HeightRulerRenderer`
|
|
22
|
-
-
|
|
23
|
-
- …and 29 more
|
|
21
|
+
- …and 24 more
|
|
24
22
|
|
|
25
23
|
See the [Downdraft engine repository](https://github.com/knackstedt/downdraft) for architecture docs and examples.
|