@bloopjs/bloop 0.0.80 → 0.0.81

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/sim.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { type EnginePointer, type Key, type MouseButton, TimeContext, type WasmEngine } from "@bloopjs/engine";
2
- import { Net } from "./net";
1
+ import { type EnginePointer, type Key, type MouseButton, NetContext, type NetEvent, type NetEventType, TimeContext, type WasmEngine } from "@bloopjs/engine";
3
2
  export type EngineHooks = {
4
3
  /**
5
4
  * Hook to serialize some data when snapshotting
@@ -46,11 +45,6 @@ export declare class Sim {
46
45
  #private;
47
46
  wasm: WasmEngine;
48
47
  id: string;
49
- /**
50
- * Network API for packet management in multiplayer sessions.
51
- * Use this to send/receive packets and query peer network state.
52
- */
53
- readonly net: Net;
54
48
  /**
55
49
  * Callback fired when tape buffer fills up and recording stops.
56
50
  * The tape data is passed so you can save it before clearing.
@@ -59,6 +53,14 @@ export declare class Sim {
59
53
  constructor(wasm: WasmEngine, memory: WebAssembly.Memory, opts?: {
60
54
  serialize?: SerializeFn;
61
55
  });
56
+ /**
57
+ * Step simulation forward by a given number of milliseconds.
58
+ * If ms is not provided, defaults to 16ms (60fps).
59
+ *
60
+ * Note: this will not advance time if the sim is paused.
61
+ *
62
+ * @returns Number of frames actually advanced
63
+ */
62
64
  step(ms?: number): number;
63
65
  /**
64
66
  * Run a single simulation frame. step wraps this in an accumulator.
@@ -73,13 +75,28 @@ export declare class Sim {
73
75
  * @param source
74
76
  */
75
77
  cloneSession(source: Sim): void;
78
+ /**
79
+ * Pause the simulation entirely
80
+ */
76
81
  pause(): void;
82
+ /**
83
+ * Unpause the simulation
84
+ */
77
85
  unpause(): void;
86
+ /**
87
+ * Whether the simulation is currently paused
88
+ */
78
89
  get isPaused(): boolean;
90
+ /**
91
+ * Step back one frame
92
+ */
79
93
  stepBack(): void;
80
94
  /**
81
- * Seek to the start of a given frame
82
- * @param frame - frame number to replay to
95
+ * Seek to the start of a given frame.
96
+ *
97
+ * Note that this will mute console output during the seek if rewinding.
98
+ *
99
+ * @param frame - frame number to replay up to
83
100
  */
84
101
  seek(frame: number, inclusive?: boolean): void;
85
102
  /**
@@ -113,6 +130,7 @@ export declare class Sim {
113
130
  */
114
131
  unmount(): void;
115
132
  get time(): TimeContext;
133
+ get net(): NetContext;
116
134
  get buffer(): ArrayBuffer;
117
135
  get isRecording(): boolean;
118
136
  get isReplaying(): boolean;
@@ -124,17 +142,27 @@ export declare class Sim {
124
142
  mousedown: (button: MouseButton, peerId?: number) => void;
125
143
  mouseup: (button: MouseButton, peerId?: number) => void;
126
144
  mousewheel: (x: number, y: number, peerId?: number) => void;
145
+ /**
146
+ * Emit a network event (join:ok, peer:join, etc.)
147
+ * Events are queued in the engine and processed during the next tick's systemsCallback.
148
+ */
149
+ network: <T extends NetEventType>(type: T, data: Extract<NetEvent, {
150
+ type: T;
151
+ }>["data"]) => void;
152
+ /**
153
+ * Receive a packet from a peer
154
+ */
155
+ packet: (data: Uint8Array) => void;
127
156
  };
128
157
  /**
129
- * Initialize a multiplayer session with rollback support.
130
- * Captures current frame as session start frame.
158
+ * Get an outbound packet to send to a target peer.
159
+ * Returns a copy of the packet data (caller owns the returned buffer).
131
160
  *
132
- * @param peerCount Number of peers in the session
133
- */
134
- sessionInit(peerCount: number): void;
135
- /**
136
- * End the current session and clean up rollback state.
161
+ * The packet contains all unacked inputs encoded in wire format.
162
+ *
163
+ * @param targetPeer - Peer ID to send the packet to
164
+ * @returns Packet data to send, or null if no packet available
137
165
  */
138
- sessionEnd(): void;
166
+ getOutboundPacket(targetPeer: number): Uint8Array<ArrayBuffer> | null;
139
167
  }
