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.
- package/dist/binary-input.d.ts +53 -0
- package/dist/binary-input.js +168 -0
- package/dist/game-input-codec.d.ts +55 -0
- package/dist/game-input-codec.js +263 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/input-codec.d.ts +33 -0
- package/dist/input-codec.js +192 -0
- package/dist/modd-engine.d.ts +77 -56
- package/dist/modd-engine.js +479 -244
- package/dist/modd-engine.test.d.ts +1 -0
- package/dist/modd-engine.test.js +190 -0
- package/dist/modd-network.d.ts +55 -22
- package/dist/modd-network.js +497 -352
- package/dist/state-codec.d.ts +81 -0
- package/dist/state-codec.js +337 -0
- package/package.json +5 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Input Protocol
|
|
3
|
+
*
|
|
4
|
+
* Minimal bandwidth for game inputs. Server knows client from connection.
|
|
5
|
+
*
|
|
6
|
+
* Message format: [type:1][payload:variable]
|
|
7
|
+
*
|
|
8
|
+
* Types:
|
|
9
|
+
* 0x01 KEY - [key:1] where bit7=down, bits0-6=keyIndex
|
|
10
|
+
* 0x02 CAMERA - [yaw:2][pitch:2] as int16 (radians * 10000)
|
|
11
|
+
* 0x03 SHOOT - no payload (uses current camera)
|
|
12
|
+
* 0x04 JOIN - no payload
|
|
13
|
+
* 0x05 LEAVE - no payload
|
|
14
|
+
* 0x06 KEYS - [count:1][keys:count] batch of key changes
|
|
15
|
+
*/
|
|
16
|
+
export declare const InputType: {
|
|
17
|
+
readonly KEY: 1;
|
|
18
|
+
readonly CAMERA: 2;
|
|
19
|
+
readonly SHOOT: 3;
|
|
20
|
+
readonly JOIN: 4;
|
|
21
|
+
readonly LEAVE: 5;
|
|
22
|
+
readonly KEYS: 6;
|
|
23
|
+
};
|
|
24
|
+
export declare const KeyIndex: {
|
|
25
|
+
readonly W: 0;
|
|
26
|
+
readonly A: 1;
|
|
27
|
+
readonly S: 2;
|
|
28
|
+
readonly D: 3;
|
|
29
|
+
readonly SPACE: 4;
|
|
30
|
+
readonly SHIFT: 5;
|
|
31
|
+
readonly CTRL: 6;
|
|
32
|
+
readonly E: 7;
|
|
33
|
+
readonly Q: 8;
|
|
34
|
+
readonly R: 9;
|
|
35
|
+
readonly F: 10;
|
|
36
|
+
};
|
|
37
|
+
export declare function encodeKey(key: string, down: boolean): Uint8Array;
|
|
38
|
+
export declare function encodeKeys(keys: Array<{
|
|
39
|
+
key: string;
|
|
40
|
+
down: boolean;
|
|
41
|
+
}>): Uint8Array;
|
|
42
|
+
export declare function encodeCamera(yaw: number, pitch: number): Uint8Array;
|
|
43
|
+
export declare function encodeShoot(): Uint8Array;
|
|
44
|
+
export declare function encodeJoin(): Uint8Array;
|
|
45
|
+
export declare function encodeLeave(): Uint8Array;
|
|
46
|
+
export interface DecodedInput {
|
|
47
|
+
type: 'key' | 'camera' | 'shoot' | 'join' | 'leave';
|
|
48
|
+
key?: string;
|
|
49
|
+
down?: boolean;
|
|
50
|
+
yaw?: number;
|
|
51
|
+
pitch?: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function decodeInput(buf: Uint8Array): DecodedInput[];
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Binary Input Protocol
|
|
4
|
+
*
|
|
5
|
+
* Minimal bandwidth for game inputs. Server knows client from connection.
|
|
6
|
+
*
|
|
7
|
+
* Message format: [type:1][payload:variable]
|
|
8
|
+
*
|
|
9
|
+
* Types:
|
|
10
|
+
* 0x01 KEY - [key:1] where bit7=down, bits0-6=keyIndex
|
|
11
|
+
* 0x02 CAMERA - [yaw:2][pitch:2] as int16 (radians * 10000)
|
|
12
|
+
* 0x03 SHOOT - no payload (uses current camera)
|
|
13
|
+
* 0x04 JOIN - no payload
|
|
14
|
+
* 0x05 LEAVE - no payload
|
|
15
|
+
* 0x06 KEYS - [count:1][keys:count] batch of key changes
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.KeyIndex = exports.InputType = void 0;
|
|
19
|
+
exports.encodeKey = encodeKey;
|
|
20
|
+
exports.encodeKeys = encodeKeys;
|
|
21
|
+
exports.encodeCamera = encodeCamera;
|
|
22
|
+
exports.encodeShoot = encodeShoot;
|
|
23
|
+
exports.encodeJoin = encodeJoin;
|
|
24
|
+
exports.encodeLeave = encodeLeave;
|
|
25
|
+
exports.decodeInput = decodeInput;
|
|
26
|
+
exports.InputType = {
|
|
27
|
+
KEY: 0x01,
|
|
28
|
+
CAMERA: 0x02,
|
|
29
|
+
SHOOT: 0x03,
|
|
30
|
+
JOIN: 0x04,
|
|
31
|
+
LEAVE: 0x05,
|
|
32
|
+
KEYS: 0x06,
|
|
33
|
+
};
|
|
34
|
+
// Key indices - extendable
|
|
35
|
+
exports.KeyIndex = {
|
|
36
|
+
W: 0,
|
|
37
|
+
A: 1,
|
|
38
|
+
S: 2,
|
|
39
|
+
D: 3,
|
|
40
|
+
SPACE: 4,
|
|
41
|
+
SHIFT: 5,
|
|
42
|
+
CTRL: 6,
|
|
43
|
+
E: 7,
|
|
44
|
+
Q: 8,
|
|
45
|
+
R: 9,
|
|
46
|
+
F: 10,
|
|
47
|
+
// Add more as needed, up to 127
|
|
48
|
+
};
|
|
49
|
+
const KEY_DOWN_BIT = 0x80;
|
|
50
|
+
// Reverse lookup for decoding
|
|
51
|
+
const keyIndexToName = {
|
|
52
|
+
0: 'w', 1: 'a', 2: 's', 3: 'd',
|
|
53
|
+
4: 'space', 5: 'shift', 6: 'ctrl',
|
|
54
|
+
7: 'e', 8: 'q', 9: 'r', 10: 'f'
|
|
55
|
+
};
|
|
56
|
+
const keyNameToIndex = {
|
|
57
|
+
'w': 0, 'a': 1, 's': 2, 'd': 3,
|
|
58
|
+
'space': 4, ' ': 4,
|
|
59
|
+
'shift': 5, 'ctrl': 6, 'control': 6,
|
|
60
|
+
'e': 7, 'q': 8, 'r': 9, 'f': 10
|
|
61
|
+
};
|
|
62
|
+
// ============================================
|
|
63
|
+
// Encoding (Client -> Server)
|
|
64
|
+
// ============================================
|
|
65
|
+
function encodeKey(key, down) {
|
|
66
|
+
const buf = new Uint8Array(2);
|
|
67
|
+
buf[0] = exports.InputType.KEY;
|
|
68
|
+
const keyIdx = keyNameToIndex[key.toLowerCase()] ?? 0;
|
|
69
|
+
buf[1] = keyIdx | (down ? KEY_DOWN_BIT : 0);
|
|
70
|
+
return buf;
|
|
71
|
+
}
|
|
72
|
+
function encodeKeys(keys) {
|
|
73
|
+
if (keys.length === 1) {
|
|
74
|
+
return encodeKey(keys[0].key, keys[0].down);
|
|
75
|
+
}
|
|
76
|
+
const buf = new Uint8Array(2 + keys.length);
|
|
77
|
+
buf[0] = exports.InputType.KEYS;
|
|
78
|
+
buf[1] = keys.length;
|
|
79
|
+
for (let i = 0; i < keys.length; i++) {
|
|
80
|
+
const keyIdx = keyNameToIndex[keys[i].key.toLowerCase()] ?? 0;
|
|
81
|
+
buf[2 + i] = keyIdx | (keys[i].down ? KEY_DOWN_BIT : 0);
|
|
82
|
+
}
|
|
83
|
+
return buf;
|
|
84
|
+
}
|
|
85
|
+
function encodeCamera(yaw, pitch) {
|
|
86
|
+
const buf = new Uint8Array(5);
|
|
87
|
+
buf[0] = exports.InputType.CAMERA;
|
|
88
|
+
// Convert radians to int16 (multiply by 10000 for ~0.0001 precision)
|
|
89
|
+
const yawInt = Math.round(yaw * 10000);
|
|
90
|
+
const pitchInt = Math.round(pitch * 10000);
|
|
91
|
+
buf[1] = yawInt & 0xFF;
|
|
92
|
+
buf[2] = (yawInt >> 8) & 0xFF;
|
|
93
|
+
buf[3] = pitchInt & 0xFF;
|
|
94
|
+
buf[4] = (pitchInt >> 8) & 0xFF;
|
|
95
|
+
return buf;
|
|
96
|
+
}
|
|
97
|
+
function encodeShoot() {
|
|
98
|
+
return new Uint8Array([exports.InputType.SHOOT]);
|
|
99
|
+
}
|
|
100
|
+
function encodeJoin() {
|
|
101
|
+
return new Uint8Array([exports.InputType.JOIN]);
|
|
102
|
+
}
|
|
103
|
+
function encodeLeave() {
|
|
104
|
+
return new Uint8Array([exports.InputType.LEAVE]);
|
|
105
|
+
}
|
|
106
|
+
function decodeInput(buf) {
|
|
107
|
+
if (buf.length === 0)
|
|
108
|
+
return [];
|
|
109
|
+
const type = buf[0];
|
|
110
|
+
const results = [];
|
|
111
|
+
switch (type) {
|
|
112
|
+
case exports.InputType.KEY: {
|
|
113
|
+
const keyByte = buf[1];
|
|
114
|
+
const down = (keyByte & KEY_DOWN_BIT) !== 0;
|
|
115
|
+
const keyIdx = keyByte & 0x7F;
|
|
116
|
+
results.push({
|
|
117
|
+
type: 'key',
|
|
118
|
+
key: keyIndexToName[keyIdx] ?? 'unknown',
|
|
119
|
+
down
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case exports.InputType.KEYS: {
|
|
124
|
+
const count = buf[1];
|
|
125
|
+
for (let i = 0; i < count; i++) {
|
|
126
|
+
const keyByte = buf[2 + i];
|
|
127
|
+
const down = (keyByte & KEY_DOWN_BIT) !== 0;
|
|
128
|
+
const keyIdx = keyByte & 0x7F;
|
|
129
|
+
results.push({
|
|
130
|
+
type: 'key',
|
|
131
|
+
key: keyIndexToName[keyIdx] ?? 'unknown',
|
|
132
|
+
down
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case exports.InputType.CAMERA: {
|
|
138
|
+
const yawInt = buf[1] | (buf[2] << 8);
|
|
139
|
+
const pitchInt = buf[3] | (buf[4] << 8);
|
|
140
|
+
// Convert from uint16 to int16
|
|
141
|
+
const yaw = (yawInt > 32767 ? yawInt - 65536 : yawInt) / 10000;
|
|
142
|
+
const pitch = (pitchInt > 32767 ? pitchInt - 65536 : pitchInt) / 10000;
|
|
143
|
+
results.push({ type: 'camera', yaw, pitch });
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case exports.InputType.SHOOT:
|
|
147
|
+
results.push({ type: 'shoot' });
|
|
148
|
+
break;
|
|
149
|
+
case exports.InputType.JOIN:
|
|
150
|
+
results.push({ type: 'join' });
|
|
151
|
+
break;
|
|
152
|
+
case exports.InputType.LEAVE:
|
|
153
|
+
results.push({ type: 'leave' });
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
// ============================================
|
|
159
|
+
// Size comparison
|
|
160
|
+
// ============================================
|
|
161
|
+
// JSON keypress: ~65 bytes
|
|
162
|
+
// Binary keypress: 2 bytes (32x smaller)
|
|
163
|
+
//
|
|
164
|
+
// JSON camera: ~50 bytes
|
|
165
|
+
// Binary camera: 5 bytes (10x smaller)
|
|
166
|
+
//
|
|
167
|
+
// JSON shoot: ~40 bytes
|
|
168
|
+
// Binary shoot: 1 byte (40x smaller)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game Input Codec - Ultra-compact binary encoding for game inputs
|
|
3
|
+
*
|
|
4
|
+
* Instead of sending 180+ byte JSON:
|
|
5
|
+
* {"type":"move","keys":{"w":true,"a":false},"mouseX":400,"mouseY":300,"yaw":1.5}
|
|
6
|
+
*
|
|
7
|
+
* We send ~10 bytes binary:
|
|
8
|
+
* [type:1][keys:2][mouseX:2][mouseY:2][yaw:2][pitch:2] = 11 bytes
|
|
9
|
+
*
|
|
10
|
+
* This is a 15-20x reduction in bandwidth for move inputs.
|
|
11
|
+
*/
|
|
12
|
+
export interface MoveInput {
|
|
13
|
+
type?: 'move' | 'input';
|
|
14
|
+
keys?: Record<string, boolean>;
|
|
15
|
+
mouseX?: number;
|
|
16
|
+
mouseY?: number;
|
|
17
|
+
x?: number;
|
|
18
|
+
y?: number;
|
|
19
|
+
yaw?: number;
|
|
20
|
+
pitch?: number;
|
|
21
|
+
seq?: number;
|
|
22
|
+
pid?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface JoinInput {
|
|
25
|
+
type: 'join';
|
|
26
|
+
user?: {
|
|
27
|
+
id: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
};
|
|
30
|
+
id?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface LeaveInput {
|
|
33
|
+
type: 'leave';
|
|
34
|
+
id?: string;
|
|
35
|
+
}
|
|
36
|
+
export type GameInput = MoveInput | JoinInput | LeaveInput | {
|
|
37
|
+
type: string;
|
|
38
|
+
[key: string]: any;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Encode a game input to compact binary
|
|
42
|
+
* Returns null if input cannot be encoded (will fall back to JSON)
|
|
43
|
+
*/
|
|
44
|
+
export declare function encodeGameInput(input: GameInput, playerId?: string): Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* Decode binary input back to object
|
|
47
|
+
*/
|
|
48
|
+
export declare function decodeGameInput(buf: Uint8Array | ArrayBuffer, offset?: number): {
|
|
49
|
+
input: GameInput;
|
|
50
|
+
bytesRead: number;
|
|
51
|
+
} | null;
|
|
52
|
+
/**
|
|
53
|
+
* Check if an input will use compact binary encoding (vs JSON fallback)
|
|
54
|
+
*/
|
|
55
|
+
export declare function isCompactInput(input: GameInput): boolean;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Game Input Codec - Ultra-compact binary encoding for game inputs
|
|
4
|
+
*
|
|
5
|
+
* Instead of sending 180+ byte JSON:
|
|
6
|
+
* {"type":"move","keys":{"w":true,"a":false},"mouseX":400,"mouseY":300,"yaw":1.5}
|
|
7
|
+
*
|
|
8
|
+
* We send ~10 bytes binary:
|
|
9
|
+
* [type:1][keys:2][mouseX:2][mouseY:2][yaw:2][pitch:2] = 11 bytes
|
|
10
|
+
*
|
|
11
|
+
* This is a 15-20x reduction in bandwidth for move inputs.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.encodeGameInput = encodeGameInput;
|
|
15
|
+
exports.decodeGameInput = decodeGameInput;
|
|
16
|
+
exports.isCompactInput = isCompactInput;
|
|
17
|
+
// Input types
|
|
18
|
+
const INPUT_TYPE = {
|
|
19
|
+
MOVE: 0x01, // Standard movement input
|
|
20
|
+
JOIN: 0x02, // Player joined
|
|
21
|
+
LEAVE: 0x03, // Player left
|
|
22
|
+
CUSTOM: 0xFF, // Fallback to JSON for unknown types
|
|
23
|
+
};
|
|
24
|
+
// Key bitmask positions (16 keys max with 2 bytes)
|
|
25
|
+
const KEY_BITS = {
|
|
26
|
+
'w': 0,
|
|
27
|
+
'a': 1,
|
|
28
|
+
's': 2,
|
|
29
|
+
'd': 3,
|
|
30
|
+
'space': 4,
|
|
31
|
+
' ': 4,
|
|
32
|
+
'shift': 5,
|
|
33
|
+
'ctrl': 6,
|
|
34
|
+
'control': 6,
|
|
35
|
+
'e': 7,
|
|
36
|
+
'q': 8,
|
|
37
|
+
'r': 9,
|
|
38
|
+
'f': 10,
|
|
39
|
+
'tab': 11,
|
|
40
|
+
'mouse0': 12, // Left click
|
|
41
|
+
'mouse1': 13, // Right click
|
|
42
|
+
'mouse2': 14, // Middle click
|
|
43
|
+
'jump': 4, // Alias for space
|
|
44
|
+
};
|
|
45
|
+
const KEY_NAMES = ['w', 'a', 's', 'd', 'space', 'shift', 'ctrl', 'e', 'q', 'r', 'f', 'tab', 'mouse0', 'mouse1', 'mouse2'];
|
|
46
|
+
/**
|
|
47
|
+
* Encode a game input to compact binary
|
|
48
|
+
* Returns null if input cannot be encoded (will fall back to JSON)
|
|
49
|
+
*/
|
|
50
|
+
function encodeGameInput(input, playerId) {
|
|
51
|
+
if (!input) {
|
|
52
|
+
return encodeCustom(input);
|
|
53
|
+
}
|
|
54
|
+
const type = input.type || 'move';
|
|
55
|
+
switch (type) {
|
|
56
|
+
case 'move':
|
|
57
|
+
case 'input':
|
|
58
|
+
case undefined:
|
|
59
|
+
return encodeMove(input, playerId);
|
|
60
|
+
case 'join':
|
|
61
|
+
return encodeJoin(input);
|
|
62
|
+
case 'leave':
|
|
63
|
+
return encodeLeave(input);
|
|
64
|
+
default:
|
|
65
|
+
return encodeCustom(input);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Encode move/input - the most common and bandwidth-critical input type
|
|
70
|
+
* Format: [MOVE:1][keys:2][mouseX:2][mouseY:2][yaw:2][pitch:2][pidLen:1][pid:var] = 12+ bytes
|
|
71
|
+
*/
|
|
72
|
+
function encodeMove(input, playerId) {
|
|
73
|
+
const pid = playerId || input.pid || '';
|
|
74
|
+
const pidBytes = new TextEncoder().encode(pid);
|
|
75
|
+
// 1 + 2 + 2 + 2 + 2 + 2 + 1 + pidLen = 12 + pidLen bytes
|
|
76
|
+
const buf = new Uint8Array(12 + pidBytes.length);
|
|
77
|
+
const view = new DataView(buf.buffer);
|
|
78
|
+
buf[0] = INPUT_TYPE.MOVE;
|
|
79
|
+
// Encode keys as bitmask
|
|
80
|
+
let keyBits = 0;
|
|
81
|
+
if (input.keys) {
|
|
82
|
+
for (const [key, pressed] of Object.entries(input.keys)) {
|
|
83
|
+
if (pressed) {
|
|
84
|
+
const bit = KEY_BITS[key.toLowerCase()];
|
|
85
|
+
if (bit !== undefined) {
|
|
86
|
+
keyBits |= (1 << bit);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
view.setUint16(1, keyBits, true);
|
|
92
|
+
// Mouse position as uint16 (0-65535 range, plenty for screens)
|
|
93
|
+
const mouseX = Math.max(0, Math.min(65535, Math.round(input.mouseX ?? input.x ?? 0)));
|
|
94
|
+
const mouseY = Math.max(0, Math.min(65535, Math.round(input.mouseY ?? input.y ?? 0)));
|
|
95
|
+
view.setUint16(3, mouseX, true);
|
|
96
|
+
view.setUint16(5, mouseY, true);
|
|
97
|
+
// Yaw/pitch as int16 (scaled by 10000 for precision)
|
|
98
|
+
// Yaw range: [-π, π] -> [-31416, 31416] fits in int16
|
|
99
|
+
const yawScaled = Math.round((input.yaw ?? 0) * 10000);
|
|
100
|
+
const pitchScaled = Math.round((input.pitch ?? 0) * 10000);
|
|
101
|
+
view.setInt16(7, Math.max(-32768, Math.min(32767, yawScaled)), true);
|
|
102
|
+
view.setInt16(9, Math.max(-32768, Math.min(32767, pitchScaled)), true);
|
|
103
|
+
// Player ID
|
|
104
|
+
buf[11] = pidBytes.length;
|
|
105
|
+
buf.set(pidBytes, 12);
|
|
106
|
+
return buf;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encode join event
|
|
110
|
+
* Format: [JOIN:1][pidLen:1][pid:var][nameLen:1][name:var]
|
|
111
|
+
*/
|
|
112
|
+
function encodeJoin(input) {
|
|
113
|
+
const pid = input.user?.id || input.id || '';
|
|
114
|
+
const name = input.user?.name || '';
|
|
115
|
+
const pidBytes = new TextEncoder().encode(pid);
|
|
116
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
117
|
+
const buf = new Uint8Array(3 + pidBytes.length + nameBytes.length);
|
|
118
|
+
buf[0] = INPUT_TYPE.JOIN;
|
|
119
|
+
buf[1] = pidBytes.length;
|
|
120
|
+
buf.set(pidBytes, 2);
|
|
121
|
+
buf[2 + pidBytes.length] = nameBytes.length;
|
|
122
|
+
buf.set(nameBytes, 3 + pidBytes.length);
|
|
123
|
+
return buf;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Encode leave event
|
|
127
|
+
* Format: [LEAVE:1][pidLen:1][pid:var]
|
|
128
|
+
*/
|
|
129
|
+
function encodeLeave(input) {
|
|
130
|
+
const pid = input.id || '';
|
|
131
|
+
const pidBytes = new TextEncoder().encode(pid);
|
|
132
|
+
const buf = new Uint8Array(2 + pidBytes.length);
|
|
133
|
+
buf[0] = INPUT_TYPE.LEAVE;
|
|
134
|
+
buf[1] = pidBytes.length;
|
|
135
|
+
buf.set(pidBytes, 2);
|
|
136
|
+
return buf;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Encode custom/unknown input types as JSON (fallback)
|
|
140
|
+
* Format: [CUSTOM:1][len:2][json:var]
|
|
141
|
+
*/
|
|
142
|
+
function encodeCustom(input) {
|
|
143
|
+
const json = JSON.stringify(input);
|
|
144
|
+
const jsonBytes = new TextEncoder().encode(json);
|
|
145
|
+
const buf = new Uint8Array(3 + jsonBytes.length);
|
|
146
|
+
const view = new DataView(buf.buffer);
|
|
147
|
+
buf[0] = INPUT_TYPE.CUSTOM;
|
|
148
|
+
view.setUint16(1, jsonBytes.length, true);
|
|
149
|
+
buf.set(jsonBytes, 3);
|
|
150
|
+
return buf;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decode binary input back to object
|
|
154
|
+
*/
|
|
155
|
+
function decodeGameInput(buf, offset = 0) {
|
|
156
|
+
const data = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf;
|
|
157
|
+
if (data.length <= offset)
|
|
158
|
+
return null;
|
|
159
|
+
const type = data[offset];
|
|
160
|
+
switch (type) {
|
|
161
|
+
case INPUT_TYPE.MOVE:
|
|
162
|
+
return decodeMove(data, offset);
|
|
163
|
+
case INPUT_TYPE.JOIN:
|
|
164
|
+
return decodeJoin(data, offset);
|
|
165
|
+
case INPUT_TYPE.LEAVE:
|
|
166
|
+
return decodeLeave(data, offset);
|
|
167
|
+
case INPUT_TYPE.CUSTOM:
|
|
168
|
+
return decodeCustom(data, offset);
|
|
169
|
+
default:
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function decodeMove(data, offset) {
|
|
174
|
+
if (data.length < offset + 12)
|
|
175
|
+
return null;
|
|
176
|
+
const view = new DataView(data.buffer, data.byteOffset + offset);
|
|
177
|
+
const keyBits = view.getUint16(1, true);
|
|
178
|
+
const mouseX = view.getUint16(3, true);
|
|
179
|
+
const mouseY = view.getUint16(5, true);
|
|
180
|
+
const yawScaled = view.getInt16(7, true);
|
|
181
|
+
const pitchScaled = view.getInt16(9, true);
|
|
182
|
+
const pidLen = data[offset + 11];
|
|
183
|
+
if (data.length < offset + 12 + pidLen)
|
|
184
|
+
return null;
|
|
185
|
+
const pid = new TextDecoder().decode(data.slice(offset + 12, offset + 12 + pidLen));
|
|
186
|
+
// Decode keys
|
|
187
|
+
const keys = {};
|
|
188
|
+
for (let i = 0; i < KEY_NAMES.length; i++) {
|
|
189
|
+
keys[KEY_NAMES[i]] = (keyBits & (1 << i)) !== 0;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
input: {
|
|
193
|
+
type: 'input',
|
|
194
|
+
keys,
|
|
195
|
+
mouseX,
|
|
196
|
+
mouseY,
|
|
197
|
+
yaw: yawScaled / 10000,
|
|
198
|
+
pitch: pitchScaled / 10000,
|
|
199
|
+
pid: pid || undefined,
|
|
200
|
+
},
|
|
201
|
+
bytesRead: 12 + pidLen
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function decodeJoin(data, offset) {
|
|
205
|
+
if (data.length < offset + 3)
|
|
206
|
+
return null;
|
|
207
|
+
const pidLen = data[offset + 1];
|
|
208
|
+
if (data.length < offset + 2 + pidLen + 1)
|
|
209
|
+
return null;
|
|
210
|
+
const pid = new TextDecoder().decode(data.slice(offset + 2, offset + 2 + pidLen));
|
|
211
|
+
const nameLen = data[offset + 2 + pidLen];
|
|
212
|
+
if (data.length < offset + 3 + pidLen + nameLen)
|
|
213
|
+
return null;
|
|
214
|
+
const name = new TextDecoder().decode(data.slice(offset + 3 + pidLen, offset + 3 + pidLen + nameLen));
|
|
215
|
+
return {
|
|
216
|
+
input: {
|
|
217
|
+
type: 'join',
|
|
218
|
+
user: { id: pid, name: name || undefined },
|
|
219
|
+
id: pid,
|
|
220
|
+
},
|
|
221
|
+
bytesRead: 3 + pidLen + nameLen
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function decodeLeave(data, offset) {
|
|
225
|
+
if (data.length < offset + 2)
|
|
226
|
+
return null;
|
|
227
|
+
const pidLen = data[offset + 1];
|
|
228
|
+
if (data.length < offset + 2 + pidLen)
|
|
229
|
+
return null;
|
|
230
|
+
const pid = new TextDecoder().decode(data.slice(offset + 2, offset + 2 + pidLen));
|
|
231
|
+
return {
|
|
232
|
+
input: {
|
|
233
|
+
type: 'leave',
|
|
234
|
+
id: pid,
|
|
235
|
+
},
|
|
236
|
+
bytesRead: 2 + pidLen
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function decodeCustom(data, offset) {
|
|
240
|
+
if (data.length < offset + 3)
|
|
241
|
+
return null;
|
|
242
|
+
const view = new DataView(data.buffer, data.byteOffset + offset);
|
|
243
|
+
const jsonLen = view.getUint16(1, true);
|
|
244
|
+
if (data.length < offset + 3 + jsonLen)
|
|
245
|
+
return null;
|
|
246
|
+
const json = new TextDecoder().decode(data.slice(offset + 3, offset + 3 + jsonLen));
|
|
247
|
+
try {
|
|
248
|
+
return {
|
|
249
|
+
input: JSON.parse(json),
|
|
250
|
+
bytesRead: 3 + jsonLen
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Check if an input will use compact binary encoding (vs JSON fallback)
|
|
259
|
+
*/
|
|
260
|
+
function isCompactInput(input) {
|
|
261
|
+
const type = input?.type;
|
|
262
|
+
return type === 'move' || type === 'input' || type === 'join' || type === 'leave' || type === undefined;
|
|
263
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { connect as networkConnect, modd, type Connection, type ConnectOptions, type Input } from './modd-network';
|
|
2
2
|
export * from './modd-engine';
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
|
|
17
|
+
exports.modd = exports.networkConnect = void 0;
|
|
18
|
+
// Re-export modd-network (but rename connect to avoid conflict)
|
|
19
|
+
var modd_network_1 = require("./modd-network");
|
|
20
|
+
Object.defineProperty(exports, "networkConnect", { enumerable: true, get: function () { return modd_network_1.connect; } });
|
|
21
|
+
Object.defineProperty(exports, "modd", { enumerable: true, get: function () { return modd_network_1.modd; } });
|
|
22
|
+
// Re-export modd-engine (this is the main API)
|
|
18
23
|
__exportStar(require("./modd-engine"), exports);
|
|
24
|
+
// modd-engine.connect is the primary connect function
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Codec - Compresses readable inputs to binary
|
|
3
|
+
*
|
|
4
|
+
* Game sends: { type: 'keydown', key: 'w' }
|
|
5
|
+
* Wire sends: [0x01, 0x80] (2 bytes)
|
|
6
|
+
*
|
|
7
|
+
* Game sends: { type: 'camera', yaw: 1.5, pitch: 0.3 }
|
|
8
|
+
* Wire sends: [0x02, yaw_lo, yaw_hi, pitch_lo, pitch_hi] (5 bytes)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Compress a readable input to binary
|
|
12
|
+
*/
|
|
13
|
+
export declare function compress(input: any): Uint8Array | null;
|
|
14
|
+
/**
|
|
15
|
+
* Compress multiple inputs into single buffer
|
|
16
|
+
*/
|
|
17
|
+
export declare function compressBatch(inputs: any[]): Uint8Array | null;
|
|
18
|
+
/**
|
|
19
|
+
* Decompress binary back to readable input
|
|
20
|
+
*/
|
|
21
|
+
export declare function decompress(buf: Uint8Array, offset?: number): {
|
|
22
|
+
input: any;
|
|
23
|
+
bytesRead: number;
|
|
24
|
+
} | null;
|
|
25
|
+
/**
|
|
26
|
+
* Decompress a batch of inputs
|
|
27
|
+
*/
|
|
28
|
+
export declare function decompressBatch(buf: Uint8Array): any[];
|
|
29
|
+
/**
|
|
30
|
+
* Check if an input can be compressed
|
|
31
|
+
* Note: join/leave are excluded because they contain unique player ID data
|
|
32
|
+
*/
|
|
33
|
+
export declare function canCompress(input: any): boolean;
|