incanto 0.1.0
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/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
package/dist/net.js
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
+
import { t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
3
|
+
import { n as createAgent8Server } from "./agent8-DzU2fFyH.js";
|
|
4
|
+
import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-Dasmnurl.js";
|
|
5
|
+
//#region src/net/loopback.ts
|
|
6
|
+
/**
|
|
7
|
+
* In-memory multiplayer hub implementing the SAME kernel contract as
|
|
8
|
+
* `templates/agent8-server.js` — local split-screen demos, offline development,
|
|
9
|
+
* and headless tests run real multi-client flows without any infrastructure.
|
|
10
|
+
*/
|
|
11
|
+
var LoopbackHub = class {
|
|
12
|
+
rooms = /* @__PURE__ */ new Map();
|
|
13
|
+
accountSeq = 0;
|
|
14
|
+
roomSeq = 0;
|
|
15
|
+
entitySeq = 0;
|
|
16
|
+
/** Per-room listener registries, keyed by roomId. */
|
|
17
|
+
listeners = {
|
|
18
|
+
roomState: /* @__PURE__ */ new Map(),
|
|
19
|
+
allUsers: /* @__PURE__ */ new Map(),
|
|
20
|
+
myState: /* @__PURE__ */ new Map(),
|
|
21
|
+
collection: /* @__PURE__ */ new Map(),
|
|
22
|
+
message: /* @__PURE__ */ new Map(),
|
|
23
|
+
join: /* @__PURE__ */ new Map(),
|
|
24
|
+
leave: /* @__PURE__ */ new Map()
|
|
25
|
+
};
|
|
26
|
+
createClient(account) {
|
|
27
|
+
this.accountSeq += 1;
|
|
28
|
+
return new LoopbackTransport(this, account ?? `user${this.accountSeq}`);
|
|
29
|
+
}
|
|
30
|
+
/** @internal */
|
|
31
|
+
_room(roomId) {
|
|
32
|
+
let room = this.rooms.get(roomId);
|
|
33
|
+
if (!room) {
|
|
34
|
+
room = {
|
|
35
|
+
users: /* @__PURE__ */ new Map(),
|
|
36
|
+
state: {},
|
|
37
|
+
collections: /* @__PURE__ */ new Map()
|
|
38
|
+
};
|
|
39
|
+
this.rooms.set(roomId, room);
|
|
40
|
+
}
|
|
41
|
+
return room;
|
|
42
|
+
}
|
|
43
|
+
/** @internal The server.js kernel, in memory. */
|
|
44
|
+
_call(account, fn, args) {
|
|
45
|
+
switch (fn) {
|
|
46
|
+
case "joinRoom": {
|
|
47
|
+
const roomId = args[0] ?? `room${++this.roomSeq}`;
|
|
48
|
+
this._room(roomId).users.set(account, {});
|
|
49
|
+
this.fire(this.listeners.join.get(roomId), account);
|
|
50
|
+
this.fireAllUsers(roomId);
|
|
51
|
+
return roomId;
|
|
52
|
+
}
|
|
53
|
+
case "leaveRoom": {
|
|
54
|
+
const roomId = args[0];
|
|
55
|
+
this._room(roomId).users.delete(account);
|
|
56
|
+
this.fire(this.listeners.leave.get(roomId), account);
|
|
57
|
+
this.fireAllUsers(roomId);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
case "setMyState": {
|
|
61
|
+
const [roomId, patch] = args;
|
|
62
|
+
const room = this._room(roomId);
|
|
63
|
+
const prev = room.users.get(account) ?? {};
|
|
64
|
+
room.users.set(account, {
|
|
65
|
+
...prev,
|
|
66
|
+
...jsonClone(patch)
|
|
67
|
+
});
|
|
68
|
+
for (const entry of this.listeners.myState.get(roomId) ?? []) if (entry.account === account) entry.cb(jsonClone(room.users.get(account) ?? {}));
|
|
69
|
+
this.fireAllUsers(roomId);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
case "patchRoomState": {
|
|
73
|
+
const [roomId, patch] = args;
|
|
74
|
+
const room = this._room(roomId);
|
|
75
|
+
room.state = {
|
|
76
|
+
...room.state,
|
|
77
|
+
...jsonClone(patch)
|
|
78
|
+
};
|
|
79
|
+
this.fire(this.listeners.roomState.get(roomId), jsonClone(room.state));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
case "addEntity": {
|
|
83
|
+
const [roomId, collectionId, entity] = args;
|
|
84
|
+
const room = this._room(roomId);
|
|
85
|
+
let coll = room.collections.get(collectionId);
|
|
86
|
+
if (!coll) {
|
|
87
|
+
coll = /* @__PURE__ */ new Map();
|
|
88
|
+
room.collections.set(collectionId, coll);
|
|
89
|
+
}
|
|
90
|
+
const id = entity.__id ?? `e${++this.entitySeq}`;
|
|
91
|
+
coll.set(id, {
|
|
92
|
+
...jsonClone(entity),
|
|
93
|
+
__id: id
|
|
94
|
+
});
|
|
95
|
+
this.fireCollection(roomId, collectionId);
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
case "updateEntity": {
|
|
99
|
+
const [roomId, collectionId, id, patch] = args;
|
|
100
|
+
const coll = this._room(roomId).collections.get(collectionId);
|
|
101
|
+
const prev = coll?.get(id);
|
|
102
|
+
if (coll && prev) {
|
|
103
|
+
coll.set(id, {
|
|
104
|
+
...prev,
|
|
105
|
+
...jsonClone(patch),
|
|
106
|
+
__id: id
|
|
107
|
+
});
|
|
108
|
+
this.fireCollection(roomId, collectionId);
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
case "removeEntity": {
|
|
113
|
+
const [roomId, collectionId, id] = args;
|
|
114
|
+
this._room(roomId).collections.get(collectionId)?.delete(id);
|
|
115
|
+
this.fireCollection(roomId, collectionId);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
case "sendEvent": {
|
|
119
|
+
const [roomId, type, payload] = args;
|
|
120
|
+
for (const entry of this.listeners.message.get(roomId) ?? []) if (entry.type === type) entry.cb(jsonClone(payload));
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
default: throw new IncantoError("BAD_FORMAT", `Loopback kernel has no remote function '${fn}'. Available: joinRoom, leaveRoom, setMyState, patchRoomState, addEntity, updateEntity, removeEntity, sendEvent.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** @internal */
|
|
127
|
+
_subscribe(kind, roomId, entry) {
|
|
128
|
+
const registry = this.listeners[kind];
|
|
129
|
+
let set = registry.get(roomId);
|
|
130
|
+
if (!set) {
|
|
131
|
+
set = /* @__PURE__ */ new Set();
|
|
132
|
+
registry.set(roomId, set);
|
|
133
|
+
}
|
|
134
|
+
set.add(entry);
|
|
135
|
+
return () => set?.delete(entry);
|
|
136
|
+
}
|
|
137
|
+
/** @internal */
|
|
138
|
+
_snapshotUsers(roomId) {
|
|
139
|
+
return Object.fromEntries([...this._room(roomId).users.entries()].map(([a, s]) => [a, jsonClone(s)]));
|
|
140
|
+
}
|
|
141
|
+
/** @internal */
|
|
142
|
+
_snapshotCollection(roomId, collectionId) {
|
|
143
|
+
const coll = this._room(roomId).collections.get(collectionId);
|
|
144
|
+
return coll ? Object.fromEntries([...coll.entries()].map(([i, e]) => [i, jsonClone(e)])) : {};
|
|
145
|
+
}
|
|
146
|
+
/** @internal */
|
|
147
|
+
_snapshotRoomState(roomId) {
|
|
148
|
+
return jsonClone(this._room(roomId).state);
|
|
149
|
+
}
|
|
150
|
+
fire(set, value) {
|
|
151
|
+
for (const cb of set ?? []) cb(value);
|
|
152
|
+
}
|
|
153
|
+
fireAllUsers(roomId) {
|
|
154
|
+
const snapshot = this._snapshotUsers(roomId);
|
|
155
|
+
for (const cb of this.listeners.allUsers.get(roomId) ?? []) cb(snapshot);
|
|
156
|
+
}
|
|
157
|
+
fireCollection(roomId, collectionId) {
|
|
158
|
+
const snapshot = this._snapshotCollection(roomId, collectionId);
|
|
159
|
+
for (const entry of this.listeners.collection.get(roomId) ?? []) if (entry.id === collectionId) entry.cb(snapshot);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/** A client on a LoopbackHub. Subscriptions fire immediately with a snapshot. */
|
|
163
|
+
var LoopbackTransport = class {
|
|
164
|
+
hub;
|
|
165
|
+
account;
|
|
166
|
+
connected = false;
|
|
167
|
+
constructor(hub, account) {
|
|
168
|
+
this.hub = hub;
|
|
169
|
+
this.account = account;
|
|
170
|
+
}
|
|
171
|
+
connect() {
|
|
172
|
+
this.connected = true;
|
|
173
|
+
return Promise.resolve(true);
|
|
174
|
+
}
|
|
175
|
+
disconnect() {
|
|
176
|
+
this.connected = false;
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
}
|
|
179
|
+
remoteFunction(fn, args = []) {
|
|
180
|
+
return Promise.resolve(this.hub._call(this.account, fn, args));
|
|
181
|
+
}
|
|
182
|
+
subscribeRoomState(roomId, cb) {
|
|
183
|
+
const off = this.hub._subscribe("roomState", roomId, cb);
|
|
184
|
+
cb(this.hub._snapshotRoomState(roomId));
|
|
185
|
+
return off;
|
|
186
|
+
}
|
|
187
|
+
subscribeRoomMyState(roomId, cb) {
|
|
188
|
+
const off = this.hub._subscribe("myState", roomId, {
|
|
189
|
+
account: this.account,
|
|
190
|
+
cb
|
|
191
|
+
});
|
|
192
|
+
cb(jsonClone(this.hub._room(roomId).users.get(this.account) ?? {}));
|
|
193
|
+
return off;
|
|
194
|
+
}
|
|
195
|
+
subscribeRoomAllUserStates(roomId, cb) {
|
|
196
|
+
const off = this.hub._subscribe("allUsers", roomId, cb);
|
|
197
|
+
cb(this.hub._snapshotUsers(roomId));
|
|
198
|
+
return off;
|
|
199
|
+
}
|
|
200
|
+
subscribeRoomCollection(roomId, collectionId, cb) {
|
|
201
|
+
const off = this.hub._subscribe("collection", roomId, {
|
|
202
|
+
id: collectionId,
|
|
203
|
+
cb
|
|
204
|
+
});
|
|
205
|
+
cb(this.hub._snapshotCollection(roomId, collectionId));
|
|
206
|
+
return off;
|
|
207
|
+
}
|
|
208
|
+
onRoomMessage(roomId, type, cb) {
|
|
209
|
+
return this.hub._subscribe("message", roomId, {
|
|
210
|
+
type,
|
|
211
|
+
cb
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
onRoomUserJoin(roomId, cb) {
|
|
215
|
+
return this.hub._subscribe("join", roomId, cb);
|
|
216
|
+
}
|
|
217
|
+
onRoomUserLeave(roomId, cb) {
|
|
218
|
+
return this.hub._subscribe("leave", roomId, cb);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/net/local-game-server.ts
|
|
223
|
+
/**
|
|
224
|
+
* Run a multiplayer game's REAL agent8-sdk-v2 `Server` class **locally**, in
|
|
225
|
+
* memory, with NO cloud and NO auth — the preview/dev path.
|
|
226
|
+
*
|
|
227
|
+
* `LoopbackHub` answers only the fixed built-in room protocol (joinRoom,
|
|
228
|
+
* setMyState, collections, events). Real games add server-AUTHORITATIVE rules
|
|
229
|
+
* (`awardPoint`, `castSpell`, `$roomTick` match clocks) in `server/src/server.ts`
|
|
230
|
+
* against the v2 contexts `$sender`/`$global`/`$room`/`$lock`. LocalGameServer
|
|
231
|
+
* runs that exact class body so the whole game — client + server logic — is
|
|
232
|
+
* playable in dev before it ever touches the Agent8 platform. Swap
|
|
233
|
+
* `local.createClient(account)` for `await createAgent8Server()` to go live;
|
|
234
|
+
* nothing else changes.
|
|
235
|
+
*
|
|
236
|
+
* How it stays faithful to the isolated-vm platform:
|
|
237
|
+
* - A FRESH `Server` instance runs per request, so `this.*` never persists.
|
|
238
|
+
* - The v2 globals are injected on `globalThis` for the duration of each call
|
|
239
|
+
* (the SAME `server/src/server.ts` body that reads bare `$room`/`$sender`
|
|
240
|
+
* runs unmodified).
|
|
241
|
+
* - Calls are SERIALIZED through one queue, so a global binding can't leak across
|
|
242
|
+
* another request's `await`s (the platform isolates per request; we serialize).
|
|
243
|
+
* - `$roomTick(deltaMS, roomId)` runs only while a room has users — drive it from
|
|
244
|
+
* the engine: `engine.updated.connect((dt) => local.tick(dt * 1000))`.
|
|
245
|
+
*
|
|
246
|
+
* NOT a substitute for the cloud: no isolated-vm sandboxing, no persistence, no
|
|
247
|
+
* rate limits — a faithful FUNCTIONAL emulator for local play and tests.
|
|
248
|
+
*/
|
|
249
|
+
var LocalGameServer = class {
|
|
250
|
+
hub = new LoopbackHub();
|
|
251
|
+
serverClass;
|
|
252
|
+
/** Each client's current room — binds `$sender.roomId` on later calls. */
|
|
253
|
+
clientRooms = /* @__PURE__ */ new Map();
|
|
254
|
+
/** All remoteFunction/tick calls run one-at-a-time so injected globals never overlap. */
|
|
255
|
+
queue = Promise.resolve();
|
|
256
|
+
/** Per-key mutex backing `$lock`. */
|
|
257
|
+
locks = /* @__PURE__ */ new Map();
|
|
258
|
+
/** Persistent (process-lifetime) store backing the `$global` + `$asset` contexts. */
|
|
259
|
+
g_state = {};
|
|
260
|
+
g_userStates = /* @__PURE__ */ new Map();
|
|
261
|
+
g_collections = /* @__PURE__ */ new Map();
|
|
262
|
+
g_assets = /* @__PURE__ */ new Map();
|
|
263
|
+
g_seq = 0;
|
|
264
|
+
/** Client-side global/asset subscription registries (fanned out on mutation). */
|
|
265
|
+
subState = /* @__PURE__ */ new Set();
|
|
266
|
+
subUser = /* @__PURE__ */ new Map();
|
|
267
|
+
subColl = /* @__PURE__ */ new Map();
|
|
268
|
+
subAsset = /* @__PURE__ */ new Map();
|
|
269
|
+
subMsg = /* @__PURE__ */ new Map();
|
|
270
|
+
constructor(serverClass) {
|
|
271
|
+
this.serverClass = serverClass;
|
|
272
|
+
}
|
|
273
|
+
/** A client on this server. Distinct `account` per player (default auto-assigned). */
|
|
274
|
+
createClient(account) {
|
|
275
|
+
return new LocalGameServerTransport(this, this.hub.createClient(account));
|
|
276
|
+
}
|
|
277
|
+
collectionSnapshot(id) {
|
|
278
|
+
const c = this.g_collections.get(id);
|
|
279
|
+
return c ? Object.fromEntries([...c.entries()].map(([k, v]) => [k, jsonClone(v)])) : {};
|
|
280
|
+
}
|
|
281
|
+
assetSnapshot(acct) {
|
|
282
|
+
return Object.fromEntries(this.g_assets.get(acct) ?? []);
|
|
283
|
+
}
|
|
284
|
+
/** @internal */
|
|
285
|
+
subscribeGlobalState(cb) {
|
|
286
|
+
this.subState.add(cb);
|
|
287
|
+
cb(jsonClone(this.g_state));
|
|
288
|
+
return () => this.subState.delete(cb);
|
|
289
|
+
}
|
|
290
|
+
/** @internal */
|
|
291
|
+
subscribeGlobalUserState(account, cb) {
|
|
292
|
+
const off = addToSetMap(this.subUser, account, cb);
|
|
293
|
+
cb(jsonClone(this.g_userStates.get(account) ?? {}));
|
|
294
|
+
return off;
|
|
295
|
+
}
|
|
296
|
+
/** @internal */
|
|
297
|
+
subscribeGlobalCollection(id, cb) {
|
|
298
|
+
const off = addToSetMap(this.subColl, id, cb);
|
|
299
|
+
cb(this.collectionSnapshot(id));
|
|
300
|
+
return off;
|
|
301
|
+
}
|
|
302
|
+
/** @internal */
|
|
303
|
+
subscribeAsset(account, cb) {
|
|
304
|
+
const off = addToSetMap(this.subAsset, account, cb);
|
|
305
|
+
cb(this.assetSnapshot(account));
|
|
306
|
+
return off;
|
|
307
|
+
}
|
|
308
|
+
/** @internal Messages are events — no immediate snapshot. */
|
|
309
|
+
onGlobalMessage(account, type, cb) {
|
|
310
|
+
const entry = {
|
|
311
|
+
account,
|
|
312
|
+
cb
|
|
313
|
+
};
|
|
314
|
+
return addToSetMap(this.subMsg, type, entry);
|
|
315
|
+
}
|
|
316
|
+
fireState() {
|
|
317
|
+
const s = jsonClone(this.g_state);
|
|
318
|
+
for (const cb of [...this.subState]) cb(s);
|
|
319
|
+
}
|
|
320
|
+
fireUser(account) {
|
|
321
|
+
const s = jsonClone(this.g_userStates.get(account) ?? {});
|
|
322
|
+
for (const cb of [...this.subUser.get(account) ?? []]) cb(s);
|
|
323
|
+
}
|
|
324
|
+
fireColl(id) {
|
|
325
|
+
const r = this.collectionSnapshot(id);
|
|
326
|
+
for (const cb of [...this.subColl.get(id) ?? []]) cb(r);
|
|
327
|
+
}
|
|
328
|
+
fireAsset(account) {
|
|
329
|
+
const w = this.assetSnapshot(account);
|
|
330
|
+
for (const cb of [...this.subAsset.get(account) ?? []]) cb(w);
|
|
331
|
+
}
|
|
332
|
+
fireMsg(type, message, target) {
|
|
333
|
+
for (const e of [...this.subMsg.get(type) ?? []]) if (!target || e.account === target) e.cb(jsonClone(message));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Advance server-driven periodic logic. Calls the `Server`'s `$roomTick(deltaMS,
|
|
337
|
+
* roomId)` once per room that currently has users. No-op without a `$roomTick`.
|
|
338
|
+
*/
|
|
339
|
+
tick(deltaMS) {
|
|
340
|
+
return this.enqueue(() => this.runTick(deltaMS));
|
|
341
|
+
}
|
|
342
|
+
/** @internal Dispatch a remoteFunction (serialized). */
|
|
343
|
+
call(account, fn, args) {
|
|
344
|
+
return this.enqueue(() => this.dispatch(account, fn, args));
|
|
345
|
+
}
|
|
346
|
+
enqueue(run) {
|
|
347
|
+
const result = this.queue.then(run, run);
|
|
348
|
+
this.queue = result.then(() => void 0, () => void 0);
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
async dispatch(account, fn, args) {
|
|
352
|
+
const server = this.serverClass ? new this.serverClass() : null;
|
|
353
|
+
const method = server && typeof server[fn] === "function" ? server[fn] : null;
|
|
354
|
+
const roomId = fn === "joinRoom" ? void 0 : this.clientRooms.get(account) ?? args[0];
|
|
355
|
+
let out;
|
|
356
|
+
if (method && server) {
|
|
357
|
+
const restore = this.bindGlobals(account, roomId);
|
|
358
|
+
try {
|
|
359
|
+
out = await method.apply(server, args);
|
|
360
|
+
} finally {
|
|
361
|
+
restore();
|
|
362
|
+
}
|
|
363
|
+
} else out = this.hub._call(account, fn, args);
|
|
364
|
+
if (fn === "joinRoom" && typeof out === "string") this.clientRooms.set(account, out);
|
|
365
|
+
else if (fn === "leaveRoom") this.clientRooms.delete(account);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
async runTick(deltaMS) {
|
|
369
|
+
if (!this.serverClass) return;
|
|
370
|
+
if (typeof new this.serverClass().$roomTick !== "function") return;
|
|
371
|
+
for (const roomId of new Set(this.clientRooms.values())) {
|
|
372
|
+
const server = new this.serverClass();
|
|
373
|
+
const restore = this.bindGlobals("", roomId);
|
|
374
|
+
try {
|
|
375
|
+
await server.$roomTick.call(server, deltaMS, roomId);
|
|
376
|
+
} finally {
|
|
377
|
+
restore();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** Install the v2 globals for one dispatch; returns a restorer. */
|
|
382
|
+
bindGlobals(account, roomId) {
|
|
383
|
+
const g = globalThis;
|
|
384
|
+
const prev = {
|
|
385
|
+
$sender: g.$sender,
|
|
386
|
+
$global: g.$global,
|
|
387
|
+
$room: g.$room,
|
|
388
|
+
$lock: g.$lock,
|
|
389
|
+
$asset: g.$asset
|
|
390
|
+
};
|
|
391
|
+
g.$sender = {
|
|
392
|
+
account,
|
|
393
|
+
roomId: roomId ?? ""
|
|
394
|
+
};
|
|
395
|
+
g.$global = this.globalContext(account);
|
|
396
|
+
g.$room = this.roomContext(account, roomId);
|
|
397
|
+
g.$asset = this.assetContext(account);
|
|
398
|
+
g.$lock = (key, fn) => this.withLock(key, fn);
|
|
399
|
+
return () => {
|
|
400
|
+
g.$sender = prev.$sender;
|
|
401
|
+
g.$global = prev.$global;
|
|
402
|
+
g.$room = prev.$room;
|
|
403
|
+
g.$lock = prev.$lock;
|
|
404
|
+
g.$asset = prev.$asset;
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The `$global` context — room membership, persistent GLOBAL state/user-state/
|
|
409
|
+
* collections, room management, and global messaging. Backed by an in-memory
|
|
410
|
+
* store that survives between requests (unlike rooms, which the hub clears when
|
|
411
|
+
* empty) — matching the platform's persistent-global / ephemeral-room split.
|
|
412
|
+
*/
|
|
413
|
+
globalContext(account) {
|
|
414
|
+
const hub = this.hub;
|
|
415
|
+
const coll = (id) => {
|
|
416
|
+
let c = this.g_collections.get(id);
|
|
417
|
+
if (!c) {
|
|
418
|
+
c = /* @__PURE__ */ new Map();
|
|
419
|
+
this.g_collections.set(id, c);
|
|
420
|
+
}
|
|
421
|
+
return c;
|
|
422
|
+
};
|
|
423
|
+
const userState = (acct) => this.g_userStates.get(acct) ?? {};
|
|
424
|
+
return {
|
|
425
|
+
joinRoom: (rid) => Promise.resolve(hub._call(account, "joinRoom", [rid])),
|
|
426
|
+
leaveRoom: () => {
|
|
427
|
+
const left = this.clientRooms.get(account) ?? "";
|
|
428
|
+
hub._call(account, "leaveRoom", [left]);
|
|
429
|
+
return Promise.resolve(left);
|
|
430
|
+
},
|
|
431
|
+
getGlobalState: () => Promise.resolve(jsonClone(this.g_state)),
|
|
432
|
+
updateGlobalState: (patch) => {
|
|
433
|
+
Object.assign(this.g_state, jsonClone(patch));
|
|
434
|
+
this.fireState();
|
|
435
|
+
return Promise.resolve(jsonClone(this.g_state));
|
|
436
|
+
},
|
|
437
|
+
getUserState: (acct) => Promise.resolve(jsonClone(userState(acct))),
|
|
438
|
+
updateUserState: (acct, patch) => {
|
|
439
|
+
const next = {
|
|
440
|
+
...userState(acct),
|
|
441
|
+
...jsonClone(patch)
|
|
442
|
+
};
|
|
443
|
+
this.g_userStates.set(acct, next);
|
|
444
|
+
this.fireUser(acct);
|
|
445
|
+
return Promise.resolve(jsonClone(next));
|
|
446
|
+
},
|
|
447
|
+
getMyState: () => Promise.resolve(jsonClone(userState(account))),
|
|
448
|
+
updateMyState: (patch) => {
|
|
449
|
+
const next = {
|
|
450
|
+
...userState(account),
|
|
451
|
+
...jsonClone(patch)
|
|
452
|
+
};
|
|
453
|
+
this.g_userStates.set(account, next);
|
|
454
|
+
this.fireUser(account);
|
|
455
|
+
return Promise.resolve(jsonClone(next));
|
|
456
|
+
},
|
|
457
|
+
addCollectionItem: (id, item) => {
|
|
458
|
+
const __id = item.__id ?? `g${++this.g_seq}`;
|
|
459
|
+
const stored = {
|
|
460
|
+
...jsonClone(item),
|
|
461
|
+
__id
|
|
462
|
+
};
|
|
463
|
+
coll(id).set(__id, stored);
|
|
464
|
+
this.fireColl(id);
|
|
465
|
+
return Promise.resolve(jsonClone(stored));
|
|
466
|
+
},
|
|
467
|
+
updateCollectionItem: (id, item) => {
|
|
468
|
+
const { __id, ...patch } = item;
|
|
469
|
+
const c = coll(id);
|
|
470
|
+
const next = {
|
|
471
|
+
...c.get(__id) ?? {},
|
|
472
|
+
...jsonClone(patch),
|
|
473
|
+
__id
|
|
474
|
+
};
|
|
475
|
+
c.set(__id, next);
|
|
476
|
+
this.fireColl(id);
|
|
477
|
+
return Promise.resolve(jsonClone(next));
|
|
478
|
+
},
|
|
479
|
+
deleteCollectionItem: (id, itemId) => {
|
|
480
|
+
coll(id).delete(itemId);
|
|
481
|
+
this.fireColl(id);
|
|
482
|
+
return Promise.resolve({ __id: itemId });
|
|
483
|
+
},
|
|
484
|
+
deleteCollection: (id) => {
|
|
485
|
+
this.g_collections.delete(id);
|
|
486
|
+
this.fireColl(id);
|
|
487
|
+
return Promise.resolve(id);
|
|
488
|
+
},
|
|
489
|
+
getCollectionItem: (id, itemId) => Promise.resolve(jsonClone(coll(id).get(itemId) ?? {})),
|
|
490
|
+
getCollectionItems: (id, options) => Promise.resolve(queryItems([...coll(id).values()].map(jsonClone), options)),
|
|
491
|
+
countCollectionItems: (id, options) => Promise.resolve(queryItems([...coll(id).values()].map(jsonClone), options).length),
|
|
492
|
+
countRooms: () => Promise.resolve(new Set(this.clientRooms.values()).size),
|
|
493
|
+
getAllRoomIds: () => Promise.resolve([...new Set(this.clientRooms.values())]),
|
|
494
|
+
getAllRoomStates: () => Promise.resolve([...new Set(this.clientRooms.values())].map((r) => roomStateWithDefaults(hub, r))),
|
|
495
|
+
getRoomUserAccounts: (rid) => Promise.resolve(Object.keys(hub._snapshotUsers(rid))),
|
|
496
|
+
countRoomUsers: (rid) => Promise.resolve(Object.keys(hub._snapshotUsers(rid)).length),
|
|
497
|
+
getRoomState: (rid) => Promise.resolve(roomStateWithDefaults(hub, rid)),
|
|
498
|
+
updateRoomState: (rid, patch) => {
|
|
499
|
+
hub._call(account, "patchRoomState", [rid, patch]);
|
|
500
|
+
return Promise.resolve(roomStateWithDefaults(hub, rid));
|
|
501
|
+
},
|
|
502
|
+
getRoomUserState: (rid, acct) => Promise.resolve(hub._snapshotUsers(rid)[acct] ?? {}),
|
|
503
|
+
updateRoomUserState: (rid, acct, patch) => {
|
|
504
|
+
hub._call(acct, "setMyState", [rid, patch]);
|
|
505
|
+
return Promise.resolve(hub._snapshotUsers(rid)[acct] ?? {});
|
|
506
|
+
},
|
|
507
|
+
broadcastToAll: (type, message) => {
|
|
508
|
+
this.fireMsg(type, message);
|
|
509
|
+
},
|
|
510
|
+
sendMessageToUser: (type, acct, message) => {
|
|
511
|
+
this.fireMsg(type, message, acct);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The `$asset` context — a per-account currency ledger. `burn`/`transfer` throw
|
|
517
|
+
* on an insufficient balance (the documented pattern always checks `has` first),
|
|
518
|
+
* surfacing economy bugs in preview just as the platform would.
|
|
519
|
+
*/
|
|
520
|
+
assetContext(account) {
|
|
521
|
+
const wallet = (acct) => {
|
|
522
|
+
let w = this.g_assets.get(acct);
|
|
523
|
+
if (!w) {
|
|
524
|
+
w = /* @__PURE__ */ new Map();
|
|
525
|
+
this.g_assets.set(acct, w);
|
|
526
|
+
}
|
|
527
|
+
return w;
|
|
528
|
+
};
|
|
529
|
+
const all = (acct) => Object.fromEntries(wallet(acct));
|
|
530
|
+
return {
|
|
531
|
+
get: (assetId) => Promise.resolve(wallet(account).get(assetId) ?? 0),
|
|
532
|
+
getAll: () => Promise.resolve(all(account)),
|
|
533
|
+
has: (assetId, amount) => Promise.resolve((wallet(account).get(assetId) ?? 0) >= amount),
|
|
534
|
+
mint: (assetId, amount) => {
|
|
535
|
+
requireAmount("mint", amount);
|
|
536
|
+
wallet(account).set(assetId, (wallet(account).get(assetId) ?? 0) + amount);
|
|
537
|
+
this.fireAsset(account);
|
|
538
|
+
return Promise.resolve(all(account));
|
|
539
|
+
},
|
|
540
|
+
burn: (assetId, amount) => {
|
|
541
|
+
requireAmount("burn", amount);
|
|
542
|
+
const have = wallet(account).get(assetId) ?? 0;
|
|
543
|
+
if (have < amount) throw new IncantoError("BAD_FORMAT", `$asset.burn: insufficient '${assetId}' for ${account} (have ${have}, need ${amount}). Guard with $asset.has first.`);
|
|
544
|
+
wallet(account).set(assetId, have - amount);
|
|
545
|
+
this.fireAsset(account);
|
|
546
|
+
return Promise.resolve(all(account));
|
|
547
|
+
},
|
|
548
|
+
transfer: (toAccount, assetId, amount) => {
|
|
549
|
+
requireAmount("transfer", amount);
|
|
550
|
+
const have = wallet(account).get(assetId) ?? 0;
|
|
551
|
+
if (have < amount) throw new IncantoError("BAD_FORMAT", `$asset.transfer: insufficient '${assetId}' for ${account} (have ${have}, need ${amount}).`);
|
|
552
|
+
wallet(account).set(assetId, have - amount);
|
|
553
|
+
wallet(toAccount).set(assetId, (wallet(toAccount).get(assetId) ?? 0) + amount);
|
|
554
|
+
this.fireAsset(account);
|
|
555
|
+
this.fireAsset(toAccount);
|
|
556
|
+
return Promise.resolve(all(account));
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** The `$room` context, bound to one room — maps onto the hub's kernel primitives. */
|
|
561
|
+
roomContext(account, roomId) {
|
|
562
|
+
const rid = () => {
|
|
563
|
+
if (!roomId) throw new IncantoError("BAD_FORMAT", "$room used outside a room — joinRoom must resolve a room before $room is touched.");
|
|
564
|
+
return roomId;
|
|
565
|
+
};
|
|
566
|
+
const hub = this.hub;
|
|
567
|
+
return {
|
|
568
|
+
updateMyState: (patch) => Promise.resolve(hub._call(account, "setMyState", [rid(), patch])).then(() => hub._snapshotUsers(rid())[account] ?? {}),
|
|
569
|
+
updateRoomState: (patch) => Promise.resolve(hub._call(account, "patchRoomState", [rid(), patch])).then(() => hub._snapshotRoomState(rid())),
|
|
570
|
+
updateUserState: (acct, patch) => Promise.resolve(hub._call(acct, "setMyState", [rid(), patch])).then(() => hub._snapshotUsers(rid())[acct] ?? {}),
|
|
571
|
+
getUserState: (acct) => Promise.resolve(hub._snapshotUsers(rid())[acct] ?? {}),
|
|
572
|
+
getMyState: () => Promise.resolve(hub._snapshotUsers(rid())[account] ?? {}),
|
|
573
|
+
getRoomState: () => Promise.resolve(roomStateWithDefaults(hub, rid())),
|
|
574
|
+
getAllUserStates: () => Promise.resolve(Object.entries(hub._snapshotUsers(rid())).map(([acct, state]) => ({
|
|
575
|
+
...state,
|
|
576
|
+
account: acct
|
|
577
|
+
}))),
|
|
578
|
+
countUsers: () => Promise.resolve(Object.keys(hub._snapshotUsers(rid())).length),
|
|
579
|
+
addCollectionItem: (coll, item) => {
|
|
580
|
+
const id = hub._call(account, "addEntity", [
|
|
581
|
+
rid(),
|
|
582
|
+
coll,
|
|
583
|
+
item
|
|
584
|
+
]);
|
|
585
|
+
return Promise.resolve(hub._snapshotCollection(rid(), coll)[id] ?? { __id: id });
|
|
586
|
+
},
|
|
587
|
+
updateCollectionItem: (coll, item) => {
|
|
588
|
+
const { __id, ...patch } = item;
|
|
589
|
+
hub._call(account, "updateEntity", [
|
|
590
|
+
rid(),
|
|
591
|
+
coll,
|
|
592
|
+
__id,
|
|
593
|
+
patch
|
|
594
|
+
]);
|
|
595
|
+
return Promise.resolve(hub._snapshotCollection(rid(), coll)[__id] ?? { __id });
|
|
596
|
+
},
|
|
597
|
+
deleteCollectionItem: (coll, itemId) => {
|
|
598
|
+
hub._call(account, "removeEntity", [
|
|
599
|
+
rid(),
|
|
600
|
+
coll,
|
|
601
|
+
itemId
|
|
602
|
+
]);
|
|
603
|
+
return Promise.resolve({ __id: itemId });
|
|
604
|
+
},
|
|
605
|
+
deleteCollection: (coll) => {
|
|
606
|
+
for (const id of Object.keys(hub._snapshotCollection(rid(), coll))) hub._call(account, "removeEntity", [
|
|
607
|
+
rid(),
|
|
608
|
+
coll,
|
|
609
|
+
id
|
|
610
|
+
]);
|
|
611
|
+
return Promise.resolve(coll);
|
|
612
|
+
},
|
|
613
|
+
getCollectionItem: (coll, itemId) => Promise.resolve(hub._snapshotCollection(rid(), coll)[itemId] ?? {}),
|
|
614
|
+
getCollectionItems: (coll, options) => Promise.resolve(queryItems(Object.values(hub._snapshotCollection(rid(), coll)), options)),
|
|
615
|
+
countCollectionItems: (coll, options) => Promise.resolve(queryItems(Object.values(hub._snapshotCollection(rid(), coll)), options).length),
|
|
616
|
+
broadcastToRoom: (type, message) => {
|
|
617
|
+
hub._call(account, "sendEvent", [
|
|
618
|
+
rid(),
|
|
619
|
+
type,
|
|
620
|
+
message
|
|
621
|
+
]);
|
|
622
|
+
},
|
|
623
|
+
sendMessageToUser: (_type, _acct, _message) => {}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
withLock(key, fn) {
|
|
627
|
+
const run = (this.locks.get(key) ?? Promise.resolve()).then(() => fn());
|
|
628
|
+
this.locks.set(key, run.then(() => void 0, () => void 0));
|
|
629
|
+
return run;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
/** Add a value to a Map-of-Sets registry; returns an unsubscribe that removes it. */
|
|
633
|
+
function addToSetMap(map, key, value) {
|
|
634
|
+
let set = map.get(key);
|
|
635
|
+
if (!set) {
|
|
636
|
+
set = /* @__PURE__ */ new Set();
|
|
637
|
+
map.set(key, set);
|
|
638
|
+
}
|
|
639
|
+
set.add(value);
|
|
640
|
+
return () => {
|
|
641
|
+
set?.delete(value);
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/** A room state snapshot with the platform-maintained `roomId` + `$users` defaults. */
|
|
645
|
+
function roomStateWithDefaults(hub, roomId) {
|
|
646
|
+
return {
|
|
647
|
+
...hub._snapshotRoomState(roomId),
|
|
648
|
+
roomId,
|
|
649
|
+
$users: Object.keys(hub._snapshotUsers(roomId))
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/** Throw unless `amount` is a non-negative finite number (matches the platform; the
|
|
653
|
+
* preview must NOT be more lenient — a negative burn would otherwise ADD currency). */
|
|
654
|
+
function requireAmount(op, amount) {
|
|
655
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) throw new IncantoError("BAD_FORMAT", `$asset.${op}: amount must be a non-negative finite number (got ${amount}).`);
|
|
656
|
+
}
|
|
657
|
+
/** Compare two JSON-ish scalars for ordering. Numbers numerically, else by string. */
|
|
658
|
+
function cmp(a, b) {
|
|
659
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
660
|
+
const sa = String(a);
|
|
661
|
+
const sb = String(b);
|
|
662
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
663
|
+
}
|
|
664
|
+
function jsonEq(a, b) {
|
|
665
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
666
|
+
}
|
|
667
|
+
function matchFilter(fieldVal, op, value) {
|
|
668
|
+
switch (op) {
|
|
669
|
+
case "<": return cmp(fieldVal, value) < 0;
|
|
670
|
+
case "<=": return cmp(fieldVal, value) <= 0;
|
|
671
|
+
case ">": return cmp(fieldVal, value) > 0;
|
|
672
|
+
case ">=": return cmp(fieldVal, value) >= 0;
|
|
673
|
+
case "==": return jsonEq(fieldVal, value);
|
|
674
|
+
case "!=": return !jsonEq(fieldVal, value);
|
|
675
|
+
case "array-contains": return Array.isArray(fieldVal) && fieldVal.some((v) => jsonEq(v, value));
|
|
676
|
+
case "in": return Array.isArray(value) && value.some((v) => jsonEq(v, fieldVal));
|
|
677
|
+
case "not-in": return Array.isArray(value) && !value.some((v) => jsonEq(v, fieldVal));
|
|
678
|
+
case "array-contains-any": return Array.isArray(fieldVal) && Array.isArray(value) && fieldVal.some((v) => value.some((w) => jsonEq(v, w)));
|
|
679
|
+
default: throw new IncantoError("BAD_FORMAT", `Unknown collection filter operator '${op}'.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/** Apply CollectionOptions to a list of items (preview emulation of the platform query). */
|
|
683
|
+
function queryItems(items, options) {
|
|
684
|
+
if (!options) return items;
|
|
685
|
+
const { filters, orderBy, limit, startAfter, endBefore } = options;
|
|
686
|
+
if (filters?.length && orderBy?.length) throw new IncantoError("BAD_FORMAT", "CollectionOptions cannot combine filters and orderBy (the platform requires a composite index). Use one or the other.");
|
|
687
|
+
let out = items;
|
|
688
|
+
if (filters?.length) out = out.filter((it) => filters.every((f) => matchFilter(it[f.field], f.operator, f.value)));
|
|
689
|
+
const ob = orderBy?.[0];
|
|
690
|
+
if (ob) {
|
|
691
|
+
const { field } = ob;
|
|
692
|
+
const dir = ob.direction === "desc" ? -1 : 1;
|
|
693
|
+
out = [...out].sort((a, b) => cmp(a[field], b[field]) * dir);
|
|
694
|
+
if (startAfter !== void 0) out = out.filter((it) => cmp(it[field], startAfter) * dir > 0);
|
|
695
|
+
if (endBefore !== void 0) out = out.filter((it) => cmp(it[field], endBefore) * dir < 0);
|
|
696
|
+
}
|
|
697
|
+
if (typeof limit === "number") out = out.slice(0, limit);
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
/** Construct a local preview server. With no `server`, it equals a raw LoopbackHub. */
|
|
701
|
+
function createLocalGameServer(opts = {}) {
|
|
702
|
+
return new LocalGameServer(opts.server);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* A client of a {@link LocalGameServer}. Subscriptions read the shared in-memory
|
|
706
|
+
* store directly (same fan-out as Loopback); `remoteFunction` routes through the
|
|
707
|
+
* server's serialized dispatch so the game's `Server` class actually runs.
|
|
708
|
+
*/
|
|
709
|
+
var LocalGameServerTransport = class {
|
|
710
|
+
server;
|
|
711
|
+
loop;
|
|
712
|
+
account;
|
|
713
|
+
connected = false;
|
|
714
|
+
constructor(server, loop) {
|
|
715
|
+
this.server = server;
|
|
716
|
+
this.loop = loop;
|
|
717
|
+
this.account = loop.account;
|
|
718
|
+
}
|
|
719
|
+
async connect() {
|
|
720
|
+
this.connected = true;
|
|
721
|
+
await this.loop.connect();
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
async disconnect() {
|
|
725
|
+
this.connected = false;
|
|
726
|
+
await this.loop.disconnect();
|
|
727
|
+
}
|
|
728
|
+
remoteFunction(fn, args = []) {
|
|
729
|
+
return this.server.call(this.account, fn, args);
|
|
730
|
+
}
|
|
731
|
+
subscribeRoomState(roomId, cb) {
|
|
732
|
+
return this.loop.subscribeRoomState(roomId, cb);
|
|
733
|
+
}
|
|
734
|
+
subscribeRoomMyState(roomId, cb) {
|
|
735
|
+
return this.loop.subscribeRoomMyState(roomId, cb);
|
|
736
|
+
}
|
|
737
|
+
subscribeRoomAllUserStates(roomId, cb) {
|
|
738
|
+
return this.loop.subscribeRoomAllUserStates(roomId, cb);
|
|
739
|
+
}
|
|
740
|
+
subscribeRoomCollection(roomId, collectionId, cb) {
|
|
741
|
+
return this.loop.subscribeRoomCollection(roomId, collectionId, cb);
|
|
742
|
+
}
|
|
743
|
+
onRoomMessage(roomId, type, cb) {
|
|
744
|
+
return this.loop.onRoomMessage(roomId, type, cb);
|
|
745
|
+
}
|
|
746
|
+
onRoomUserJoin(roomId, cb) {
|
|
747
|
+
return this.loop.onRoomUserJoin(roomId, cb);
|
|
748
|
+
}
|
|
749
|
+
onRoomUserLeave(roomId, cb) {
|
|
750
|
+
return this.loop.onRoomUserLeave(roomId, cb);
|
|
751
|
+
}
|
|
752
|
+
subscribeGlobalState(cb) {
|
|
753
|
+
return this.server.subscribeGlobalState(cb);
|
|
754
|
+
}
|
|
755
|
+
subscribeGlobalMyState(cb) {
|
|
756
|
+
return this.server.subscribeGlobalUserState(this.account, cb);
|
|
757
|
+
}
|
|
758
|
+
subscribeGlobalUserState(account, cb) {
|
|
759
|
+
return this.server.subscribeGlobalUserState(account, cb);
|
|
760
|
+
}
|
|
761
|
+
subscribeGlobalCollection(collectionId, cb) {
|
|
762
|
+
return this.server.subscribeGlobalCollection(collectionId, cb);
|
|
763
|
+
}
|
|
764
|
+
subscribeAsset(account, cb) {
|
|
765
|
+
return this.server.subscribeAsset(account, cb);
|
|
766
|
+
}
|
|
767
|
+
onGlobalMessage(type, cb) {
|
|
768
|
+
return this.server.onGlobalMessage(this.account, type, cb);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
//#endregion
|
|
772
|
+
export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, registerNodesNet };
|