@bloopjs/bloop 0.0.12 → 0.0.14

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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloop/bloop",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts"
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@bloopjs/bloop",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/bloopgames/bloop"
8
+ },
5
9
  "exports": {
6
10
  ".": {
7
11
  "import": "./dist/mod.js",
@@ -23,7 +27,7 @@
23
27
  "@types/bun": "latest"
24
28
  },
25
29
  "dependencies": {
26
- "@bloopjs/engine": "0.0.12"
30
+ "@bloopjs/engine": "0.0.14"
27
31
  },
28
32
  "peerDependencies": {
29
33
  "typescript": "^5"
package/src/bloop.ts ADDED
@@ -0,0 +1,239 @@
1
+ import {
2
+ type EnginePointer,
3
+ Enums,
4
+ EVENT_PAYLOAD_ALIGN,
5
+ EVENT_PAYLOAD_SIZE,
6
+ EVENTS_OFFSET,
7
+ INPUT_CTX_OFFSET,
8
+ InputContext,
9
+ keyCodeToKey,
10
+ mouseButtonCodeToMouseButton,
11
+ TIME_CTX_OFFSET,
12
+ TimeContext,
13
+ } from "@bloopjs/engine";
14
+ import type { Context } from "./context";
15
+ import type { Bag } from "./data/bag";
16
+ import type { BloopSchema } from "./data/schema";
17
+ import type {
18
+ InputEvent,
19
+ KeyEvent,
20
+ MouseButtonEvent,
21
+ MouseMoveEvent,
22
+ MouseWheelEvent,
23
+ } from "./events";
24
+ import type { DeserializeFn, SerializeFn } from "./runtime";
25
+ import type { System } from "./system";
26
+
27
+ export type BloopOpts<B extends Bag> = {
28
+ /** defaults to "Game" */
29
+ name?: string;
30
+ /**
31
+ * component definitions, defaults to empty object
32
+ *
33
+ * use `defineComponents` to generate component definitions from a simple schema
34
+ */
35
+ // components?: SchemaWithLayout<CS>;
36
+ /**
37
+ * input map, defaults to empty object
38
+ */
39
+ // inputMap?: IM;
40
+ /**
41
+ * bag definition, defaults to empty object
42
+ *
43
+ */
44
+ bag?: B;
45
+ // schema: GS;
46
+ };
47
+
48
+ export class Bloop<GS extends BloopSchema> {
49
+ #systems: System<GS>[] = [];
50
+ #context: Context<GS>;
51
+ #engineBuffer: ArrayBuffer = new ArrayBuffer(0);
52
+
53
+ /**
54
+ * Bloop.create() is the way to create a new bloop instance.
55
+ */
56
+ static create<
57
+ // S extends Schema,
58
+ // RS extends ResourceSchema,
59
+ B extends Bag,
60
+ // const IM extends InputMap,
61
+ // >(opts: GameOpts<S, RS, B, IM> = {}) {
62
+ >(opts: BloopOpts<B> = {}): Bloop<MakeGS<B>> {
63
+ return new Bloop<MakeGS<B>>(opts, "dontCallMeDirectly");
64
+ }
65
+
66
+ constructor(opts: BloopOpts<GS["B"]> = {}, dontCallMeDirectly: string) {
67
+ if (dontCallMeDirectly !== "dontCallMeDirectly") {
68
+ throw new Error(
69
+ "Bloop constructor is private. Use Bloop.create() to create a new game instance.",
70
+ );
71
+ }
72
+
73
+ this.#context = {
74
+ bag: opts.bag ?? {},
75
+ time: new TimeContext(),
76
+ inputs: new InputContext(),
77
+ rawPointer: -1,
78
+ };
79
+ }
80
+
81
+ get bag(): GS["B"] {
82
+ return this.#context.bag;
83
+ }
84
+
85
+ get context(): Readonly<Context<GS>> {
86
+ return this.#context;
87
+ }
88
+
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
+ /**
124
+ * Register a system with the game loop.
125
+ *
126
+ */
127
+ system(label: string, system: System<GS>): number {
128
+ system.label ??= label;
129
+ this.#systems.push(system);
130
+ return this.#systems.length;
131
+ }
132
+
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;
178
+ }
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}`);
218
+ }
219
+ // the event type u8 + padding + payload
220
+ offset += EVENT_PAYLOAD_ALIGN + EVENT_PAYLOAD_SIZE;
221
+ }
222
+ (this.#context as any).event = undefined;
223
+ }
224
+ }
225
+
226
+ setBuffer(buffer: ArrayBuffer) {
227
+ this.#engineBuffer = buffer;
228
+ }
229
+ }
230
+
231
+ function attachEvent<IE extends InputEvent, GS extends BloopSchema>(
232
+ context: Context<GS>,
233
+ event: IE,
234
+ ): Context<GS> & { event: IE } {
235
+ (context as any).event = event;
236
+ return context as Context<GS> & { event: IE };
237
+ }
238
+
239
+ type MakeGS<B extends Bag> = BloopSchema<B>;
package/src/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InputSnapshot } from "@bloopjs/engine";
1
+ import type { EnginePointer, InputContext, TimeContext } from "@bloopjs/engine";
2
2
  import type { BloopSchema } from "./data/schema";
3
3
 
4
4
  export type Context<
@@ -8,8 +8,6 @@ export type Context<
8
8
  > = {
9
9
  /** The wrapper to the engine instance */
10
10
  // engine: Bridge<GS["CS"]>;
11
- /** The engine pointer to the injected system arguments (for advanced use cases) */
12
- // rawPointer: EnginePointer;
13
11
  /** Result of any resources requested */
14
12
  // resources: ResourcesResult<GS["RS"], R>;
15
13
  /** Result of the main query if there was one */
@@ -18,70 +16,10 @@ export type Context<
18
16
  // queries: QueriesResults<GS["CS"], QS>;
19
17
  /** The bag of values for the system */
20
18
  bag: GS["B"];
21
- /** The input snapshot */
22
- inputs: InputSnapshot;
23
19
  /** The timing information for the current frame */
24
- time: TimingSnapshot;
25
- };
26
-
27
- export type TimingSnapshot = {
28
- /** The number of seconds (usually fractional) since the last frame */
29
- dt: number;
30
- /** The total number of seconds since the engine started */
31
- time: number;
32
- /** The number of frames rendered since the engine started */
33
- frame: number;
34
-
35
- /** The current frame rate of the engine in frames per second */
36
- // fps: number;
37
- /** The number of frames rendered since the engine started */
38
- highResFrame: bigint;
39
- /** The total number of milliseconds since the engine started */
40
- highResTime: bigint;
20
+ time: TimeContext;
21
+ /** The input snapshot */
22
+ inputs: InputContext;
23
+ /** The engine pointer to the injected system arguments (for advanced use cases) */
24
+ rawPointer: EnginePointer;
41
25
  };
42
-
43
- export const TIMING_SNAPSHOT_SIZE = 32;
44
-
45
- // these functions should go in engine
46
- export function encodeTimingSnapshot(
47
- ts: TimingSnapshot,
48
- target: Uint8Array,
49
- ): number {
50
- if (target.byteLength !== TIMING_SNAPSHOT_SIZE) {
51
- throw new Error("Target buffer must be exactly 32 bytes");
52
- }
53
- const view = new DataView(
54
- target.buffer,
55
- target.byteOffset,
56
- target.byteLength,
57
- );
58
- view.setFloat32(0, ts.dt, true);
59
- view.setFloat32(4, ts.time, true);
60
- view.setBigUint64(8, ts.highResFrame, true);
61
- view.setBigUint64(16, ts.highResTime, true);
62
- view.setUint32(24, ts.frame, true);
63
- return TIMING_SNAPSHOT_SIZE;
64
- }
65
-
66
- export function decodeTimingSnapshot(
67
- source: Uint8Array,
68
- target: TimingSnapshot,
69
- ): number {
70
- if (source.byteLength !== TIMING_SNAPSHOT_SIZE) {
71
- throw new Error(
72
- `Source buffer must be exactly ${TIMING_SNAPSHOT_SIZE} bytes`,
73
- );
74
- }
75
- const view = new DataView(
76
- source.buffer,
77
- source.byteOffset,
78
- source.byteLength,
79
- );
80
- target.dt = view.getFloat32(0, true);
81
- target.time = view.getFloat32(4, true);
82
- target.highResFrame = view.getBigUint64(8, true);
83
- target.highResTime = view.getBigUint64(16, true);
84
- target.frame = view.getUint32(24, true);
85
-
86
- return TIMING_SNAPSHOT_SIZE;
87
- }
package/src/events.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Key, MouseButton } from "@bloopjs/engine";
2
+
3
+ export type InputEvent =
4
+ | KeyEvent
5
+ | MouseButtonEvent
6
+ | MouseMoveEvent
7
+ | MouseWheelEvent;
8
+
9
+ export type KeyEvent = {
10
+ key: Key;
11
+ };
12
+
13
+ export type MouseButtonEvent = {
14
+ button: MouseButton;
15
+ };
16
+
17
+ export type MouseMoveEvent = {
18
+ x: number;
19
+ y: number;
20
+ };
21
+
22
+ export type MouseWheelEvent = {
23
+ x: number;
24
+ y: number;
25
+ };
package/src/mod.ts CHANGED
@@ -1,201 +1,4 @@
1
- import type { BloopSchema } from "./data/schema";
2
- import type { Bag } from "./data/bag";
3
- import type { System } from "./system";
4
- import { EngineInputs, type PlatformEvent } from "@bloopjs/engine";
5
- import { decodeTimingSnapshot, encodeTimingSnapshot, TIMING_SNAPSHOT_SIZE, type Context } from "./context";
6
-
7
- export type BloopOpts<B extends Bag> = {
8
- /** defaults to "Game" */
9
- name?: string;
10
- /**
11
- * component definitions, defaults to empty object
12
- *
13
- * use `defineComponents` to generate component definitions from a simple schema
14
- */
15
- // components?: SchemaWithLayout<CS>;
16
- /**
17
- * input map, defaults to empty object
18
- */
19
- // inputMap?: IM;
20
- /**
21
- * bag definition, defaults to empty object
22
- *
23
- */
24
- bag?: B
25
- // schema: GS;
26
- };
27
-
28
- type MakeGS<B extends Bag> = BloopSchema<B>;
29
-
30
- export class Bloop<GS extends BloopSchema> {
31
- #systems: System<GS>[] = [];
32
- #context: Context<GS>;
33
-
34
- /**
35
- * Bloop.create() is the way to create a new bloop instance.
36
- */
37
- static create<
38
- // S extends Schema,
39
- // RS extends ResourceSchema,
40
- B extends Bag,
41
- // const IM extends InputMap,
42
- // >(opts: GameOpts<S, RS, B, IM> = {}) {
43
- >(opts: BloopOpts<B> = {}): Bloop<MakeGS<B>> {
44
- return new Bloop<MakeGS<B>>(opts, "dontCallMeDirectly");
45
- }
46
-
47
- constructor(opts: BloopOpts<GS["B"]> = {}, dontCallMeDirectly: string) {
48
- if (dontCallMeDirectly !== "dontCallMeDirectly") {
49
- throw new Error(
50
- "Bloop constructor is private. Use Bloop.create() to create a new game instance."
51
- );
52
- }
53
-
54
- this.#context = {
55
- bag: opts.bag ?? {},
56
- // todo: this should be a pointer to memory managed by the engine
57
- inputs: new EngineInputs.InputSnapshot(new DataView(new ArrayBuffer(100))),
58
- time: {
59
- dt: 0,
60
- time: 0,
61
- frame: 0,
62
- highResFrame: 0n,
63
- highResTime: 0n,
64
- },
65
- };
66
- }
67
-
68
- get bag() {
69
- return this.#context.bag;
70
- }
71
-
72
- get context() : Readonly<Context<GS>> {
73
- return this.#context;
74
- }
75
-
76
- /**
77
- * Take a snapshot of the game state
78
- * @returns linear memory representation of the game state
79
- */
80
- snapshot(): Uint8Array {
81
- const str = JSON.stringify(this.#context.bag);
82
- const encoder = new TextEncoder();
83
- const textBytes = encoder.encode(str);
84
-
85
- const size = TIMING_SNAPSHOT_SIZE + 4 + textBytes.length;
86
-
87
- const buffer = new Uint8Array(size);
88
- const view = new DataView(buffer.buffer);
89
- let offset = encodeTimingSnapshot(this.#context.time, buffer.subarray(0, TIMING_SNAPSHOT_SIZE));
90
- view.setUint32(offset, textBytes.length, true);
91
- offset += 4;
92
-
93
- buffer.set(textBytes, offset);
94
- offset += textBytes.length;
95
- return buffer;
96
- }
97
-
98
- restore(snapshot: Uint8Array) {
99
- const size = decodeTimingSnapshot(new Uint8Array(snapshot.buffer, 0, 32), this.#context.time);
100
- const view = new DataView(snapshot.buffer);
101
- let offset = size;
102
- const length = view.getUint32(0, true);
103
- offset += 4;
104
- const bagBytes = snapshot.slice(offset, offset + length);
105
- const decoder = new TextDecoder();
106
- const str = decoder.decode(bagBytes);
107
- this.#context.bag = JSON.parse(str);
108
-
109
- }
110
-
111
- /**
112
- * Register a system with the game loop.
113
- *
114
- */
115
- system(label: string, system: System<GS>): number {
116
- system.label ??= label;
117
- this.#systems.push(system);
118
- return this.#systems.length;
119
- }
120
-
121
- // todo - get data from ptr
122
- systemsCallback(ptr: number, events: PlatformEvent[], dt: number) {
123
- this.#context.inputs.update(events);
124
- const dtSeconds = dt / 1000;
125
- this.#context.time.dt = dtSeconds;
126
- this.#context.time.time += dtSeconds;
127
- this.#context.time.highResTime += BigInt(dt);
128
-
129
- for (const system of this.#systems) {
130
- system.update?.(this.#context);
131
-
132
- for (const event of events) {
133
- switch(event.type) {
134
- case "keydown":
135
- system.keydown?.({
136
- ...this.#context,
137
- event: {
138
- key: event.key,
139
- pressure: 1,
140
- }
141
- })
142
- break;
143
- case "keyup":
144
- system.keyup?.({
145
- ...this.#context,
146
- event: {
147
- key: event.key,
148
- pressure: 0,
149
- }
150
- })
151
- break;
152
- case "mousemove":
153
- system.mousemove?.({
154
- ...this.#context,
155
- event: {
156
- x: event.x,
157
- y: event.y,
158
- }
159
- })
160
- break;
161
- case "mousedown":
162
- system.mousedown?.({
163
- ...this.#context,
164
- event: {
165
- button: event.button,
166
- pressure: event.pressure,
167
- }
168
- })
169
- break;
170
- case "mouseup":
171
- system.mouseup?.({
172
- ...this.#context,
173
- event: {
174
- button: event.button,
175
- pressure: event.pressure,
176
- }
177
- })
178
- break;
179
- case "mousewheel":
180
- system.mousewheel?.({
181
- ...this.#context,
182
- event: {
183
- x: event.x,
184
- y: event.y,
185
- }
186
- })
187
- break;
188
- default:
189
- break;
190
- }
191
- }
192
- }
193
-
194
- // do this in the engine snapshot
195
- this.#context.time.frame++;
196
- this.#context.time.highResFrame += 1n;
197
-
198
- this.#context.inputs.flush();
199
- }
200
- }
1
+ export * from "./runtime"
2
+ export * from "./bloop"
201
3
 
4
+ export * as Util from "./util";