@crowdedkingdoms/construct 0.3.5-dev.1 → 0.3.7-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/construct",
3
- "version": "0.3.5-dev.1",
3
+ "version": "0.3.7-dev.1",
4
4
  "description": "The Construct framework: the Crowded Kingdoms game platform layer (hosted sign-in, World Stores, voice/webcam/chat, Crowdy Studio dock, app and grid mods, grid programs) and engine loop, shared by the-construct starter and the games built on it. Ships TypeScript source for Vite.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,8 +45,8 @@
45
45
  "./grid/*": "./src/grid/*.ts"
46
46
  },
47
47
  "peerDependencies": {
48
- "@crowdedkingdoms/crowdyjs": ">=18.0.4-dev.0 <19 || >=18.1.0-dev.0 <19",
49
- "@crowdedkingdoms/crowdy-dsh": ">=0.4.0-dev.0 <1 || >=0.4.1-dev.0 <1 || >=0.4.2-dev.0 <1 || >=0.4.3-dev.0 <1 || >=0.4.4-dev.0 <1"
48
+ "@crowdedkingdoms/crowdyjs": ">=18.0.4-dev.0 <19 || >=18.1.0-dev.0 <19 || >=18.2.0-dev.0 <19",
49
+ "@crowdedkingdoms/crowdy-dsh": ">=0.4.0-dev.0 <1 || >=0.4.1-dev.0 <1 || >=0.4.2-dev.0 <1 || >=0.4.3-dev.0 <1 || >=0.4.4-dev.0 <1 || >=0.4.5-dev.0 <1"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@crowdedkingdoms/crowdy-dsh": {
@@ -9,7 +9,8 @@
9
9
  * would get their own lane.
10
10
  * - `chunks` — the voxel cache Paint draws in 2D and the holodeck draws as
11
11
  * cubes. CLIENT `voxel_set` writes here so other players see
12
- * the same blocks.
12
+ * the same blocks. Every loaded chunk is hydrated, so blocks a
13
+ * SERVER mod placed come back after a reload.
13
14
  * - `errors` — server-reported send failures attributed to what we sent.
14
15
  * - `events` — SERVER `emit_spatial("server_event")` catalog/pose packets
15
16
  * for the replicated instance layer (construct.scene.v1).
@@ -32,6 +33,7 @@ import { ACTOR_SYNC_INTERVAL_MS, REPLICATION_DISTANCE, STALE_ACTOR_TIMEOUT_MS }
32
33
  import { envScopedKey } from '../envScope';
33
34
  import { NetworkManager } from '../network/NetworkManager';
34
35
  import { NEUTRAL_POSE, poseCodec, type Pose } from './actorCodec';
36
+ import { chunkStoreConfig } from './chunkStoreConfig';
35
37
  import { InstanceStore } from '../studio/instanceStore';
36
38
 
37
39
  /** What survives between sessions. Deliberately small; add fields freely. */
@@ -62,13 +64,7 @@ function buildConfig() {
62
64
  players: () => true,
63
65
  },
64
66
  },
65
- chunks: {
66
- distance: REPLICATION_DISTANCE,
67
- // The Construct has no terrain to generate: a chunk the server has never
68
- // stored is simply empty. Seed it locally (no write-back) so the paint
69
- // program and CLIENT mods always see a dense grid.
70
- onMissing: () => ({ voxels: new Uint8Array(4096), writeBack: false }),
71
- },
67
+ chunks: chunkStoreConfig(),
72
68
  errors: true as const,
73
69
  events: true as const,
