@bloopjs/bloop 0.0.21 → 0.0.22

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/src/bloop.ts CHANGED
@@ -21,7 +21,7 @@ import type {
21
21
  MouseMoveEvent,
22
22
  MouseWheelEvent,
23
23
  } from "./events";
24
- import type { DeserializeFn, SerializeFn } from "./runtime";
24
+ import type { DeserializeFn, EngineHooks, SerializeFn } from "./runtime";
25
25
  import type { System } from "./system";
26
26
 
27
27
  export type BloopOpts<B extends Bag> = {
@@ -63,6 +63,9 @@ export class Bloop<GS extends BloopSchema> {
63
63
  return new Bloop<MakeGS<B>>(opts, "dontCallMeDirectly");
64
64
  }
65
65
 
66
+ /**
67
+ * DO NOT USE `new Bloop` - use `Bloop.create()` instead for proper type hints.
68
+ */
66
69
  constructor(opts: BloopOpts<GS["B"]> = {}, dontCallMeDirectly: string) {
67
70
  if (dontCallMeDirectly !== "dontCallMeDirectly") {
68
71
  throw new Error(
@@ -78,51 +81,22 @@ export class Bloop<GS extends BloopSchema> {
78
81
  };
79
82
  }
80
83
 
84
+ /**
85
+ * Read the game singleton bag
86
+ */
81
87
  get bag(): GS["B"] {
82
88
  return this.#context.bag;
83
89
  }
84
90
 
91
+ /**
92
+ * Read the game context object
93
+ */
85
94
  get context(): Readonly<Context<GS>> {
86
95
  return this.#context;
87
96
  }
88
97
 
89
- /**
90
- * Take a snapshot of the game state outside the engine
91
- * @returns linear memory representation of the game state that the engine is unaware of
92
- */
93
- serialize = (): ReturnType<SerializeFn> => {
94
- const str = JSON.stringify(this.#context.bag);
95
- const encoder = new TextEncoder();
96
- const textBytes = encoder.encode(str);
97
-
98
- return {
99
- size: textBytes.byteLength,
100
- write: (buffer: ArrayBufferLike, ptr: EnginePointer) => {
101
- const memoryView = new Uint8Array(buffer, ptr, textBytes.byteLength);
102
- memoryView.set(textBytes);
103
- },
104
- };
105
- };
106
-
107
- /**
108
- * Restore a snapshot of the game state outside the engine
109
- * @returns linear memory representation of the game state that the engine is unaware of
110
- */
111
- deserialize: DeserializeFn = (buffer, ptr, len) => {
112
- const bagBytes = new Uint8Array(buffer, ptr, len);
113
- const decoder = new TextDecoder();
114
- const str = decoder.decode(bagBytes);
115
-
116
- try {
117
- this.#context.bag = JSON.parse(str);
118
- } catch (e) {
119
- console.error("failed to deserialize bag", { json: str, error: e });
120
- }
121
- };
122
-
123
98
  /**
124
99
  * Register a system with the game loop.
125
- *
126
100
  */
127
101
  system(label: string, system: System<GS>): number {
128
102
  system.label ??= label;
@@ -130,102 +104,144 @@ export class Bloop<GS extends BloopSchema> {
130
104
  return this.#systems.length;
131
105
  }
132
106
 
133
- systemsCallback(system_handle: number, ptr: EnginePointer) {
134
- // todo - move this to engine
135
- const dv = new DataView(this.#engineBuffer, ptr);
136
- const timeCtxPtr = dv.getUint32(TIME_CTX_OFFSET, true);
137
- const inputCtxPtr = dv.getUint32(INPUT_CTX_OFFSET, true);
138
- const eventsPtr = dv.getUint32(EVENTS_OFFSET, true);
139
-
140
- this.#context.rawPointer = ptr;
141
- this.#context.inputs.dataView = new DataView(
142
- this.#engineBuffer,
143
- inputCtxPtr,
144
- );
145
- this.#context.time.dataView = new DataView(this.#engineBuffer, timeCtxPtr);
146
-
147
- const eventsDataView = new DataView(this.#engineBuffer, eventsPtr);
148
-
149
- for (const system of this.#systems) {
150
- system.update?.(this.#context);
151
-
152
- const eventCount = eventsDataView.getUint32(0, true);
153
-
154
- let offset = Uint32Array.BYTES_PER_ELEMENT;
155
-
156
- for (let i = 0; i < eventCount; i++) {
157
- const eventType = eventsDataView.getUint8(offset);
158
- const payloadSize = EVENT_PAYLOAD_SIZE;
159
- const payloadByte = eventsDataView.getUint8(
160
- offset + EVENT_PAYLOAD_ALIGN,
161
- );
162
- const payloadVec2 = {
163
- x: eventsDataView.getFloat32(offset + EVENT_PAYLOAD_ALIGN, true),
164
- y: eventsDataView.getFloat32(
165
- offset + EVENT_PAYLOAD_ALIGN + Float32Array.BYTES_PER_ELEMENT,
166
- true,
167
- ),
168
- };
169
-
170
- switch (eventType) {
171
- case Enums.EventType.KeyDown: {
172
- system.keydown?.(
173
- attachEvent<KeyEvent, GS>(this.#context, {
174
- key: keyCodeToKey(payloadByte),
175
- }),
176
- );
177
- break;
107
+ /**
108
+ * Low level hooks to engine functionality. Editing these is for advanced use cases, defaults should usually work.
109
+ */
110
+ hooks: EngineHooks = {
111
+ /**
112
+ * Take a snapshot of the game state outside the engine
113
+ * @returns linear memory representation of the game state that the engine is unaware of
114
+ */
115
+ serialize: () => {
116
+ const str = JSON.stringify(this.#context.bag);
117
+ const encoder = new TextEncoder();
118
+ const textBytes = encoder.encode(str);
119
+
120
+ return {
121
+ size: textBytes.byteLength,
122
+ write: (buffer: ArrayBufferLike, ptr: EnginePointer) => {
123
+ const memoryView = new Uint8Array(buffer, ptr, textBytes.byteLength);
124
+ memoryView.set(textBytes);
125
+ },
126
+ };
127
+ },
128
+
129
+ /**
130
+ * Restore a snapshot of the game state outside the engine
131
+ * @returns linear memory representation of the game state that the engine is unaware of
132
+ */
133
+ deserialize: (buffer, ptr, len) => {
134
+ const bagBytes = new Uint8Array(buffer, ptr, len);
135
+ const decoder = new TextDecoder();
136
+ const str = decoder.decode(bagBytes);
137
+
138
+ try {
139
+ this.#context.bag = JSON.parse(str);
140
+ } catch (e) {
141
+ console.error("failed to deserialize bag", { json: str, error: e });
142
+ }
143
+ },
144
+
145
+ setBuffer: (buffer: ArrayBuffer) => {
146
+ this.#engineBuffer = buffer;
147
+ },
148
+
149
+ systemsCallback: (system_handle: number, ptr: EnginePointer) => {
150
+ // todo - move this to engine
151
+ const dv = new DataView(this.#engineBuffer, ptr);
152
+ const timeCtxPtr = dv.getUint32(TIME_CTX_OFFSET, true);
153
+ const inputCtxPtr = dv.getUint32(INPUT_CTX_OFFSET, true);
154
+ const eventsPtr = dv.getUint32(EVENTS_OFFSET, true);
155
+
156
+ this.#context.rawPointer = ptr;
157
+ this.#context.inputs.dataView = new DataView(
158
+ this.#engineBuffer,
159
+ inputCtxPtr,
160
+ );
161
+ this.#context.time.dataView = new DataView(
162
+ this.#engineBuffer,
163
+ timeCtxPtr,
164
+ );
165
+
166
+ const eventsDataView = new DataView(this.#engineBuffer, eventsPtr);
167
+
168
+ for (const system of this.#systems) {
169
+ system.update?.(this.#context);
170
+
171
+ const eventCount = eventsDataView.getUint32(0, true);
172
+
173
+ let offset = Uint32Array.BYTES_PER_ELEMENT;
174
+
175
+ for (let i = 0; i < eventCount; i++) {
176
+ const eventType = eventsDataView.getUint8(offset);
177
+ const payloadSize = EVENT_PAYLOAD_SIZE;
178
+ const payloadByte = eventsDataView.getUint8(
179
+ offset + EVENT_PAYLOAD_ALIGN,
180
+ );
181
+ const payloadVec2 = {
182
+ x: eventsDataView.getFloat32(offset + EVENT_PAYLOAD_ALIGN, true),
183
+ y: eventsDataView.getFloat32(
184
+ offset + EVENT_PAYLOAD_ALIGN + Float32Array.BYTES_PER_ELEMENT,
185
+ true,
186
+ ),
187
+ };
188
+
189
+ switch (eventType) {
190
+ case Enums.EventType.KeyDown: {
191
+ system.keydown?.(
192
+ attachEvent<KeyEvent, GS>(this.#context, {
193
+ key: keyCodeToKey(payloadByte),
194
+ }),
195
+ );
196
+ break;
197
+ }
198
+ case Enums.EventType.KeyUp:
199
+ system.keyup?.(
200
+ attachEvent<KeyEvent, GS>(this.#context, {
201
+ key: keyCodeToKey(payloadByte),
202
+ }),
203
+ );
204
+ break;
205
+ case Enums.EventType.MouseDown:
206
+ system.mousedown?.(
207
+ attachEvent<MouseButtonEvent, GS>(this.#context, {
208
+ button: mouseButtonCodeToMouseButton(payloadByte),
209
+ }),
210
+ );
211
+ break;
212
+ case Enums.EventType.MouseUp:
213
+ system.mouseup?.(
214
+ attachEvent<MouseButtonEvent, GS>(this.#context, {
215
+ button: mouseButtonCodeToMouseButton(payloadByte),
216
+ }),
217
+ );
218
+ break;
219
+ case Enums.EventType.MouseMove:
220
+ system.mousemove?.(
221
+ attachEvent<MouseMoveEvent, GS>(this.#context, {
222
+ x: payloadVec2.x,
223
+ y: payloadVec2.y,
224
+ }),
225
+ );
226
+ break;
227
+ case Enums.EventType.MouseWheel:
228
+ system.mousewheel?.(
229
+ attachEvent<MouseWheelEvent, GS>(this.#context, {
230
+ x: payloadVec2.x,
231
+ y: payloadVec2.y,
232
+ }),
233
+ );
234
+ break;
235
+ default:
236
+ throw new Error(`Unknown event type: ${eventType}`);
178
237
  }
179
- case Enums.EventType.KeyUp:
180
- system.keyup?.(
181
- attachEvent<KeyEvent, GS>(this.#context, {
182
- key: keyCodeToKey(payloadByte),
183
- }),
184
- );
185
- break;
186
- case Enums.EventType.MouseDown:
187
- system.mousedown?.(
188
- attachEvent<MouseButtonEvent, GS>(this.#context, {
189
- button: mouseButtonCodeToMouseButton(payloadByte),
190
- }),
191
- );
192
- break;
193
- case Enums.EventType.MouseUp:
194
- system.mouseup?.(
195
- attachEvent<MouseButtonEvent, GS>(this.#context, {
196
- button: mouseButtonCodeToMouseButton(payloadByte),
197
- }),
198
- );
199
- break;
200
- case Enums.EventType.MouseMove:
201
- system.mousemove?.(
202
- attachEvent<MouseMoveEvent, GS>(this.#context, {
203
- x: payloadVec2.x,
204
- y: payloadVec2.y,
205
- }),
206
- );
207
- break;
208
- case Enums.EventType.MouseWheel:
209
- system.mousewheel?.(
210
- attachEvent<MouseWheelEvent, GS>(this.#context, {
211
- x: payloadVec2.x,
212
- y: payloadVec2.y,
213
- }),
214
- );
215
- break;
216
- default:
217
- throw new Error(`Unknown event type: ${eventType}`);
238
+ // the event type u8 + padding + payload
239
+ offset += EVENT_PAYLOAD_ALIGN + EVENT_PAYLOAD_SIZE;
218
240
  }
219
- // the event type u8 + padding + payload
220
- offset += EVENT_PAYLOAD_ALIGN + EVENT_PAYLOAD_SIZE;
241
+ (this.#context as any).event = undefined;
221
242
  }
222
- (this.#context as any).event = undefined;
223
- }
224
- }
225
-
226
- setBuffer(buffer: ArrayBuffer) {
227
- this.#engineBuffer = buffer;
228
- }
243
+ },
244
+ };
229
245
  }
230
246
 
231
247
  function attachEvent<IE extends InputEvent, GS extends BloopSchema>(
package/src/mod.ts CHANGED
@@ -1,4 +1,9 @@
1
- export * from "./runtime"
2
- export * from "./bloop"
1
+ export type * as typesRuntime from "./runtime"
2
+ export type * as typesMount from "./mount"
3
+ export type * as typesBloop from "./bloop"
4
+ export type * as typesContext from "./context"
3
5
 
4
- export * as Util from "./util";
6
+ export * as Util from "./util";
7
+
8
+ export { mount } from "./mount";
9
+ export { Bloop } from "./bloop"
package/src/mount.ts ADDED
@@ -0,0 +1,86 @@
1
+ import {
2
+ DEFAULT_WASM_URL,
3
+ type EnginePointer,
4
+ type WasmEngine,
5
+ } from "@bloopjs/engine";
6
+ import { type EngineHooks, Runtime } from "./runtime";
7
+ import { assert } from "./util";
8
+
9
+ export async function mount(opts: MountOpts): Promise<MountResult> {
10
+ // https://github.com/oven-sh/bun/issues/12434
11
+ const bytes = await fetch(opts.wasmUrl ?? DEFAULT_WASM_URL)
12
+ .then((res) => res.arrayBuffer())
13
+ .catch((e) => {
14
+ console.error(
15
+ `Failed to fetch wasm at ${opts.wasmUrl ?? DEFAULT_WASM_URL}`,
16
+ e,
17
+ );
18
+ throw e;
19
+ });
20
+
21
+ // 1mb to 64mb
22
+ // use bun check:wasm to find initial memory page size
23
+ const memory = new WebAssembly.Memory({ initial: 17, maximum: 1000 });
24
+ const wasmInstantiatedSource = await WebAssembly.instantiate(bytes, {
25
+ env: {
26
+ memory,
27
+ __cb: function (system_handle: number, ptr: number) {
28
+ opts.hooks.setBuffer(memory.buffer);
29
+ opts.hooks.systemsCallback(system_handle, ptr);
30
+ },
31
+ console_log: function (ptr: EnginePointer, len: number) {
32
+ const bytes = new Uint8Array(memory.buffer, ptr, len);
33
+ const string = new TextDecoder("utf-8").decode(bytes);
34
+ console.log(string);
35
+ },
36
+ user_data_len: function () {
37
+ const serializer = opts.hooks.serialize();
38
+ return serializer ? serializer.size : 0;
39
+ },
40
+ user_data_serialize: function (ptr: EnginePointer, len: number) {
41
+ const serializer = opts.hooks.serialize();
42
+ assert(
43
+ len === serializer.size,
44
+ `user_data_serialize length mismatch, expected=${serializer.size} got=${len}`,
45
+ );
46
+ serializer.write(memory.buffer, ptr);
47
+ },
48
+ user_data_deserialize: function (ptr: EnginePointer, len: number) {
49
+ opts.hooks.deserialize(memory.buffer, ptr, len);
50
+ },
51
+ },
52
+ });
53
+ const wasm = wasmInstantiatedSource.instance.exports as WasmEngine;
54
+
55
+ wasm.initialize();
56
+
57
+ const runtime = new Runtime(wasm, memory, {
58
+ serialize: opts.hooks.serialize,
59
+ });
60
+
61
+ if (opts.startRecording ?? true) {
62
+ runtime.record();
63
+ }
64
+
65
+ return {
66
+ runtime,
67
+ wasm,
68
+ };
69
+ }
70
+
71
+ export type MountOpts = {
72
+ hooks: EngineHooks;
73
+
74
+ wasmUrl?: URL;
75
+
76
+ /**
77
+ * Whether to start recording immediately upon mount
78
+ * Defaults to true
79
+ */
80
+ startRecording?: boolean;
81
+ };
82
+
83
+ export type MountResult = {
84
+ runtime: Runtime;
85
+ wasm: WasmEngine;
86
+ };
package/src/runtime.ts CHANGED
@@ -6,44 +6,38 @@ import {
6
6
  type MouseButton,
7
7
  mouseButtonToMouseButtonCode,
8
8
  SNAPSHOT_HEADER_ENGINE_LEN_OFFSET,
9
- SNAPSHOT_HEADER_LEN,
10
9
  SNAPSHOT_HEADER_USER_LEN_OFFSET,
11
10
  TimeContext,
12
11
  type WasmEngine,
13
12
  } from "@bloopjs/engine";
14
13
  import { assert } from "./util";
15
14
 
16
- export type MountOpts = {
17
- wasmUrl?: URL;
15
+ export type EngineHooks = {
16
+ /**
17
+ * Hook to serialize some data when snapshotting
18
+ */
19
+ serialize: SerializeFn;
20
+ /**
21
+ * Hook to deserialize some data when restoring
22
+ */
23
+ deserialize: DeserializeFn;
18
24
  /**
19
25
  * A callback function to run logic for a given frame
20
26
  */
21
- systemsCallback: (system_handle: number, ptr: EnginePointer) => void;
22
-
27
+ systemsCallback: SystemsCallback;
23
28
  /**
24
29
  * Sets buffer to the latest engine memory buffer
25
30
  *
26
31
  * Note that if the engine wasm memory grows, all dataviews into the memory must be updated
27
32
  */
28
33
  setBuffer: (buffer: ArrayBuffer) => void;
29
-
30
- /**
31
- * Whether to start recording immediately upon mount
32
- * Defaults to true
33
- */
34
- startRecording?: boolean;
35
-
36
- /**
37
- * Optional hook to serialize some data when snapshotting
38
- */
39
- serialize?: SerializeFn;
40
-
41
- /**
42
- * Optional hook to deserialize some data when restoring
43
- */
44
- deserialize?: DeserializeFn;
45
34
  };
46
35
 
36
+ export type SystemsCallback = (
37
+ system_handle: number,
38
+ ptr: EnginePointer,
39
+ ) => void;
40
+
47
41
  export type SerializeFn = () => {
48
42
  size: number;
49
43
  write(buffer: ArrayBufferLike, ptr: EnginePointer): void;
@@ -198,76 +192,3 @@ export class Runtime {
198
192
  },
199
193
  };
200
194
  }
201
-
202
- export type MountResult = {
203
- runtime: Runtime;
204
- wasm: WasmEngine;
205
- };
206
-
207
- export async function mount(opts: MountOpts): Promise<MountResult> {
208
- if (
209
- (opts.serialize && !opts.deserialize) ||
210
- (!opts.serialize && opts.deserialize)
211
- ) {
212
- throw new Error("Snapshot and restore hooks must be provided together");
213
- }
214
-
215
- // https://github.com/oven-sh/bun/issues/12434
216
- const bytes = await Bun.file(opts.wasmUrl ?? DEFAULT_WASM_URL).arrayBuffer();
217
-
218
- // 1mb to 64mb
219
- // use bun check:wasm to find initial memory page size
220
- const memory = new WebAssembly.Memory({ initial: 17, maximum: 1000 });
221
- const wasmInstantiatedSource = await WebAssembly.instantiate(bytes, {
222
- env: {
223
- memory,
224
- __cb: function (system_handle: number, ptr: number) {
225
- opts.setBuffer(memory.buffer);
226
- opts.systemsCallback(system_handle, ptr);
227
- },
228
- console_log: function (ptr: EnginePointer, len: number) {
229
- const bytes = new Uint8Array(memory.buffer, ptr, len);
230
- const string = new TextDecoder("utf-8").decode(bytes);
231
- console.log(string);
232
- },
233
- user_data_len: function () {
234
- const serializer = opts.serialize ? opts.serialize() : null;
235
- return serializer ? serializer.size : 0;
236
- },
237
- user_data_serialize: function (ptr: EnginePointer, len: number) {
238
- if (!opts.serialize) {
239
- return;
240
- }
241
- const serializer = opts.serialize();
242
- if (len !== serializer.size) {
243
- throw new Error(
244
- `user_data_write length mismatch, expected=${serializer.size} got=${len}`,
245
- );
246
- }
247
- serializer.write(memory.buffer, ptr);
248
- },
249
- user_data_deserialize: function (ptr: EnginePointer, len: number) {
250
- if (!opts.deserialize) {
251
- return;
252
- }
253
- opts.deserialize(memory.buffer, ptr, len);
254
- },
255
- },
256
- });
257
- const wasm = wasmInstantiatedSource.instance.exports as WasmEngine;
258
-
259
- wasm.initialize();
260
-
261
- const runtime = new Runtime(wasm, memory, {
262
- serialize: opts.serialize,
263
- });
264
-
265
- if (opts.startRecording ?? true) {
266
- runtime.record();
267
- }
268
-
269
- return {
270
- runtime,
271
- wasm,
272
- };
273
- }