@downdraft/engine 0.2.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/index.ts +2 -2
- 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/platform/rpc.ts +4 -1
- package/core/src/render/canvas-resize-watcher.ts +1 -10
- package/core/src/render/game-renderer.ts +164 -79
- 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/sab/grid-buffer.ts +55 -2
- package/core/src/sab/record-buffer.ts +48 -5
- package/core/src/sab/sim-channel.ts +181 -17
- package/core/src/sab/slot-buffer.ts +55 -1
- package/core/src/sab/types.ts +11 -0
- package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
- package/core/src/ui-font-scale.ts +5 -38
- package/core/src/util/index.ts +1 -0
- package/core/src/util/session-timer.ts +23 -0
- package/core/src/worker/base-worker-host.ts +2 -1
- package/core/src/worker/buffer-sync.ts +20 -13
- package/core/src/worker/rpc.ts +119 -5
- package/core/src/worker/sim-worker-base.ts +5 -2
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +893 -908
- 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/index.js +2 -2
- package/dist/core/src/module/host.js +3 -6
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/canvas-resize-watcher.js +1 -10
- package/dist/core/src/render/game-renderer.js +166 -74
- 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/sab/grid-buffer.js +53 -1
- package/dist/core/src/sab/record-buffer.js +49 -5
- package/dist/core/src/sab/sim-channel.js +173 -17
- package/dist/core/src/sab/slot-buffer.js +54 -1
- package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
- package/dist/core/src/ui-font-scale.js +5 -38
- package/dist/core/src/util/index.js +1 -0
- package/dist/core/src/util/session-timer.js +22 -0
- package/dist/core/src/worker/base-worker-host.js +2 -1
- package/dist/core/src/worker/buffer-sync.js +16 -9
- package/dist/core/src/worker/rpc.js +105 -8
- package/dist/core/src/worker/sim-worker-base.js +5 -2
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/blitz-ui/src/native-module.js +8 -1
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- 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/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +124 -30
- package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
- package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
- package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
- package/dist/modules/devtools/src/blitz/panels/performance.js +26 -12
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
- package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
- package/dist/modules/devtools/src/blitz/theme.js +26 -4
- 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/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/test/src/vision-test.js +1 -3
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/blitz-ui/src/native-module.ts +8 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- 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/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +107 -31
- package/modules/devtools/src/blitz/panels/console.ts +20 -8
- package/modules/devtools/src/blitz/panels/elements.ts +212 -31
- package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
- package/modules/devtools/src/blitz/panels/performance.ts +24 -12
- package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
- package/modules/devtools/src/blitz/panels/types.ts +2 -1
- package/modules/devtools/src/blitz/theme.ts +26 -4
- 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/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/protocol.ts +1 -1
- 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
|
@@ -86,9 +86,11 @@ export class ModelRenderer {
|
|
|
86
86
|
private reusableUniformsU32 = new Uint32Array(this.reusableUniforms.buffer);
|
|
87
87
|
private textureLoadVersion = new Map<string, number>();
|
|
88
88
|
|
|
89
|
-
// Skinning: a shared storage buffer of bone matrices (
|
|
90
|
-
//
|
|
91
|
-
// draws only.
|
|
89
|
+
// Skinning: a shared storage buffer of bone matrices (mat4 each), updated
|
|
90
|
+
// per-frame via updateSkinMatrices/updateSkinMatricesFor. Bound at @group(1)
|
|
91
|
+
// for skinned draws only. Each skinned nodeId owns a region of the buffer —
|
|
92
|
+
// the draw's `boneBase` uniform selects it so multiple skinned models can be
|
|
93
|
+
// animated independently in one frame.
|
|
92
94
|
private skinMatrixBuffer: GPUBuffer | null = null;
|
|
93
95
|
private skinBindGroupLayout: GPUBindGroupLayout | null = null;
|
|
94
96
|
private skinBindGroup: GPUBindGroup | null = null;
|
|
@@ -96,6 +98,16 @@ export class ModelRenderer {
|
|
|
96
98
|
private skinBindGroupSetThisFrame = false;
|
|
97
99
|
/** Current bone capacity (number of mat4s the skin buffer can hold). Grows on demand. */
|
|
98
100
|
private boneCapacity = ModelRenderer.INITIAL_BONE_CAPACITY;
|
|
101
|
+
/** nodeId → reserved bone region (offset+count in bones). */
|
|
102
|
+
private skinRegions = new Map<string, { offset: number; count: number }>();
|
|
103
|
+
/** nodeId → bone base index used by the skinning shader for that draw. */
|
|
104
|
+
private skinBaseByNode = new Map<string, number>();
|
|
105
|
+
/** Freed bone regions available for reuse (first-fit). */
|
|
106
|
+
private skinFreeList: Array<{ offset: number; count: number }> = [];
|
|
107
|
+
/** Next never-allocated bone index (high-water mark). */
|
|
108
|
+
private nextBoneOffset = 0;
|
|
109
|
+
/** Legacy caller region — updateSkinMatrices(m) without a nodeId. */
|
|
110
|
+
private static readonly DEFAULT_SKIN_NODE = "__skin_default";
|
|
99
111
|
|
|
100
112
|
// Bindless deps
|
|
101
113
|
private bindless: ModelRendererBindlessDeps | null = null;
|
|
@@ -822,6 +834,7 @@ export class ModelRenderer {
|
|
|
822
834
|
}
|
|
823
835
|
|
|
824
836
|
removeModel(nodeId: string): void {
|
|
837
|
+
this.releaseSkinRegion(nodeId);
|
|
825
838
|
const resources = this.modelResources.get(nodeId);
|
|
826
839
|
if (resources) {
|
|
827
840
|
for (let i = 0; i < resources.length; i++) {
|
|
@@ -1082,43 +1095,85 @@ export class ModelRenderer {
|
|
|
1082
1095
|
}
|
|
1083
1096
|
|
|
1084
1097
|
/**
|
|
1085
|
-
* Upload bone skin matrices for the current frame
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
* small for the current skeleton, it is recreated (grown) automatically.
|
|
1089
|
-
* Call once per frame before render() for skinned models.
|
|
1098
|
+
* Upload bone skin matrices for the current frame into the shared default
|
|
1099
|
+
* region. Kept for single-avatar callers — equivalent to
|
|
1100
|
+
* updateSkinMatricesFor("__skin_default", matrices).
|
|
1090
1101
|
*/
|
|
1091
1102
|
updateSkinMatrices(matrices: Float32Array): void {
|
|
1103
|
+
this.updateSkinMatricesFor(ModelRenderer.DEFAULT_SKIN_NODE, matrices);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Upload bone skin matrices for one nodeId's reserved region. The region is
|
|
1108
|
+
* (re)allocated automatically — freed regions from removeModel() are reused
|
|
1109
|
+
* first-fit. Render() looks up the node's region offset and passes it to the
|
|
1110
|
+
* skinning shader via the `boneBase` uniform, so multiple skinned models
|
|
1111
|
+
* animate independently.
|
|
1112
|
+
*/
|
|
1113
|
+
updateSkinMatricesFor(nodeId: string, matrices: Float32Array): void {
|
|
1092
1114
|
if (!this.skinMatrixBuffer) return;
|
|
1093
1115
|
|
|
1094
1116
|
const requiredBones = matrices.length / 16;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1100
|
-
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1101
|
-
size: newCapacity * 64,
|
|
1102
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1103
|
-
});
|
|
1104
|
-
this.boneCapacity = newCapacity;
|
|
1105
|
-
// Recreate the bind group with the new buffer
|
|
1106
|
-
if (this.skinBindGroupLayout) {
|
|
1107
|
-
this.skinBindGroup = this.device.createBindGroup({
|
|
1108
|
-
layout: this.skinBindGroupLayout,
|
|
1109
|
-
entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
this.skinBindGroupSetThisFrame = false; // force re-bind
|
|
1117
|
+
let region = this.skinRegions.get(nodeId);
|
|
1118
|
+
if (!region || region.count < requiredBones) {
|
|
1119
|
+
if (region) this.skinFreeList.push(region);
|
|
1120
|
+
region = this.allocSkinRegion(requiredBones);
|
|
1121
|
+
this.skinRegions.set(nodeId, region);
|
|
1113
1122
|
}
|
|
1123
|
+
this.skinBaseByNode.set(nodeId, region.offset);
|
|
1114
1124
|
|
|
1115
1125
|
this.device.queue.writeBuffer(
|
|
1116
1126
|
this.skinMatrixBuffer,
|
|
1117
|
-
|
|
1127
|
+
region.offset * 64,
|
|
1118
1128
|
matrices as Float32Array<ArrayBuffer>,
|
|
1119
1129
|
);
|
|
1120
1130
|
}
|
|
1121
1131
|
|
|
1132
|
+
/** Allocate a bone region for `count` bones — free-list first, then bump
|
|
1133
|
+
* allocation; grows the shared buffer when the capacity is exceeded. */
|
|
1134
|
+
private allocSkinRegion(count: number): { offset: number; count: number } {
|
|
1135
|
+
for (let i = 0; i < this.skinFreeList.length; i++) {
|
|
1136
|
+
const f = this.skinFreeList[i];
|
|
1137
|
+
if (f.count >= count) {
|
|
1138
|
+
this.skinFreeList.splice(i, 1);
|
|
1139
|
+
return { offset: f.offset, count: f.count };
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const needed = this.nextBoneOffset + count;
|
|
1143
|
+
if (needed > this.boneCapacity) this.growSkinBuffer(needed);
|
|
1144
|
+
const region = { offset: this.nextBoneOffset, count };
|
|
1145
|
+
this.nextBoneOffset += count;
|
|
1146
|
+
return region;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** Recreate the skin matrix buffer at a larger capacity and rebind group(1).
|
|
1150
|
+
* Region offsets stay valid — callers rewrite their matrices next frame. */
|
|
1151
|
+
private growSkinBuffer(minBones: number): void {
|
|
1152
|
+
const newCapacity = Math.max(minBones, this.boneCapacity * 2);
|
|
1153
|
+
this.skinMatrixBuffer?.destroy();
|
|
1154
|
+
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1155
|
+
size: newCapacity * 64,
|
|
1156
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1157
|
+
});
|
|
1158
|
+
this.boneCapacity = newCapacity;
|
|
1159
|
+
if (this.skinBindGroupLayout) {
|
|
1160
|
+
this.skinBindGroup = this.device.createBindGroup({
|
|
1161
|
+
layout: this.skinBindGroupLayout,
|
|
1162
|
+
entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
this.skinBindGroupSetThisFrame = false; // force re-bind
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/** Release a node's bone region back to the free list. */
|
|
1169
|
+
releaseSkinRegion(nodeId: string): void {
|
|
1170
|
+
const region = this.skinRegions.get(nodeId);
|
|
1171
|
+
if (!region) return;
|
|
1172
|
+
this.skinRegions.delete(nodeId);
|
|
1173
|
+
this.skinBaseByNode.delete(nodeId);
|
|
1174
|
+
this.skinFreeList.push(region);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1122
1177
|
render(
|
|
1123
1178
|
passEncoder: GPURenderPassEncoder,
|
|
1124
1179
|
nodeId: string,
|
|
@@ -1221,6 +1276,12 @@ export class ModelRenderer {
|
|
|
1221
1276
|
// fs_outline). Defaults: 0 width, black color.
|
|
1222
1277
|
uniforms[39] = 0.0;
|
|
1223
1278
|
uniforms[40] = 0.0; uniforms[41] = 0.0; uniforms[42] = 0.0;
|
|
1279
|
+
// boneBase (u32, slot 44) — index of this node's bone region inside the
|
|
1280
|
+
// shared skinMatrices storage buffer. Only read by vs_skinned.
|
|
1281
|
+
this.reusableUniformsU32[44] =
|
|
1282
|
+
this.skinBaseByNode.get(nodeId)
|
|
1283
|
+
?? this.skinBaseByNode.get(ModelRenderer.DEFAULT_SKIN_NODE)
|
|
1284
|
+
?? 0;
|
|
1224
1285
|
|
|
1225
1286
|
this.device.queue.writeBuffer(
|
|
1226
1287
|
this.uniformBuffer,
|
|
@@ -22,6 +22,14 @@ struct Uniforms {
|
|
|
22
22
|
// Inverted-hull outline parameters (used by vs_outline / fs_outline only).
|
|
23
23
|
outlineWidth: f32,
|
|
24
24
|
outlineColor: vec3<f32>,
|
|
25
|
+
// Base index of this draw's bone region inside the shared skinMatrices
|
|
26
|
+
// storage buffer (group(1)). Written by ModelRenderer from the node's
|
|
27
|
+
// reserved region — lets multiple skinned models animate independently.
|
|
28
|
+
// Only read by vs_skinned.
|
|
29
|
+
boneBase: u32,
|
|
30
|
+
_padSkin0: u32,
|
|
31
|
+
_padSkin1: u32,
|
|
32
|
+
_padSkin2: u32,
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
@@ -165,10 +173,11 @@ fn qrotate(q: vec4<f32>, v: vec3<f32>) -> vec3<f32> {
|
|
|
165
173
|
// If all weights are zero (vertex has no bone assignment), return identity so
|
|
166
174
|
// the vertex renders at its baked position instead of collapsing to the origin.
|
|
167
175
|
fn skinMatrix(j: vec4<u32>, w: vec4<f32>) -> mat4x4<f32> {
|
|
168
|
-
let
|
|
169
|
-
|
|
170
|
-
+ skinMatrices[j.
|
|
171
|
-
+ skinMatrices[j.
|
|
176
|
+
let base = uniforms.boneBase;
|
|
177
|
+
let m = skinMatrices[base + j.x] * w.x
|
|
178
|
+
+ skinMatrices[base + j.y] * w.y
|
|
179
|
+
+ skinMatrices[base + j.z] * w.z
|
|
180
|
+
+ skinMatrices[base + j.w] * w.w;
|
|
172
181
|
let wsum = w.x + w.y + w.z + w.w;
|
|
173
182
|
if (wsum < 0.001) {
|
|
174
183
|
return mat4x4<f32>(
|
|
@@ -43,3 +43,7 @@ export type { NetworkingLibConfig } from "./library";
|
|
|
43
43
|
|
|
44
44
|
export { LanDiscovery, MdnsBrowser } from "./discovery";
|
|
45
45
|
export type { LanBeacon, LanDiscoveryOptions, LanPeer, MdnsBrowseOptions, MdnsService } from "./discovery";
|
|
46
|
+
|
|
47
|
+
export { createWebSocketServer } from "./ws-server";
|
|
48
|
+
export type { WebSocketServerHandlers, WebSocketServerOptions, WsServer, WsServerConnection } from "./ws-server";
|
|
49
|
+
|
|
@@ -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`;
|