74
70
  host: {
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { ChunkStore, type ChunkStoreConfig } from '@crowdedkingdoms/crowdyjs/stores';
3
+
4
+ import { chunkStoreConfig } from './chunkStoreConfig';
5
+
6
+ const STORED = { x: 3, y: 0, z: 3 };
7
+ const NEVER_STORED = { x: 4, y: 0, z: 3 };
8
+ const at = (x: number, y: number, z: number) => x + y * 16 + z * 256;
9
+
10
+ /**
11
+ * What ck-api v2.33.0's `getChunk` returns for a chunk whose dense grid holds one block (5) and
12
+ * whose edit log holds the pool cue mod's felt and white ball (`world.set_voxels`, no state), a
13
+ * painted cell (one byte of state), and that block mined (type 0).
14
+ */
15
+ const RECORDED_EDITS = [
16
+ { voxelCoord: { x: 6, y: 1, z: 7 }, voxelType: 1, state: null },
17
+ { voxelCoord: { x: 7, y: 2, z: 8 }, voxelType: 3, state: null },
18
+ { voxelCoord: { x: 8, y: 0, z: 8 }, voxelType: 4, state: 'AA==' },
19
+ { voxelCoord: { x: 2, y: 0, z: 2 }, voxelType: 0, state: null },
20
+ ];
21
+
22
+ function base64(bytes: Uint8Array): string {
23
+ return Buffer.from(bytes).toString('base64');
24
+ }
25
+
26
+ /** The real SDK store over a fake game API that stores only `STORED`. */
27
+ function storeWith(config: ChunkStoreConfig) {
28
+ const grid = new Uint8Array(4096);
29
+ grid[at(2, 0, 2)] = 5;
30
+ const coordinates = { x: String(STORED.x), y: String(STORED.y), z: String(STORED.z) };
31
+ const client = {
32
+ chunks: {
33
+ byDistance: vi.fn(async () => ({
34
+ chunks: [{ coordinates, voxels: base64(grid), chunkState: null }],
35
+ })),
36
+ get: vi.fn(async () => ({
37
+ voxels: base64(grid),
38
+ chunkState: null,
39
+ voxelStates: RECORDED_EDITS,
40
+ })),
41
+ update: vi.fn(async () => ({})),
42
+ },
43
+ udp: { sendVoxelUpdate: vi.fn(async () => true) },
44
+ };
45
+ const ctx = {
46
+ appId: '1',
47
+ client,
48
+ ticker: { every: () => () => undefined },
49
+ on: () => () => undefined,
50
+ trackSend: () => undefined,
51
+ setSendTracker: () => undefined,
52
+ onDispose: () => undefined,
53
+ };
54
+ const store = new ChunkStore(ctx as never, { ...config, writeBackIntervalMs: false });
55
+ return { store, client };
56
+ }
57
+
58
+ describe('chunkStoreConfig', () => {
59
+ it("brings back a SERVER mod's blocks, a painted cell and a mined block after a reload", async () => {
60
+ const { store, client } = storeWith(chunkStoreConfig());
61
+ await store.ensureAround(STORED, 1);
62
+
63
+ expect(client.chunks.get).toHaveBeenCalledTimes(1);
64
+ expect(client.chunks.get).toHaveBeenCalledWith({
65
+ appId: '1',
66
+ coordinates: { x: '3', y: '0', z: '3' },
67
+ });
68
+ expect(store.get(STORED)?.hydrated).toBe(true);
69
+ expect(store.voxelTypeAt(STORED, 6, 1, 7)).toBe(1);
70
+ expect(store.voxelTypeAt(STORED, 7, 2, 8)).toBe(3);
71
+ expect(store.voxelTypeAt(STORED, 8, 0, 8)).toBe(4);
72
+ expect(store.voxelStateAt(STORED, 8, 0, 8)).toBe('AA==');
73
+ expect(store.voxelTypeAt(STORED, 2, 0, 2)).toBe(0);
74
+
75
+ // Moving on loads the next cube, which returns this chunk again (CrowdyJS 18.2.0 keeps it).
76
+ await store.ensureAround({ x: 3, y: 0, z: 4 }, 1);
77
+ expect(client.chunks.byDistance).toHaveBeenCalledTimes(2);
78
+ expect(store.voxelTypeAt(STORED, 6, 1, 7)).toBe(1);
79
+ expect(store.voxelTypeAt(STORED, 2, 0, 2)).toBe(0);
80
+ expect(client.chunks.get).toHaveBeenCalledTimes(1);
81
+ });
82
+
83
+ it('shows only the stored grid when the store does not hydrate (the construct before 0.3.6)', async () => {
84
+ const { store, client } = storeWith({ ...chunkStoreConfig(), hydrateVoxelStates: false });
85
+ await store.ensureAround(STORED, 1);
86
+
87
+ expect(client.chunks.get).not.toHaveBeenCalled();
88
+ expect(store.voxelTypeAt(STORED, 6, 1, 7)).toBe(0);
89
+ expect(store.voxelTypeAt(STORED, 2, 0, 2)).toBe(5);
90
+ });
91
+
92
+ it('seeds a chunk the server never stored, empty and without a write-back', async () => {
93
+ const { store } = storeWith(chunkStoreConfig());
94
+ await store.ensureAround(STORED, 1);
95
+
96
+ const seeded = store.get(NEVER_STORED);
97
+ expect(seeded?.loadState).toBe('seeded');
98
+ expect(seeded?.voxels).toEqual(new Uint8Array(4096));
99
+ expect(store.pendingWriteBacks).toBe(0);
100
+ });
101
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The World Stores chunk cache's settings, shared by the session and its test.
3
+ */
4
+ import { REPLICATION_DISTANCE } from '../config';
5
+
6
+ export function chunkStoreConfig() {
7
+ return {
8
+ distance: REPLICATION_DISTANCE,
9
+ // A SERVER mod's or hub's `world.set_voxels`, `updateVoxel` and realtime
10
+ // voxel updates reach a reload only as `getChunk` `voxelStates` entries
11
+ // (ck-api v2.33.0): the bulk load's dense `voxels` hold none of them. The
12
+ // store hydrates every loaded chunk only when asked, or when it has a voxel
13
+ // state codec, which The Construct does not.
14
+ hydrateVoxelStates: true,
15
+ // The Construct has no terrain to generate: a chunk the server has never
16
+ // stored is simply empty. Seed it locally (no write-back) so the paint
17
+ // program and CLIENT mods always see a dense grid.
18
+ onMissing: () => ({ voxels: new Uint8Array(4096), writeBack: false }),
19
+ };
20
+ }