140
168
  //# sourceMappingURL=sim.d.ts.map
package/dist/sim.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../src/sim.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../src/sim.ts"],"names":[],"mappings":"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"}
package/dist/system.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { NetEvent } from "@bloopjs/engine";
1
2
  import type { Context } from "./context";
2
3
  import type { BloopSchema } from "./data/schema";
3
4
  import type { KeyEvent, MouseButtonEvent, MouseMoveEvent, MouseWheelEvent } from "./events";
@@ -22,5 +23,9 @@ export type System<GS extends BloopSchema = BloopSchema> = {
22
23
  mousewheel?: (context: Context<GS> & {
23
24
  event: MouseWheelEvent;
24
25
  }) => void;
26
+ /** Handle network events (join:ok, peer:join, etc.) */
27
+ netcode?: (context: Context<GS> & {
28
+ event: NetEvent;
29
+ }) => void;
25
30
  };
26
31
  //# sourceMappingURL=system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../src/system.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,eAAe,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,MAAM,CAAC,EAAE,SAAS,WAAW,GAAG,WAAW,IAAI;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,QAAQ,CAAC;KACjB,KACE,IAAI,CAAC;IAEV,KAAK,CAAC,EAAE,CACN,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,QAAQ,CAAC;KACjB,KACE,IAAI,CAAC;IAEV,SAAS,CAAC,EAAE,CACV,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,gBAAgB,CAAC;KACzB,KACE,IAAI,CAAC;IAEV,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,gBAAgB,CAAC;KACzB,KACE,IAAI,CAAC;IAEV,SAAS,CAAC,EAAE,CACV,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,cAAc,CAAC;KACvB,KACE,IAAI,CAAC;IAEV,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,eAAe,CAAC;KACxB,KACE,IAAI,CAAC;CACX,CAAC"}
1
+ {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../src/system.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,eAAe,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,MAAM,CAAC,EAAE,SAAS,WAAW,GAAG,WAAW,IAAI;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,QAAQ,CAAC;KACjB,KACE,IAAI,CAAC;IAEV,KAAK,CAAC,EAAE,CACN,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,QAAQ,CAAC;KACjB,KACE,IAAI,CAAC;IAEV,SAAS,CAAC,EAAE,CACV,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,gBAAgB,CAAC;KACzB,KACE,IAAI,CAAC;IAEV,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,gBAAgB,CAAC;KACzB,KACE,IAAI,CAAC;IAEV,SAAS,CAAC,EAAE,CACV,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,cAAc,CAAC;KACvB,KACE,IAAI,CAAC;IAEV,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,eAAe,CAAC;KACxB,KACE,IAAI,CAAC;IAEV,uDAAuD;IACvD,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG;QACrB,KAAK,EAAE,QAAQ,CAAC;KACjB,KACE,IAAI,CAAC;CACX,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/bloop",
3
- "version": "0.0.80",
3
+ "version": "0.0.81",
4
4
  "author": "Neil Sarkar",
5
5
  "type": "module",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@types/bun": "latest"
36
36
  },
37
37
  "dependencies": {
38
- "@bloopjs/engine": "0.0.80"
38
+ "@bloopjs/engine": "0.0.81"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "typescript": "^5"
package/src/bloop.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  type EnginePointer,
3
3
  Enums,
4
- EVENT_PAYLOAD_ALIGN,
5
- EVENT_PAYLOAD_SIZE,
6
4
  EVENTS_OFFSET,
7
5
  INPUT_CTX_OFFSET,
8
6
  InputContext,
@@ -10,6 +8,7 @@ import {
10
8
  mouseButtonCodeToMouseButton,
11
9
  NET_CTX_OFFSET,
12
10
  NetContext,
11
+ type NetEvent,
13
12
  TIME_CTX_OFFSET,
14
13
  TimeContext,
15
14
  } from "@bloopjs/engine";
@@ -254,8 +253,70 @@ export class Bloop<GS extends BloopSchema> {
254
253
  }),
