@bloopjs/engine 0.0.87 → 0.0.89

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.
@@ -1,3 +1,24 @@
1
+ import {
2
+ MAX_PLAYERS,
3
+ NET_CTX_PEERS_OFFSET,
4
+ NET_CTX_LAST_ROLLBACK_DEPTH_OFFSET,
5
+ NET_CTX_TOTAL_ROLLBACKS_OFFSET,
6
+ NET_CTX_FRAMES_RESIMULATED_OFFSET,
7
+ NET_CTX_PEER_COUNT_OFFSET,
8
+ NET_CTX_LOCAL_PEER_ID_OFFSET,
9
+ NET_CTX_IN_SESSION_OFFSET,
10
+ NET_CTX_STATUS_OFFSET,
11
+ NET_CTX_MATCH_FRAME_OFFSET,
12
+ NET_CTX_SESSION_START_FRAME_OFFSET,
13
+ NET_CTX_ROOM_CODE_OFFSET,
14
+ NET_CTX_WANTS_ROOM_CODE_OFFSET,
15
+ NET_CTX_WANTS_DISCONNECT_OFFSET,
16
+ PEER_CTX_SIZE,
17
+ PEER_CTX_CONNECTED_OFFSET,
18
+ PEER_CTX_SEQ_OFFSET,
19
+ PEER_CTX_ACK_OFFSET,
20
+ } from "../codegen/offsets";
21
+
1
22
  /**
2
23
  * Network status values matching Zig NetStatus enum
3
24
  */
@@ -28,17 +49,6 @@ export type PeerInfo = {
28
49
  ack: number; // -1 if no packets received
29
50
  };
30
51
 
