@crowdedkingdoms/construct 0.0.0 → 0.2.1-dev.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.
Files changed (69) hide show
  1. package/README.md +76 -1
  2. package/node/copy-dsh-web.mjs +71 -0
  3. package/node/security-headers.d.mts +23 -0
  4. package/node/security-headers.mjs +243 -0
  5. package/node/vite-plugins.d.mts +6 -0
  6. package/node/vite-plugins.mjs +102 -0
  7. package/package.json +48 -3
  8. package/sandbox/boot.ts +74 -0
  9. package/src/engine/GameLoop.ts +106 -0
  10. package/src/engine/GameScene.ts +74 -0
  11. package/src/engine/Input.test.ts +129 -0
  12. package/src/engine/Input.ts +257 -0
  13. package/src/engine/SceneRouter.test.ts +80 -0
  14. package/src/engine/SceneRouter.ts +113 -0
  15. package/src/engine/controls.test.ts +26 -0
  16. package/src/engine/controls.ts +63 -0
  17. package/src/grid/GridProgramRunner.ts +205 -0
  18. package/src/grid/sandboxProtocol.ts +9 -0
  19. package/src/index.ts +88 -0
  20. package/src/platform/GameSession.ts +239 -0
  21. package/src/platform/auth/AuthService.ts +45 -0
  22. package/src/platform/config.test.ts +110 -0
  23. package/src/platform/config.ts +212 -0
  24. package/src/platform/envScope.test.ts +42 -0
  25. package/src/platform/envScope.ts +76 -0
  26. package/src/platform/exec/worldHub.test.ts +61 -0
  27. package/src/platform/exec/worldHub.ts +86 -0
  28. package/src/platform/media/VoiceService.ts +423 -0
  29. package/src/platform/media/WebcamService.ts +346 -0
  30. package/src/platform/media/mulaw.test.ts +20 -0
  31. package/src/platform/media/mulaw.ts +56 -0
  32. package/src/platform/media/videoFrames.test.ts +197 -0
  33. package/src/platform/media/voiceFrames.test.ts +82 -0
  34. package/src/platform/model/ModelService.test.ts +59 -0
  35. package/src/platform/model/ModelService.ts +124 -0
  36. package/src/platform/network/NetworkManager.ts +519 -0
  37. package/src/platform/onboarding/steps.d.mts +154 -0
  38. package/src/platform/onboarding/steps.mjs +616 -0
  39. package/src/platform/onboarding/steps.test.ts +486 -0
  40. package/src/platform/realtime/WorldStores.ts +153 -0
  41. package/src/platform/realtime/actorCodec.test.ts +62 -0
  42. package/src/platform/realtime/actorCodec.ts +142 -0
  43. package/src/platform/realtime/space.test.ts +33 -0
  44. package/src/platform/realtime/space.ts +63 -0
  45. package/src/platform/social/ChatService.ts +92 -0
  46. package/src/platform/studio/ClientModLifecycle.test.ts +56 -0
  47. package/src/platform/studio/ClientModLifecycle.ts +102 -0
  48. package/src/platform/studio/ConstructPlayerHostAdapter.test.ts +159 -0
  49. package/src/platform/studio/ConstructPlayerHostAdapter.ts +392 -0
  50. package/src/platform/studio/GridService.test.ts +131 -0
  51. package/src/platform/studio/GridService.ts +295 -0
  52. package/src/platform/studio/HumanInputMonitor.test.ts +51 -0
  53. package/src/platform/studio/HumanInputMonitor.ts +48 -0
  54. package/src/platform/studio/StudioService.ts +686 -0
  55. package/src/platform/studio/agentLocomotion.test.ts +31 -0
  56. package/src/platform/studio/agentLocomotion.ts +97 -0
  57. package/src/platform/studio/clientModHost.test.ts +199 -0
  58. package/src/platform/studio/clientModHost.ts +369 -0
  59. package/src/platform/studio/instanceSchema.test.ts +280 -0
  60. package/src/platform/studio/instanceSchema.ts +746 -0
  61. package/src/platform/studio/instanceStore.test.ts +97 -0
  62. package/src/platform/studio/instanceStore.ts +120 -0
  63. package/src/platform/studio/modOverlay.test.ts +56 -0
  64. package/src/platform/studio/modOverlay.ts +51 -0
  65. package/src/platform/studio/permissions.test.ts +51 -0
  66. package/src/platform/studio/permissions.ts +134 -0
  67. package/src/platform/studio/pointerClicks.test.ts +23 -0
  68. package/src/platform/studio/pointerClicks.ts +132 -0
  69. package/src/platform/util/Emitter.ts +32 -0
