modd-network 1.0.2 → 1.0.3

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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Binary State Codec
3
+ *
4
+ * Efficient encoding for player/entity state updates.
5
+ *
6
+ * Player State (variable, ~15-25 bytes vs ~150 JSON):
7
+ * [0x10][id_len:1][id:var][flags:1][fields based on flags]
8
+ * flags: bit0=pos, bit1=vel, bit2=rotY, bit3=hp, bit4=dead
9
+ *
10
+ * Entity State (variable, ~20-35 bytes vs ~200 JSON):
11
+ * [0x11][id_len:1][id:var][flags:1][fields based on flags]
12
+ * flags: bit0=pos, bit1=vel, bit2=quat, bit3=angvel
13
+ *
14
+ * Position Correction (compact, for periodic sync):
15
+ * [0x12][id_len:1][id:var][x:4][y:4][z:4][rotY:2]
16
+ */
17
+ export declare const StateType: {
18
+ readonly PLAYER_STATE: 16;
19
+ readonly ENTITY_STATE: 17;
20
+ readonly POSITION_CORRECTION: 18;
21
+ readonly INPUT_STATE: 19;
22
+ };
23
+ export interface PlayerStateData {
24
+ id: string;
25
+ x?: number;
26
+ y?: number;
27
+ z?: number;
28
+ vx?: number;
29
+ vy?: number;
30
+ vz?: number;
31
+ rotY?: number;
32
+ hp?: number;
33
+ dead?: boolean;
34
+ }
35
+ export declare function encodePlayerState(data: PlayerStateData): Uint8Array;
36
+ export declare function decodePlayerState(buf: Uint8Array, offset?: number): {
37
+ data: PlayerStateData;
38
+ bytesRead: number;
39
+ } | null;
40
+ export interface EntityStateData {
41
+ id: string;
42
+ x?: number;
43
+ y?: number;
44
+ z?: number;
45
+ vx?: number;
46
+ vy?: number;
47
+ vz?: number;
48
+ qx?: number;
49
+ qy?: number;
50
+ qz?: number;
51
+ qw?: number;
52
+ avx?: number;
53
+ avy?: number;
54
+ avz?: number;
55
+ }
56
+ export declare function encodeEntityState(data: EntityStateData): Uint8Array;
57
+ export declare function decodeEntityState(buf: Uint8Array, offset?: number): {
58
+ data: EntityStateData;
59
+ bytesRead: number;
60
+ } | null;
61
+ export interface InputStateData {
62
+ id: string;
63
+ keys: number;
64
+ rotY: number;
65
+ }
66
+ export declare const KEY_W = 1;
67
+ export declare const KEY_A = 2;
68
+ export declare const KEY_S = 4;
69
+ export declare const KEY_D = 8;
70
+ export declare const KEY_SPACE = 16;
71
+ export declare const KEY_SHIFT = 32;
72
+ export declare function encodeInputState(data: InputStateData): Uint8Array;
73
+ export declare function decodeInputState(buf: Uint8Array, offset?: number): {
74
+ data: InputStateData;
75
+ bytesRead: number;
76
+ } | null;
77
+ export declare function decodeState(buf: Uint8Array, offset?: number): {
78
+ type: string;
79
+ data: any;
80
+ bytesRead: number;
81
+ } | null;
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ /**
3
+ * Binary State Codec
4
+ *
5
+ * Efficient encoding for player/entity state updates.
6
+ *
7
+ * Player State (variable, ~15-25 bytes vs ~150 JSON):
8
+ * [0x10][id_len:1][id:var][flags:1][fields based on flags]
9
+ * flags: bit0=pos, bit1=vel, bit2=rotY, bit3=hp, bit4=dead
10
+ *
11
+ * Entity State (variable, ~20-35 bytes vs ~200 JSON):
12
+ * [0x11][id_len:1][id:var][flags:1][fields based on flags]
13
+ * flags: bit0=pos, bit1=vel, bit2=quat, bit3=angvel
14
+ *
15
+ * Position Correction (compact, for periodic sync):
16
+ * [0x12][id_len:1][id:var][x:4][y:4][z:4][rotY:2]
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.KEY_SHIFT = exports.KEY_SPACE = exports.KEY_D = exports.KEY_S = exports.KEY_A = exports.KEY_W = exports.StateType = void 0;
20
+ exports.encodePlayerState = encodePlayerState;
21
+ exports.decodePlayerState = decodePlayerState;
22
+ exports.encodeEntityState = encodeEntityState;
23
+ exports.decodeEntityState = decodeEntityState;
24
+ exports.encodeInputState = encodeInputState;
25
+ exports.decodeInputState = decodeInputState;
26
+ exports.decodeState = decodeState;
27
+ exports.StateType = {
28
+ PLAYER_STATE: 0x10,
29
+ ENTITY_STATE: 0x11,
30
+ POSITION_CORRECTION: 0x12,
31
+ INPUT_STATE: 0x13, // Full input state snapshot
32
+ };
33
+ // Flags for player state
34
+ const PLAYER_HAS_POS = 0x01;
35
+ const PLAYER_HAS_VEL = 0x02;
36
+ const PLAYER_HAS_ROTY = 0x04;
37
+ const PLAYER_HAS_HP = 0x08;
38
+ const PLAYER_HAS_DEAD = 0x10;
39
+ // Flags for entity state
40
+ const ENTITY_HAS_POS = 0x01;
41
+ const ENTITY_HAS_VEL = 0x02;
42
+ const ENTITY_HAS_QUAT = 0x04;
43
+ const ENTITY_HAS_ANGVEL = 0x08;
44
+ // ============================================
45
+ // Float encoding helpers
46
+ // ============================================
47
+ function writeFloat32(buf, offset, value) {
48
+ const view = new DataView(buf.buffer, buf.byteOffset);
49
+ view.setFloat32(offset, value, true);
50
+ }
51
+ function readFloat32(buf, offset) {
52
+ const view = new DataView(buf.buffer, buf.byteOffset);
53
+ return view.getFloat32(offset, true);
54
+ }
55
+ function writeInt16(buf, offset, value) {
56
+ const view = new DataView(buf.buffer, buf.byteOffset);
57
+ view.setInt16(offset, value, true);
58
+ }
59
+ function readInt16(buf, offset) {
60
+ const view = new DataView(buf.buffer, buf.byteOffset);
61
+ return view.getInt16(offset, true);
62
+ }
63
+ function encodePlayerState(data) {
64
+ const idBytes = new TextEncoder().encode(data.id);
65
+ // Calculate flags and size
66
+ let flags = 0;
67
+ let dataSize = 0;
68
+ const hasPos = data.x !== undefined || data.y !== undefined || data.z !== undefined;
69
+ const hasVel = data.vx !== undefined || data.vy !== undefined || data.vz !== undefined;
70
+ const hasRotY = data.rotY !== undefined;
71
+ const hasHp = data.hp !== undefined;
72
+ const hasDead = data.dead !== undefined;
73
+ if (hasPos) {
74
+ flags |= PLAYER_HAS_POS;
75
+ dataSize += 12;
76
+ } // 3 floats
77
+ if (hasVel) {
78
+ flags |= PLAYER_HAS_VEL;
79
+ dataSize += 12;
80
+ } // 3 floats
81
+ if (hasRotY) {
82
+ flags |= PLAYER_HAS_ROTY;
83
+ dataSize += 2;
84
+ } // int16
85
+ if (hasHp) {
86
+ flags |= PLAYER_HAS_HP;
87
+ dataSize += 1;
88
+ } // uint8
89
+ if (hasDead) {
90
+ flags |= PLAYER_HAS_DEAD;
91
+ dataSize += 1;
92
+ } // uint8
93
+ const buf = new Uint8Array(1 + 1 + idBytes.length + 1 + dataSize);
94
+ let offset = 0;
95
+ buf[offset++] = exports.StateType.PLAYER_STATE;
96
+ buf[offset++] = idBytes.length;
97
+ buf.set(idBytes, offset);
98
+ offset += idBytes.length;
99
+ buf[offset++] = flags;
100
+ if (hasPos) {
101
+ writeFloat32(buf, offset, data.x ?? 0);
102
+ offset += 4;
103
+ writeFloat32(buf, offset, data.y ?? 0);
104
+ offset += 4;
105
+ writeFloat32(buf, offset, data.z ?? 0);
106
+ offset += 4;
107
+ }
108
+ if (hasVel) {
109
+ writeFloat32(buf, offset, data.vx ?? 0);
110
+ offset += 4;
111
+ writeFloat32(buf, offset, data.vy ?? 0);
112
+ offset += 4;
113
+ writeFloat32(buf, offset, data.vz ?? 0);
114
+ offset += 4;
115
+ }
116
+ if (hasRotY) {
117
+ writeInt16(buf, offset, Math.round((data.rotY ?? 0) * 10000));
118
+ offset += 2;
119
+ }
120
+ if (hasHp) {
121
+ buf[offset++] = Math.min(255, Math.max(0, data.hp ?? 100));
122
+ }
123
+ if (hasDead) {
124
+ buf[offset++] = data.dead ? 1 : 0;
125
+ }
126
+ return buf;
127
+ }
128
+ function decodePlayerState(buf, offset = 0) {
129
+ if (buf[offset] !== exports.StateType.PLAYER_STATE)
130
+ return null;
131
+ let pos = offset + 1;
132
+ const idLen = buf[pos++];
133
+ const id = new TextDecoder().decode(buf.slice(pos, pos + idLen));
134
+ pos += idLen;
135
+ const flags = buf[pos++];
136
+ const data = { id };
137
+ if (flags & PLAYER_HAS_POS) {
138
+ data.x = readFloat32(buf, pos);
139
+ pos += 4;
140
+ data.y = readFloat32(buf, pos);
141
+ pos += 4;
142
+ data.z = readFloat32(buf, pos);
143
+ pos += 4;
144
+ }
145
+ if (flags & PLAYER_HAS_VEL) {
146
+ data.vx = readFloat32(buf, pos);
147
+ pos += 4;
148
+ data.vy = readFloat32(buf, pos);
149
+ pos += 4;
150
+ data.vz = readFloat32(buf, pos);
151
+ pos += 4;
152
+ }
153
+ if (flags & PLAYER_HAS_ROTY) {
154
+ data.rotY = readInt16(buf, pos) / 10000;
155
+ pos += 2;
156
+ }
157
+ if (flags & PLAYER_HAS_HP) {
158
+ data.hp = buf[pos++];
159
+ }
160
+ if (flags & PLAYER_HAS_DEAD) {
161
+ data.dead = buf[pos++] === 1;
162
+ }
163
+ return { data, bytesRead: pos - offset };
164
+ }
165
+ function encodeEntityState(data) {
166
+ const idBytes = new TextEncoder().encode(data.id);
167
+ let flags = 0;
168
+ let dataSize = 0;
169
+ const hasPos = data.x !== undefined || data.y !== undefined || data.z !== undefined;
170
+ const hasVel = data.vx !== undefined || data.vy !== undefined || data.vz !== undefined;
171
+ const hasQuat = data.qx !== undefined || data.qy !== undefined || data.qz !== undefined || data.qw !== undefined;
172
+ const hasAngvel = data.avx !== undefined || data.avy !== undefined || data.avz !== undefined;
173
+ if (hasPos) {
174
+ flags |= ENTITY_HAS_POS;
175
+ dataSize += 12;
176
+ }
177
+ if (hasVel) {
178
+ flags |= ENTITY_HAS_VEL;
179
+ dataSize += 12;
180
+ }
181
+ if (hasQuat) {
182
+ flags |= ENTITY_HAS_QUAT;
183
+ dataSize += 16;
184
+ } // 4 floats
185
+ if (hasAngvel) {
186
+ flags |= ENTITY_HAS_ANGVEL;
187
+ dataSize += 12;
188
+ }
189
+ const buf = new Uint8Array(1 + 1 + idBytes.length + 1 + dataSize);
190
+ let offset = 0;
191
+ buf[offset++] = exports.StateType.ENTITY_STATE;
192
+ buf[offset++] = idBytes.length;
193
+ buf.set(idBytes, offset);
194
+ offset += idBytes.length;
195
+ buf[offset++] = flags;
196
+ if (hasPos) {
197
+ writeFloat32(buf, offset, data.x ?? 0);
198
+ offset += 4;
199
+ writeFloat32(buf, offset, data.y ?? 0);
200
+ offset += 4;
201
+ writeFloat32(buf, offset, data.z ?? 0);
202
+ offset += 4;
203
+ }
204
+ if (hasVel) {
205
+ writeFloat32(buf, offset, data.vx ?? 0);
206
+ offset += 4;
207
+ writeFloat32(buf, offset, data.vy ?? 0);
208
+ offset += 4;
209
+ writeFloat32(buf, offset, data.vz ?? 0);
210
+ offset += 4;
211
+ }
212
+ if (hasQuat) {
213
+ writeFloat32(buf, offset, data.qx ?? 0);
214
+ offset += 4;
215
+ writeFloat32(buf, offset, data.qy ?? 0);
216
+ offset += 4;
217
+ writeFloat32(buf, offset, data.qz ?? 0);
218
+ offset += 4;
219
+ writeFloat32(buf, offset, data.qw ?? 1);
220
+ offset += 4;
221
+ }
222
+ if (hasAngvel) {
223
+ writeFloat32(buf, offset, data.avx ?? 0);
224
+ offset += 4;
225
+ writeFloat32(buf, offset, data.avy ?? 0);
226
+ offset += 4;
227
+ writeFloat32(buf, offset, data.avz ?? 0);
228
+ offset += 4;
229
+ }
230
+ return buf;
231
+ }
232
+ function decodeEntityState(buf, offset = 0) {
233
+ if (buf[offset] !== exports.StateType.ENTITY_STATE)
234
+ return null;
235
+ let pos = offset + 1;
236
+ const idLen = buf[pos++];
237
+ const id = new TextDecoder().decode(buf.slice(pos, pos + idLen));
238
+ pos += idLen;
239
+ const flags = buf[pos++];
240
+ const data = { id };
241
+ if (flags & ENTITY_HAS_POS) {
242
+ data.x = readFloat32(buf, pos);
243
+ pos += 4;
244
+ data.y = readFloat32(buf, pos);
245
+ pos += 4;
246
+ data.z = readFloat32(buf, pos);
247
+ pos += 4;
248
+ }
249
+ if (flags & ENTITY_HAS_VEL) {
250
+ data.vx = readFloat32(buf, pos);
251
+ pos += 4;
252
+ data.vy = readFloat32(buf, pos);
253
+ pos += 4;
254
+ data.vz = readFloat32(buf, pos);
255
+ pos += 4;
256
+ }
257
+ if (flags & ENTITY_HAS_QUAT) {
258
+ data.qx = readFloat32(buf, pos);
259
+ pos += 4;
260
+ data.qy = readFloat32(buf, pos);
261
+ pos += 4;
262
+ data.qz = readFloat32(buf, pos);
263
+ pos += 4;
264
+ data.qw = readFloat32(buf, pos);
265
+ pos += 4;
266
+ }
267
+ if (flags & ENTITY_HAS_ANGVEL) {
268
+ data.avx = readFloat32(buf, pos);
269
+ pos += 4;
270
+ data.avy = readFloat32(buf, pos);
271
+ pos += 4;
272
+ data.avz = readFloat32(buf, pos);
273
+ pos += 4;
274
+ }
275
+ return { data, bytesRead: pos - offset };
276
+ }
277
+ // Key bitmask
278
+ exports.KEY_W = 0x01;
279
+ exports.KEY_A = 0x02;
280
+ exports.KEY_S = 0x04;
281
+ exports.KEY_D = 0x08;
282
+ exports.KEY_SPACE = 0x10;
283
+ exports.KEY_SHIFT = 0x20;
284
+ function encodeInputState(data) {
285
+ const idBytes = new TextEncoder().encode(data.id);
286
+ const buf = new Uint8Array(1 + 1 + idBytes.length + 1 + 2);
287
+ let offset = 0;
288
+ buf[offset++] = exports.StateType.INPUT_STATE;
289
+ buf[offset++] = idBytes.length;
290
+ buf.set(idBytes, offset);
291
+ offset += idBytes.length;
292
+ buf[offset++] = data.keys;
293
+ writeInt16(buf, offset, Math.round(data.rotY * 10000));
294
+ return buf;
295
+ }
296
+ function decodeInputState(buf, offset = 0) {
297
+ if (buf[offset] !== exports.StateType.INPUT_STATE)
298
+ return null;
299
+ let pos = offset + 1;
300
+ const idLen = buf[pos++];
301
+ const id = new TextDecoder().decode(buf.slice(pos, pos + idLen));
302
+ pos += idLen;
303
+ const keys = buf[pos++];
304
+ const rotY = readInt16(buf, pos) / 10000;
305
+ pos += 2;
306
+ return { data: { id, keys, rotY }, bytesRead: pos - offset };
307
+ }
308
+ // ============================================
309
+ // Generic decode
310
+ // ============================================
311
+ function decodeState(buf, offset = 0) {
312
+ if (buf.length <= offset)
313
+ return null;
314
+ const type = buf[offset];
315
+ switch (type) {
316
+ case exports.StateType.PLAYER_STATE: {
317
+ const result = decodePlayerState(buf, offset);
318
+ if (!result)
319
+ return null;
320
+ return { type: 'playerState', data: result.data, bytesRead: result.bytesRead };
321
+ }
322
+ case exports.StateType.ENTITY_STATE: {
323
+ const result = decodeEntityState(buf, offset);
324
+ if (!result)
325
+ return null;
326
+ return { type: 'entityState', data: result.data, bytesRead: result.bytesRead };
327
+ }
328
+ case exports.StateType.INPUT_STATE: {
329
+ const result = decodeInputState(buf, offset);
330
+ if (!result)
331
+ return null;
332
+ return { type: 'inputState', data: result.data, bytesRead: result.bytesRead };
333
+ }
334
+ default:
335
+ return null;
336
+ }
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modd-network",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "SDK for connecting to mesh network for multiplayer applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,8 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "build:browser": "node build-browser.js",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
19
21
  "prepublishOnly": "npm run build"
20
22
  },
21
23
  "keywords": [
@@ -33,6 +35,7 @@
33
35
  "@types/node": "^20.10.6",
34
36
  "@types/ws": "^8.5.10",
35
37
  "esbuild": "^0.27.2",
36
- "typescript": "^5.3.3"
38
+ "typescript": "^5.3.3",
39
+ "vitest": "^2.1.0"
37
40
  }
38
41
  }