255
254
  );
256
255
  break;
256
+ case Enums.EventType.NetJoinOk: {
257
+ // Room code is 8 bytes starting at payload offset
258
+ const roomCodeBytes: number[] = [];
259
+ for (let j = 0; j < 8; j++) {
260
+ const byte = eventsDataView.getUint8(payloadOffset + j);
261
+ if (byte === 0) break;
262
+ roomCodeBytes.push(byte);
263
+ }
264
+ const roomCode = String.fromCharCode(...roomCodeBytes);
265
+ (this.#context as any).event = {
266
+ type: "join:ok",
267
+ data: { roomCode },
268
+ };
269
+ system.netcode?.(
270
+ this.#context as Context<GS> & { event: NetEvent },
271
+ );
272
+ break;
273
+ }
274
+ case Enums.EventType.NetJoinFail: {
275
+ // Reason is stored at payload offset as u8
276
+ const reasonCode = eventsDataView.getUint8(payloadOffset);
277
+ const reasons = [
278
+ "unknown",
279
+ "timeout",
280
+ "room_full",
281
+ "room_not_found",
282
+ "already_in_room",
283
+ ];
284
+ const reason = reasons[reasonCode] ?? "unknown";
285
+ (this.#context as any).event = {
286
+ type: "join:fail",
287
+ data: { reason },
288
+ };
289
+ system.netcode?.(
290
+ this.#context as Context<GS> & { event: NetEvent },
291
+ );
292
+ break;
293
+ }
294
+ case Enums.EventType.NetPeerJoin: {
295
+ const peerId = eventsDataView.getUint8(payloadOffset);
296
+ (this.#context as any).event = {
297
+ type: "peer:join",
298
+ data: { peerId },
299
+ };
300
+ system.netcode?.(
301
+ this.#context as Context<GS> & { event: NetEvent },
302
+ );
303
+ break;
304
+ }
305
+ case Enums.EventType.NetPeerLeave: {
306
+ const peerId = eventsDataView.getUint8(payloadOffset);
307
+ (this.#context as any).event = {
308
+ type: "peer:leave",
309
+ data: { peerId },
310
+ };
311
+ system.netcode?.(
312
+ this.#context as Context<GS> & { event: NetEvent },
313
+ );
314
+ break;
315
+ }
257
316
  default:
258
- throw new Error(`Unknown event type: ${eventType}`);
317
+ // Session lifecycle and other internal events are handled by engine
318
+ // They don't need to be dispatched to game systems
319
+ break;
259
320
  }
260
321
  // Event is 12 bytes: kind (1) + source (1) + padding (2) + payload (8)
261
322
  offset += 12;
package/src/sim.ts CHANGED
@@ -4,12 +4,14 @@ import {
4
4
  keyToKeyCode,
5
5
  type MouseButton,
6
6
  mouseButtonToMouseButtonCode,
7
+ NetContext,
8
+ type NetEvent,
9
+ type NetEventType,
7
10
  SNAPSHOT_HEADER_ENGINE_LEN_OFFSET,
8
11
  SNAPSHOT_HEADER_USER_LEN_OFFSET,
9
12
  TimeContext,
10
13
  type WasmEngine,
11
14
  } from "@bloopjs/engine";
12
- import { Net } from "./net";
13
15
  import { assert } from "./util";
14
16
 
15
17
  const originalConsole = (globalThis as any).console;