@@ -0,0 +1,280 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ CatalogAssembler,
5
+ MAX_MESHES,
6
+ MAX_NODES,
7
+ MAX_OVERLAY_OBJECTS,
8
+ MAX_VERTS,
9
+ applyPoses,
10
+ composeScene,
11
+ decodePosePacket,
12
+ encodeCatalogChunks,
13
+ encodePosePacket,
14
+ parseOverlayPayload,
15
+ parseSceneCatalog,
16
+ yawPitchQuat,
17
+ } from './instanceSchema';
18
+
19
+ const grid = { low: { x: 3n, y: 0n, z: 3n }, high: { x: 3n, y: 0n, z: 3n } };
20
+
21
+ describe('parseOverlayPayload', () => {
22
+ it('reads an objects list and clamps to the grid AABB', () => {
23
+ const rows = parseOverlayPayload(
24
+ {
25
+ objects: [
26
+ { id: 'cue', shape: 'sphere', x: 56, y: 0.5, z: 56, r: 0.08, color: '#f8fafc' },
27
+ { id: 'far', x: 0, y: 0, z: 0 },
28
+ ],
29
+ },
30
+ grid,
31
+ );
32
+ expect(rows).toHaveLength(2);
33
+ expect(rows[0]).toMatchObject({ id: 'cue', shape: 'sphere', kind: 'sphere', x: 56, z: 56 });
34
+ expect(rows[0]?.sx).toBeCloseTo(0.08);
35
+ expect(rows[0]?.qw).toBeCloseTo(1);
36
+ expect(rows[1]?.x).toBe(48);
37
+ expect(rows[1]?.z).toBe(48);
38
+ });
39
+
40
+ it('accepts parent plus quaternion', () => {
41
+ const rows = parseOverlayPayload({
42
+ nodes: [
43
+ { id: 'handle', x: 1, y: 1, z: 1, qx: 0, qy: 0.7071, qz: 0, qw: 0.7071 },
44
+ { id: 'shaft', parent: 'handle', x: 0, y: 0, z: 0.5, kind: 'cylinder' },
45
+ ],
46
+ });
47
+ expect(rows[1]).toMatchObject({ id: 'shaft', parent: 'handle', kind: 'cylinder' });
48
+ expect(rows[0]?.qy).toBeCloseTo(0.7071, 3);
49
+ });
50
+
51
+ it('caps the list and ignores duplicates', () => {
52
+ const objects = Array.from({ length: MAX_OVERLAY_OBJECTS + 5 }, (_, i) => ({
53
+ id: i < 2 ? 'same' : `b${i}`,
54
+ x: 50,
55
+ z: 50,
56
+ }));
57
+ expect(parseOverlayPayload({ objects })).toHaveLength(MAX_OVERLAY_OBJECTS);
58
+ });
59
+
60
+ it('never treats a string as HTML', () => {
61
+ expect(parseOverlayPayload('<div id="x">nope</div>')).toEqual([]);
62
+ expect(parseOverlayPayload({ objects: [{ x: 1, z: 1, color: '<script>' }] })[0]?.color).toBe(
63
+ 0xf8fafc,
64
+ );
65
+ });
66
+ });
67
+
68
+ describe('parseSceneCatalog', () => {
69
+ it('keeps primitives and a capped procedural mesh', () => {
70
+ const positions = [0, 0, 0, 1, 0, 0, 0, 1, 0];
71
+ const catalog = parseSceneCatalog({
72
+ v: 1,
73
+ revision: 4,
74
+ meshes: [{ id: 'tri', kind: 'mesh', positions, indices: [0, 1, 2], color: 0xff0000 }],
75
+ nodes: [{ id: 'cue', mesh: 'tri', x: 1, y: 2, z: 3 }],
76
+ });
77
+ expect(catalog?.revision).toBe(4);
78
+ expect(catalog?.meshes[0]?.positions).toHaveLength(9);
79
+ expect(catalog?.nodes[0]?.mesh).toBe('tri');
80
+ });
81
+
82
+ it('drops over-cap vertex lists and parent-less garbage', () => {
83
+ const positions = Array.from({ length: (MAX_VERTS + 1) * 3 }, () => 1);
84
+ expect(
85
+ parseSceneCatalog({
86
+ v: 1,
87
+ meshes: [{ id: 'big', kind: 'mesh', positions, indices: [0, 1, 2] }],
88
+ nodes: [{ id: 'ok', x: 0, y: 0, z: 0 }],
89
+ })?.meshes,
90
+ ).toEqual([]);
91
+ });
92
+
93
+ it('caps meshes at MAX_MESHES', () => {
94
+ const meshes = Array.from({ length: MAX_MESHES + 3 }, (_, i) => ({
95
+ id: `m${i}`,
96
+ kind: 'box',
97
+ }));
98
+ expect(parseSceneCatalog({ v: 1, meshes, nodes: [] })?.meshes).toHaveLength(MAX_MESHES);
99
+ });
100
+ });
101
+
102
+ describe('composeScene', () => {
103
+ it('parents a child through a quaternion and clamps to the grid', () => {
104
+ const [qx, qy, qz, qw] = yawPitchQuat(Math.PI / 2, 0);
105
+ const composed = composeScene(
106
+ [
107
+ {
108
+ id: 'root',
109
+ kind: 'box',
110
+ x: 50,
111
+ y: 1,
112
+ z: 50,
113
+ qx,
114
+ qy,
115
+ qz,
116
+ qw,
117
+ sx: 1,
118
+ sy: 1,
119
+ sz: 1,
120
+ color: 1,
121
+ visible: true,
122
+ },
123
+ {
124
+ id: 'child',
125
+ parent: 'root',
126
+ kind: 'box',
127
+ x: 1,
128
+ y: 0,
129
+ z: 0,
130
+ qx: 0,
131
+ qy: 0,
132
+ qz: 0,
133
+ qw: 1,
134
+ sx: 1,
135
+ sy: 1,
136
+ sz: 1,
137
+ color: 2,
138
+ visible: true,
139
+ },
140
+ ],
141
+ [],
142
+ [],
143
+ grid,
144
+ );
145
+ const child = composed.find((n) => n.id === 'child');
146
+ // +90° yaw (Y): local +X maps to world −Z in the right-handed convention.
147
+ expect(child?.z).toBeCloseTo(49, 1);
148
+ expect(child?.x).toBeCloseTo(50, 1);
149
+ });
150
+
151
+ it('drops a parent cycle', () => {
152
+ const composed = composeScene(
153
+ [
154
+ {
155
+ id: 'a',
156
+ parent: 'b',
157
+ kind: 'box',
158
+ x: 0,
159
+ y: 0,
160
+ z: 0,
161
+ qx: 0,
162
+ qy: 0,
163
+ qz: 0,
164
+ qw: 1,
165
+ sx: 1,
166
+ sy: 1,
167
+ sz: 1,
168
+ color: 1,
169
+ visible: true,
170
+ },
171
+ {
172
+ id: 'b',
173
+ parent: 'a',
174
+ kind: 'box',
175
+ x: 0,
176
+ y: 0,
177
+ z: 0,
178
+ qx: 0,
179
+ qy: 0,
180
+ qz: 0,
181
+ qw: 1,
182
+ sx: 1,
183
+ sy: 1,
184
+ sz: 1,
185
+ color: 1,
186
+ visible: true,
187
+ },
188
+ ],
189
+ [],
190
+ [],
191
+ );
192
+ expect(composed).toEqual([]);
193
+ });
194
+
195
+ it('binds a node to an actor pose', () => {
196
+ const composed = composeScene(
197
+ [
198
+ {
199
+ id: 'cue',
200
+ bindActor: 'player-1',
201
+ kind: 'box',
202
+ x: 0,
203
+ y: 0,
204
+ z: 1,
205
+ qx: 0,
206
+ qy: 0,
207
+ qz: 0,
208
+ qw: 1,
209
+ sx: 1,
210
+ sy: 1,
211
+ sz: 1,
212
+ color: 1,
213
+ visible: true,
214
+ },
215
+ ],
216
+ [],
217
+ [{ uuid: 'player-1', x: 10, y: 0, z: 10, yaw: 0, pitch: 0 }],
218
+ );
219
+ expect(composed[0]?.z).toBeCloseTo(11);
220
+ expect(composed[0]?.x).toBeCloseTo(10);
221
+ });
222
+ });
223
+
224
+ describe('pose and catalog packets', () => {
225
+ it('round-trips packed poses onto named nodes', () => {
226
+ const nodes = [
227
+ {
228
+ id: 'a',
229
+ kind: 'box' as const,
230
+ x: 1.5,
231
+ y: 2,
232
+ z: 3,
233
+ qx: 0,
234
+ qy: 0.7071,
235
+ qz: 0,
236
+ qw: 0.7071,
237
+ sx: 1,
238
+ sy: 2,
239
+ sz: 1,
240
+ color: 0,
241
+ visible: true,
242
+ },
243
+ ];
244
+ const packet = encodePosePacket(9, nodes);
245
+ const decoded = decodePosePacket(packet, ['a']);
246
+ expect(decoded?.catalogRev).toBe(9);
247
+ const pose = decoded?.poses.get('a');
248
+ expect(pose?.x).toBeCloseTo(1.5);
249
+ expect(pose?.qy).toBeCloseTo(0.7071, 2);
250
+ const applied = applyPoses(
251
+ [{ ...nodes[0]!, x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 }],
252
+ decoded!.poses,
253
+ );
254
+ expect(applied[0]?.x).toBeCloseTo(1.5);
255
+ });
256
+
257
+ it('assembles chunked catalog JSON', () => {
258
+ const json = new TextEncoder().encode(
259
+ JSON.stringify({
260
+ v: 1,
261
+ revision: 2,
262
+ meshes: [],
263
+ nodes: [{ id: 'box', kind: 'box', x: 1, y: 1, z: 1 }],
264
+ }),
265
+ );
266
+ const chunks = encodeCatalogChunks(2, json);
267
+ expect(chunks.length).toBeGreaterThanOrEqual(1);
268
+ const assembler = new CatalogAssembler();
269
+ let joined: Uint8Array | null = null;
270
+ for (const chunk of chunks) joined = assembler.push(chunk);
271
+ expect(joined).not.toBeNull();
272
+ const catalog = parseSceneCatalog(JSON.parse(new TextDecoder().decode(joined!)));
273
+ expect(catalog?.nodes[0]?.id).toBe('box');
274
+ });
275
+
276
+ it('exports MAX_NODES as the overlay cap', () => {
277
+ expect(MAX_NODES).toBe(64);
278
+ expect(MAX_MESHES).toBe(16);
279
+ });
280
+ });