31
- const MAX_PEERS = 12;
32
- const PEERS_ARRAY_OFFSET = 32; // After _pad at offset 29-31
33
- const PEER_CTX_SIZE = 8; // connected(1) + packet_count(1) + seq(2) + ack(2) + ack_count(1) + pad(1)
34
-
35
- // Offsets within PeerCtx struct
36
- const PEER_CONNECTED_OFFSET = 0;
37
- const PEER_PACKET_COUNT_OFFSET = 1;
38
- const PEER_SEQ_OFFSET = 2;
39
- const PEER_ACK_OFFSET = 4;
40
- const PEER_ACK_COUNT_OFFSET = 6;
41
-
42
52
  const STATUS_MAP: Record<number, NetStatus> = {
43
53
  0: "offline",
44
54
  1: "local",
@@ -48,14 +58,8 @@ const STATUS_MAP: Record<number, NetStatus> = {
48
58
  };
49
59
 
50
60
  /**
51
- * NetCtx struct layout (from context.zig):
52
- * - peer_count: u8 (offset 0)
53
- * - local_peer_id: u8 (offset 1)
54
- * - in_session: u8 (offset 2)
55
- * - status: u8 (offset 3)
56
- * - match_frame: u32 (offset 4)
57
- * - session_start_frame: u32 (offset 8)
58
- * - room_code: [8]u8 (offset 12)
61
+ * NetCtx struct layout is defined in context.zig.
62
+ * Field offsets are generated in codegen/offsets.ts.
59
63
  *
60
64
  * All getters read directly from the engine's memory via DataView.
61
65
  * State is managed by the Zig engine, not TypeScript.
@@ -64,7 +68,7 @@ export class NetContext {
64
68
  dataView?: DataView;
65
69
 
66
70
  // Pre-allocated peer objects to avoid GC pressure
67
- #peers: PeerInfo[] = Array.from({ length: MAX_PEERS }, () => ({
71
+ #peers: PeerInfo[] = Array.from({ length: MAX_PLAYERS }, () => ({
68
72
  isLocal: false,
69
73
  seq: -1,
70
74
  ack: -1,
@@ -87,7 +91,7 @@ export class NetContext {
87
91
  if (!this.#hasValidBuffer()) {
88
92
  throw new Error("NetContext dataView is not valid");
89
93
  }
90
- return this.dataView!.getUint8(0);
94
+ return this.dataView!.getUint8(NET_CTX_PEER_COUNT_OFFSET);
91
95
  }
92
96
 
93
97
  /** Local peer ID in the session */
@@ -95,7 +99,7 @@ export class NetContext {
95
99
  if (!this.#hasValidBuffer()) {
96
100
  throw new Error("NetContext dataView is not valid");
97
101
  }
98
- return this.dataView!.getUint8(1);
102
+ return this.dataView!.getUint8(NET_CTX_LOCAL_PEER_ID_OFFSET);
99
103
  }
100
104
 
101
105
  /** Whether we're in an active multiplayer session */
@@ -103,7 +107,7 @@ export class NetContext {
103
107
  if (!this.#hasValidBuffer()) {
104
108
  throw new Error("NetContext dataView is not valid");
105
109
  }
106
- return this.dataView!.getUint8(2) !== 0;
110
+ return this.dataView!.getUint8(NET_CTX_IN_SESSION_OFFSET) !== 0;
107
111
  }
108
112
 
109
113
  /** Current network status */
@@ -111,7 +115,7 @@ export class NetContext {
111
115
  if (!this.#hasValidBuffer()) {
112
116
  throw new Error("NetContext dataView is not valid");
113
117
  }
114
- const statusByte = this.dataView!.getUint8(3);
118
+ const statusByte = this.dataView!.getUint8(NET_CTX_STATUS_OFFSET);
115
119
  return STATUS_MAP[statusByte] ?? "local";
116
120
  }
117
121
 
@@ -120,7 +124,7 @@ export class NetContext {
120
124
  if (!this.#hasValidBuffer()) {
121
125
  throw new Error("NetContext dataView is not valid");
122
126
  }
123
- return this.dataView!.getUint32(4, true);
127
+ return this.dataView!.getUint32(NET_CTX_MATCH_FRAME_OFFSET, true);
124
128
  }
125
129
 
126
130
  /**
@@ -131,7 +135,7 @@ export class NetContext {
131
135
  if (!this.#hasValidBuffer()) {
132
136
  throw new Error("NetContext dataView is not valid");
133
137
  }
134
- return this.dataView!.getUint32(8, true);
138
+ return this.dataView!.getUint32(NET_CTX_SESSION_START_FRAME_OFFSET, true);
135
139
  }
136
140
 
137
141
  /** Current room code (empty string if not in a room) */
@@ -139,10 +143,10 @@ export class NetContext {
139
143
  if (!this.#hasValidBuffer()) {
140
144
  throw new Error("NetContext dataView is not valid");
141
145
  }
142
- // Read 8 bytes starting at offset 12, convert to string until null terminator
146
+ // Read 8 bytes starting at room_code offset, convert to string until null terminator
143
147
  const bytes: number[] = [];
144
148
  for (let i = 0; i < 8; i++) {
145
- const byte = this.dataView!.getUint8(12 + i);
149
+ const byte = this.dataView!.getUint8(NET_CTX_ROOM_CODE_OFFSET + i);
146
150
  if (byte === 0) break;
147
151
  bytes.push(byte);
148
152
  }
@@ -154,10 +158,10 @@ export class NetContext {
154
158
  if (!this.#hasValidBuffer()) {
155
159
  return undefined;
156
160
  }
157
- // Read 8 bytes starting at offset 20, convert to string until null terminator
161
+ // Read 8 bytes starting at wants_room_code offset, convert to string until null terminator
158
162
  const bytes: number[] = [];
159
163
  for (let i = 0; i < 8; i++) {
160
- const byte = this.dataView!.getUint8(20 + i);
164
+ const byte = this.dataView!.getUint8(NET_CTX_WANTS_ROOM_CODE_OFFSET + i);
161
165
  if (byte === 0) break;
162
166
  bytes.push(byte);
163
167
  }
@@ -170,12 +174,12 @@ export class NetContext {
170
174
  }
171
175
  // Clear first
172
176
  for (let i = 0; i < 8; i++) {
173
- this.dataView!.setUint8(20 + i, 0);
177
+ this.dataView!.setUint8(NET_CTX_WANTS_ROOM_CODE_OFFSET + i, 0);
174
178
  }
175
179
  if (code) {
176
180
  // Write up to 7 chars (leave room for null terminator)
177
181
  for (let i = 0; i < Math.min(code.length, 7); i++) {
178
- this.dataView!.setUint8(20 + i, code.charCodeAt(i));
182
+ this.dataView!.setUint8(NET_CTX_WANTS_ROOM_CODE_OFFSET + i, code.charCodeAt(i));
179
183
  }
180
184
  }
181
185
  }
@@ -185,14 +189,14 @@ export class NetContext {
185
189
  if (!this.#hasValidBuffer()) {
186
190
  return false;
187
191
  }
188
- return this.dataView!.getUint8(28) !== 0;
192
+ return this.dataView!.getUint8(NET_CTX_WANTS_DISCONNECT_OFFSET) !== 0;
189
193
  }
190
194
 
191
195
  set wantsDisconnect(value: boolean) {
192
196
  if (!this.#hasValidBuffer()) {
193
197
  throw new Error("NetContext dataView is not valid");
194
198
  }
195
- this.dataView!.setUint8(28, value ? 1 : 0);
199
+ this.dataView!.setUint8(NET_CTX_WANTS_DISCONNECT_OFFSET, value ? 1 : 0);
196
200
  }
197
201
 
198
202
  /**
@@ -216,14 +220,15 @@ export class NetContext {
216
220
  const localPeerId = this.localPeerId;
217
221
  const matchFrame = this.matchFrame;
218
222
 
219
- // Calculate local peer ack = min(seq) across connected remotes with packets
223
+ // Calculate local peer ack = min(seq) across connected remotes with data
220
224
  let minRemoteSeq = -1;
221
- for (let i = 0; i < MAX_PEERS; i++) {
225
+ for (let i = 0; i < MAX_PLAYERS; i++) {
222
226
  if (i === localPeerId) continue;
223
- const peerOffset = PEERS_ARRAY_OFFSET + i * PEER_CTX_SIZE;
224
- if (dv.getUint8(peerOffset + PEER_CONNECTED_OFFSET) !== 1) continue;
225
- if (dv.getUint8(peerOffset + PEER_PACKET_COUNT_OFFSET) === 0) continue;
226
- const seq = dv.getUint16(peerOffset + PEER_SEQ_OFFSET, true);
227
+ const peerOffset = NET_CTX_PEERS_OFFSET + i * PEER_CTX_SIZE;
228
+ if (dv.getUint8(peerOffset + PEER_CTX_CONNECTED_OFFSET) !== 1) continue;
229
+ // seq is now i16 with -1 meaning "no data yet"
230
+ const seq = dv.getInt16(peerOffset + PEER_CTX_SEQ_OFFSET, true);
231
+ if (seq < 0) continue; // No data from this peer yet
227
232
  if (minRemoteSeq === -1 || seq < minRemoteSeq) {
228
233
  minRemoteSeq = seq;
229
234
  }
@@ -231,9 +236,9 @@ export class NetContext {
231
236
 
232
237
  // Update pre-allocated peer objects and build result array
233
238
  this.#peersResult.length = 0;
234
- for (let i = 0; i < MAX_PEERS; i++) {
235
- const peerOffset = PEERS_ARRAY_OFFSET + i * PEER_CTX_SIZE;
236
- if (dv.getUint8(peerOffset + PEER_CONNECTED_OFFSET) !== 1) continue;
239
+ for (let i = 0; i < MAX_PLAYERS; i++) {
240
+ const peerOffset = NET_CTX_PEERS_OFFSET + i * PEER_CTX_SIZE;
241
+ if (dv.getUint8(peerOffset + PEER_CTX_CONNECTED_OFFSET) !== 1) continue;
237
242
 
238
243
  const peer = this.#peers[i];
239
244
  if (!peer) {
@@ -246,13 +251,37 @@ export class NetContext {
246
251
  peer.seq = matchFrame;
247
252
  peer.ack = minRemoteSeq;
248
253
  } else {
249
- const packetCount = dv.getUint8(peerOffset + PEER_PACKET_COUNT_OFFSET);
250
- const ackCount = dv.getUint8(peerOffset + PEER_ACK_COUNT_OFFSET);
251
- peer.seq = packetCount === 0 ? -1 : dv.getUint16(peerOffset + PEER_SEQ_OFFSET, true);
252
- peer.ack = ackCount === 0 ? -1 : dv.getUint16(peerOffset + PEER_ACK_OFFSET, true);
254
+ // seq and ack are now i16 with -1 meaning "no data yet"
255
+ peer.seq = dv.getInt16(peerOffset + PEER_CTX_SEQ_OFFSET, true);
256
+ peer.ack = dv.getInt16(peerOffset + PEER_CTX_ACK_OFFSET, true);
253
257
  }
254
258
  this.#peersResult.push(peer);
255
259
  }
256
260
  return this.#peersResult;
257
261
  }
262
+
263
+ /** Last rollback depth (how many frames were rolled back) */
264
+ get lastRollbackDepth(): number {
265
+ if (!this.#hasValidBuffer()) {
266
+ throw new Error("NetContext dataView is not valid");
267
+ }
268
+ return this.dataView!.getUint32(NET_CTX_LAST_ROLLBACK_DEPTH_OFFSET, true);
269
+ }
270
+
271
+ /** Total number of rollbacks during this session */
272
+ get totalRollbacks(): number {
273
+ if (!this.#hasValidBuffer()) {
274
+ throw new Error("NetContext dataView is not valid");
275
+ }
276
+ return this.dataView!.getUint32(NET_CTX_TOTAL_ROLLBACKS_OFFSET, true);
277
+ }
278
+
279
+ /** Total frames resimulated during this session */
280
+ get framesResimulated(): number {
281
+ if (!this.#hasValidBuffer()) {
282
+ throw new Error("NetContext dataView is not valid");
283
+ }
284
+ // Read u64 as BigInt then convert to number (safe for reasonable frame counts)
285
+ return Number(this.dataView!.getBigUint64(NET_CTX_FRAMES_RESIMULATED_OFFSET, true));
286
+ }
258
287
  }
@@ -1,3 +1,9 @@
1
+ import {
2
+ TIME_CTX_FRAME_OFFSET,
3
+ TIME_CTX_DT_MS_OFFSET,
4
+ TIME_CTX_TOTAL_MS_OFFSET,
5
+ } from "../codegen/offsets";
6
+
1
7
  export class TimeContext {
2
8
  dataView?: DataView;
3
9
 
@@ -10,7 +16,7 @@ export class TimeContext {
10
16
  if (!this.dataView) {
11
17
  throw new Error("TimeContext DataView is not initialized");
12
18
  }
13
- return this.dataView.getUint32(0, true);
19
+ return this.dataView.getUint32(TIME_CTX_FRAME_OFFSET, true);
14
20
  }
15
21
 
16
22
  /** The number of seconds since the last frame */
@@ -18,7 +24,7 @@ export class TimeContext {
18
24
  if (!this.dataView) {
19
25
  throw new Error("TimeContext DataView is not initialized");
20
26
  }
21
- return this.dataView.getUint32(4, true) / 1000;
27
+ return this.dataView.getUint32(TIME_CTX_DT_MS_OFFSET, true) / 1000;
22
28
  }
23
29
 
24
30
  /** The total number of seconds since the engine started */
@@ -26,6 +32,6 @@ export class TimeContext {
26
32
  if (!this.dataView) {
27
33
  throw new Error("TimeContext DataView is not initialized");
28
34
  }
29
- return this.dataView.getUint32(8, true) / 1000;
35
+ return this.dataView.getUint32(TIME_CTX_TOTAL_MS_OFFSET, true) / 1000;
30
36
  }
31
37
  }
package/js/defaultUrl.ts CHANGED
@@ -1 +1 @@
1
- export const DEFAULT_WASM_URL: URL = new URL("https://unpkg.com/@bloopjs/engine@0.0.87/wasm/bloop.wasm");
1
+ export const DEFAULT_WASM_URL: URL = new URL("https://unpkg.com/@bloopjs/engine@0.0.89/wasm/bloop.wasm");
package/js/inputs.ts CHANGED
@@ -1,20 +1,24 @@
1
1
  import * as Enums from "./codegen/enums";
2
2
 
3
- // Constants for memory layout
4
- // TODO: move magic numbers to codegen
5
- export const MAX_PLAYERS = 12;
3
+ // Re-export layout constants from generated offsets
4
+ export {
5
+ MAX_PLAYERS,
6
+ KEY_CTX_SIZE,
7
+ MOUSE_CTX_SIZE,
8
+ PLAYER_INPUTS_SIZE,
9
+ INPUT_CTX_SIZE,
10
+ PLAYER_INPUTS_KEY_CTX_OFFSET,
11
+ PLAYER_INPUTS_MOUSE_CTX_OFFSET,
12
+ MOUSE_CTX_BUTTON_STATES_OFFSET,
13
+ } from "./codegen/offsets";
6
14
 
7
- // Per-player offsets (relative to start of PlayerInputs)
8
- export const KEYBOARD_OFFSET = 0;
9
- export const KEYBOARD_SIZE = 256;
10
- export const MOUSE_OFFSET = 256; // After keyboard
11
- export const MOUSE_BUTTONS_OFFSET = 16; // Within MouseCtx, after x, y, wheel_x, wheel_y (4 floats = 16 bytes)
12
-
13
- // PlayerInputs size = KeyCtx (256) + MouseCtx (24) = 280 bytes
14
- export const PLAYER_INPUTS_SIZE = 280;
15
-
16
- // InputCtx layout: players[12] = 12 * 280 = 3360 bytes
17
- export const INPUT_CTX_SIZE = MAX_PLAYERS * PLAYER_INPUTS_SIZE;
15
+ // Backwards compatibility aliases
16
+ export {
17
+ PLAYER_INPUTS_KEY_CTX_OFFSET as KEYBOARD_OFFSET,
18
+ KEY_CTX_SIZE as KEYBOARD_SIZE,
19
+ PLAYER_INPUTS_MOUSE_CTX_OFFSET as MOUSE_OFFSET,
20
+ MOUSE_CTX_BUTTON_STATES_OFFSET as MOUSE_BUTTONS_OFFSET,
21
+ } from "./codegen/offsets";
18
22
 
19
23
  export const EVENT_PAYLOAD_SIZE = 8;
20
24
  export const EVENT_PAYLOAD_ALIGN = 4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/engine",
3
- "version": "0.0.87",
3
+ "version": "0.0.89",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/wasm/bloop.wasm CHANGED
Binary file