@@ -90,10 +92,10 @@ export class Sim {
90
92
  #isPaused: boolean = false;
91
93
 
92
94
  /**
93
- * Network API for packet management in multiplayer sessions.
94
- * Use this to send/receive packets and query peer network state.
95
+ * Shared network context - reads from same WASM memory as game.context.net.
96
+ * Provides access to network state (status, peers, roomCode, wantsRoomCode, etc.)
95
97
  */
96
- readonly net: Net;
98
+ #net: NetContext;
97
99
 
98
100
  /**
99
101
  * Callback fired when tape buffer fills up and recording stops.
@@ -115,9 +117,17 @@ export class Sim {
115
117
  this.id = `${Math.floor(Math.random() * 1_000_000)}`;
116
118
 
117
119
  this.#serialize = opts?.serialize;
118
- this.net = new Net(wasm, memory);
120
+ this.#net = new NetContext();
119
121
  }
120
122
 
123
+ /**
124
+ * Step simulation forward by a given number of milliseconds.
125
+ * If ms is not provided, defaults to 16ms (60fps).
126
+ *
127
+ * Note: this will not advance time if the sim is paused.
128
+ *
129
+ * @returns Number of frames actually advanced
130
+ */
121
131
  step(ms?: number): number {
122
132
  if (this.#isPaused) {
123
133
  return 0;
@@ -147,18 +157,30 @@ export class Sim {
147
157
  this.restore(source.snapshot());
148
158
  }
149
159
 
160
+ /**
161
+ * Pause the simulation entirely
162
+ */
150
163
  pause() {
151
164
  this.#isPaused = true;
152
165
  }
153
166
 
167
+ /**
168
+ * Unpause the simulation
169
+ */
154
170
  unpause() {
155
171
  this.#isPaused = false;
156
172
  }
157
173
 
174
+ /**
175
+ * Whether the simulation is currently paused
176
+ */
158
177
  get isPaused(): boolean {
159
178
  return this.#isPaused;
160
179
  }
161
180
 
181
+ /**
182
+ * Step back one frame
183
+ */
162
184
  stepBack() {
163
185
  if (this.time.frame === 0) {
164
186
  return;
@@ -168,8 +190,11 @@ export class Sim {
168
190
  }
169
191
 
170
192
  /**
171
- * Seek to the start of a given frame
172
- * @param frame - frame number to replay to
193
+ * Seek to the start of a given frame.
194
+ *
195
+ * Note that this will mute console output during the seek if rewinding.
196
+ *
197
+ * @param frame - frame number to replay up to
173
198
  */
174
199
  seek(frame: number, inclusive?: boolean) {
175
200
  assert(
@@ -268,7 +293,9 @@ export class Sim {
268
293
  memoryView.set(tape);
269
294
 
270
295
  // load the tape
271
- this.wasm.stop_recording();
296
+ if (this.isRecording) {
297
+ this.wasm.stop_recording();
298
+ }
272
299
  const result = this.wasm.load_tape(tapePtr, tape.byteLength);
273
300
  assert(result === 0, `failed to load tape, error code=${result}`);
274
301
 
@@ -322,6 +349,20 @@ export class Sim {
322
349
  return this.#time;
323
350
  }
324
351
 
352
+ get net(): NetContext {
353
+ if (
354
+ !this.#net.dataView ||
355
+ this.#net.dataView.buffer !== this.#memory.buffer
356
+ ) {
357
+ // update the data view to the latest memory buffer
358
+ this.#net.dataView = new DataView(
359
+ this.#memory.buffer,
360
+ this.wasm.get_net_ctx(),
361
+ );
362
+ }
363
+ return this.#net;
364
+ }
365
+
325
366
  get buffer(): ArrayBuffer {
326
367
  return this.#memory.buffer;
327
368
  }
@@ -357,29 +398,120 @@ export class Sim {
357
398
  mousewheel: (x: number, y: number, peerId: number = 0): void => {
358
399
  this.wasm.emit_mousewheel(x, y, peerId);
359
400
  },
360
- };
401
+ /**
402
+ * Emit a network event (join:ok, peer:join, etc.)
403
+ * Events are queued in the engine and processed during the next tick's systemsCallback.
404
+ */
405
+ network: <T extends NetEventType>(
406
+ type: T,
407
+ data: Extract<NetEvent, { type: T }>["data"],
408
+ ): void => {
409
+ switch (type) {
410
+ case "join:ok": {
411
+ const roomCode = (data as { roomCode: string }).roomCode;
412
+ const encoded = new TextEncoder().encode(roomCode);
413
+ const ptr = this.wasm.alloc(encoded.length);
414
+ new Uint8Array(this.#memory.buffer, ptr, encoded.length).set(encoded);
415
+ this.wasm.emit_net_join_ok(ptr, encoded.length);
416
+ this.wasm.free(ptr, encoded.length);
417
+ break;
418
+ }
419
+ case "join:fail":
420
+ this.wasm.emit_net_join_fail(0); // reason code: unknown
421
+ break;
422
+ case "peer:join": {
423
+ const peerId = (data as { peerId: number }).peerId;
424
+ this.wasm.emit_net_peer_join(peerId);
425
+ break;
426
+ }
427
+ case "peer:leave": {
428
+ const peerId = (data as { peerId: number }).peerId;
429
+ this.wasm.emit_net_peer_leave(peerId);
430
+ break;
431
+ }
432
+ case "peer:assign_local_id": {
433
+ const peerId = (data as { peerId: number }).peerId;
434
+ this.wasm.emit_net_peer_assign_local_id(peerId);
435
+ break;
436
+ }
437
+ case "session:start":
438
+ this.wasm.emit_net_session_init();
439
+ break;
440
+ case "session:end":
441
+ this.wasm.emit_net_session_end();
442
+ break;
443
+ }
444
+ },
361
445
 
362
- // ─────────────────────────────────────────────────────────────
363
- // Session / Rollback
364
- // ─────────────────────────────────────────────────────────────
446
+ /**
447
+ * Receive a packet from a peer
448
+ */
449
+ packet: (data: Uint8Array): void => {
450
+ if (data.length === 0) {
451
+ return;
452
+ }
453
+
454
+ // Allocate memory and copy packet
455
+ const ptr = this.wasm.alloc(data.byteLength);
456
+ assert(
457
+ ptr > 0,
458
+ `Failed to allocate ${data.byteLength} bytes for received packet`,
459
+ );
460
+
461
+ const memoryView = new Uint8Array(
462
+ this.#memory.buffer,
463
+ ptr,
464
+ data.byteLength,
465
+ );
466
+ memoryView.set(data);
467
+
468
+ // Process the packet
469
+ const result = this.wasm.emit_receive_packet(ptr, data.byteLength);
470
+
471
+ // Free the allocated memory
472
+ this.wasm.free(ptr, data.byteLength);
473
+
474
+ // Check result
475
+ if (result !== 0) {
476
+ const errorMessages: Record<number, string> = {
477
+ 1: "No active session",
478
+ 2: "Buffer too small",
479
+ 3: "Unsupported packet version",
480
+ 4: "Invalid event count",
481
+ };
482
+ throw new Error(
483
+ errorMessages[result] ?? `Unknown packet error: ${result}`,
484
+ );
485
+ }
486
+ },
487
+ };
365
488
 
366
489
  /**
367
- * Initialize a multiplayer session with rollback support.
368
- * Captures current frame as session start frame.
490
+ * Get an outbound packet to send to a target peer.
491
+ * Returns a copy of the packet data (caller owns the returned buffer).
369
492
  *
370
- * @param peerCount Number of peers in the session
493
+ * The packet contains all unacked inputs encoded in wire format.
494
+ *
495
+ * @param targetPeer - Peer ID to send the packet to
496
+ * @returns Packet data to send, or null if no packet available
371
497
  */
372
- sessionInit(peerCount: number): void {
373
- const serializer = this.#serialize ? this.#serialize() : null;
374
- const size = serializer ? serializer.size : 0;
375
- const result = this.wasm.session_init(peerCount, size);
376
- assert(result === 0, `failed to initialize session, error code=${result}`);
377
- }
498
+ getOutboundPacket(targetPeer: number): Uint8Array<ArrayBuffer> | null {
499
+ // Build the packet in engine memory
500
+ this.wasm.build_outbound_packet(targetPeer);
378
501
 
379
- /**
380
- * End the current session and clean up rollback state.
381
- */
382
- sessionEnd(): void {
383
- this.wasm.session_end();
502
+ const len = this.wasm.get_outbound_packet_len();
503
+ if (len === 0) {
504
+ throw new Error(`No outbound packet available for peer ${targetPeer}`);
505
+ }
506
+
507
+ const ptr = this.wasm.get_outbound_packet();
508
+ assert(ptr > 0, `Invalid outbound packet pointer: ${ptr}`);
509
+
510
+ // Copy from WASM memory
511
+ const memoryView = new Uint8Array(this.#memory.buffer, ptr, len);
512
+ const copy = new Uint8Array(len);
513
+ copy.set(memoryView);
514
+
515
+ return copy;
384
516
  }
385
517
  }
package/src/system.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { NetEvent } from "@bloopjs/engine";
1
2
  import type { Context } from "./context";
2
3
  import type { BloopSchema } from "./data/schema";
3
4
  import type {
@@ -47,4 +48,11 @@ export type System<GS extends BloopSchema = BloopSchema> = {
47
48
  event: MouseWheelEvent;
48
49
  },
49
50
  ) => void;
51
+
52
+ /** Handle network events (join:ok, peer:join, etc.) */
53
+ netcode?: (
54
+ context: Context<GS> & {
55
+ event: NetEvent;
56
+ },
57
+ ) => void;
50
58
  };
package/dist/net.d.ts DELETED
@@ -1,60 +0,0 @@
1
- import type { WasmEngine } from "@bloopjs/engine";
2
- /**
3
- * Network API for packet management in multiplayer sessions.
4
- *
5
- * This class provides methods for:
6
- * - Building outbound packets to send to peers
7
- * - Processing received packets
8
- * - Querying peer network state (seq/ack/unacked)
9
- * - Managing peer connections
10
- *
11
- * Access via `sim.net` after initializing a session.
12
- */
13
- export declare class Net {
14
- #private;
15
- constructor(wasm: WasmEngine, memory: WebAssembly.Memory);
16
- /**
17
- * Set the local peer ID for packet encoding.
18
- * Call this after session init to identify which peer we are.
19
- */
20
- setLocalPeer(peerId: number): void;
21
- /**
22
- * Mark a peer as connected for packet management.
23
- * Call this when a peer joins the session.
24
- */
25
- connectPeer(peerId: number): void;
26
- /**
27
- * Mark a peer as disconnected.
28
- * Call this when a peer leaves the session.
29
- */
30
- disconnectPeer(peerId: number): void;
31
- /**
32
- * Get an outbound packet to send to a target peer.
33
- * Returns a copy of the packet data (caller owns the returned buffer).
34
- *
35
- * The packet contains all unacked inputs encoded in wire format.
36
- *
37
- * @param targetPeer - Peer ID to send the packet to
38
- * @returns Packet data to send, or null if no packet available
39
- */
40
- getOutboundPacket(targetPeer: number): Uint8Array<ArrayBuffer> | null;
41
- /**
42
- * Process a received packet from a peer.
43
- * This updates seq/ack state and stores events for rollback.
44
- *
45
- * @param data - Packet data received from the network
46
- * @throws Error if packet is invalid
47
- */
48
- receivePacket(data: Uint8Array): void;
49
- /**
50
- * Get network state for a specific peer.
51
- *
52
- * @param peer - Peer ID to query
53
- * @returns Object with seq and ack
54
- */
55
- getPeerState(peer: number): {
56
- seq: number;
57
- ack: number;
58
- };
59
- }
60
- //# sourceMappingURL=net.d.ts.map
package/dist/net.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGjE;;;;;;;;;;GAUG;AACH,qBAAa,GAAG;;gBAIF,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM;IAKxD;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIjC;;;OAGG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,IAAI;IAoBrE;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAuCrC;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG;QAC1B,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;KACb;CAMF"}