dredless 0.2.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 +21 -0
- package/README.md +48 -0
- package/docs/API_REFERENCE.md +2481 -0
- package/docs/BROWSER_API.md +98 -0
- package/docs/NODE_API.md +359 -0
- package/package.json +173 -0
- package/src/browser.d.ts +212 -0
- package/src/browser.js +133 -0
- package/src/client/core.js +1202 -0
- package/src/client/domains/index.js +1818 -0
- package/src/client/index.d.ts +2 -0
- package/src/client/index.js +1 -0
- package/src/compression/lz4.d.ts +1 -0
- package/src/compression/lz4.js +112 -0
- package/src/constants.js +74 -0
- package/src/crypto/chacha-native.d.ts +9 -0
- package/src/crypto/chacha-native.js +48 -0
- package/src/crypto/chacha.d.ts +25 -0
- package/src/crypto/chacha.js +274 -0
- package/src/data/item-schema.js +2235 -0
- package/src/data/official-client-data.js +156 -0
- package/src/events.js +39 -0
- package/src/index.d.ts +2277 -0
- package/src/index.js +125 -0
- package/src/network/browser-session.js +291 -0
- package/src/network/connection.d.ts +1 -0
- package/src/network/connection.js +31 -0
- package/src/network/cookies.js +85 -0
- package/src/network/http.js +34 -0
- package/src/network/index.d.ts +4 -0
- package/src/network/index.js +4 -0
- package/src/network/servers.d.ts +7 -0
- package/src/network/servers.js +133 -0
- package/src/network/session.d.ts +7 -0
- package/src/network/session.js +320 -0
- package/src/network/ships.d.ts +12 -0
- package/src/network/ships.js +73 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +8 -0
- package/src/protocol/codec/binary.d.ts +3 -0
- package/src/protocol/codec/binary.js +52 -0
- package/src/protocol/codec/msgpack.d.ts +3 -0
- package/src/protocol/codec/msgpack.js +555 -0
- package/src/protocol/inbound/frame.d.ts +1 -0
- package/src/protocol/inbound/frame.js +8 -0
- package/src/protocol/inbound/world-payload.d.ts +3 -0
- package/src/protocol/inbound/world-payload.js +21 -0
- package/src/protocol/index.d.ts +11 -0
- package/src/protocol/index.js +19 -0
- package/src/protocol/outbound/blueprint.d.ts +22 -0
- package/src/protocol/outbound/blueprint.js +36 -0
- package/src/protocol/outbound/commands.d.ts +3 -0
- package/src/protocol/outbound/commands.js +149 -0
- package/src/protocol/outbound/comms.d.ts +28 -0
- package/src/protocol/outbound/comms.js +40 -0
- package/src/protocol/outbound/inventory.d.ts +64 -0
- package/src/protocol/outbound/inventory.js +147 -0
- package/src/protocol/outbound/ship-management.d.ts +84 -0
- package/src/protocol/outbound/ship-management.js +221 -0
- package/src/protocol/outbound/sign.d.ts +18 -0
- package/src/protocol/outbound/sign.js +45 -0
- package/src/protocol/outbound/ui-config.d.ts +157 -0
- package/src/protocol/outbound/ui-config.js +578 -0
- package/src/runtime.js +36 -0
- package/src/state/generator-maze.d.ts +32 -0
- package/src/state/generator-maze.js +370 -0
- package/src/state/index.d.ts +3 -0
- package/src/state/index.js +3 -0
- package/src/state/items.js +20 -0
- package/src/state/model/decoder.js +1554 -0
- package/src/state/model/index.d.ts +33 -0
- package/src/state/model/index.js +2 -0
- package/src/state/model/loader-config.js +1020 -0
- package/src/state/model/reader.js +69 -0
- package/src/state/model/state.js +1257 -0
- package/src/state/overworld.js +72 -0
- package/src/state/tilesets.js +53 -0
- package/src/state/world/index.d.ts +29 -0
- package/src/state/world/index.js +180 -0
- package/src/state/world/state.js +297 -0
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
import * as modelDecoder from "./decoder.js";
|
|
2
|
+
import {LoaderConfigTracker} from "./loader-config.js";
|
|
3
|
+
import {navigationDestinationFromEncodedValue} from "../overworld.js";
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
ModelReader,
|
|
7
|
+
numericFields,
|
|
8
|
+
TWO_FIELD_SPEC,
|
|
9
|
+
LABEL_STATE_SPEC,
|
|
10
|
+
SIMPLE_LABEL_STATE_SPEC,
|
|
11
|
+
peekStreamInt,
|
|
12
|
+
nextValueLooksLikeTextBlob,
|
|
13
|
+
MODEL_TABLE_SPECS,
|
|
14
|
+
TABLE_78_NUMERIC_FIELDS,
|
|
15
|
+
WIRE_TAG_TABLES,
|
|
16
|
+
MASK_ONLY_TABLES,
|
|
17
|
+
ENTITY_FOOTPRINTS,
|
|
18
|
+
PLACED_ENTITY_TYPE_IDS,
|
|
19
|
+
FabricatorType,
|
|
20
|
+
FABRICATOR_TYPES,
|
|
21
|
+
HELM_TYPE_IDS,
|
|
22
|
+
COMMS_STATION_TYPE_ID,
|
|
23
|
+
COMMS_STATION_MAX_CHARGES,
|
|
24
|
+
CARGO_HATCH_TYPE_IDS,
|
|
25
|
+
THRUSTER_TYPE_IDS,
|
|
26
|
+
EXPANDO_BOX_TYPE_ID,
|
|
27
|
+
ITEM_LAUNCHER_TYPE_ID,
|
|
28
|
+
LOADER_TYPE_ID,
|
|
29
|
+
NAVIGATION_UNIT_TYPE_ID,
|
|
30
|
+
entityTypeIdFromRecord,
|
|
31
|
+
CANNON_AMMO_COLOR_ITEM_IDS,
|
|
32
|
+
CANNON_TYPE_IDS,
|
|
33
|
+
THRUSTER_FACING_NAMES,
|
|
34
|
+
DEFAULT_OVERWORLD_WARP_DURATION_SECONDS,
|
|
35
|
+
OVERWORLD_WARP_TICKS_PER_SECOND,
|
|
36
|
+
MARKER_TYPE_IDS,
|
|
37
|
+
TEAM_RANK_NAMES,
|
|
38
|
+
PLAYER_SHIP_RANKS,
|
|
39
|
+
PUSHER_MODE_NAMES,
|
|
40
|
+
SIGN_DISPLAY_MODE_NAMES,
|
|
41
|
+
SHIELD_GENERATOR_BOOST_STATE_NAMES,
|
|
42
|
+
NAVIGATION_DEFAULT_DESTINATION,
|
|
43
|
+
navigationDestinationName,
|
|
44
|
+
isNavigationDestination,
|
|
45
|
+
numberOrNull,
|
|
46
|
+
qKey,
|
|
47
|
+
blobKey,
|
|
48
|
+
cloneRecord,
|
|
49
|
+
firstRecord,
|
|
50
|
+
TABLE_78_FIELD_BITS,
|
|
51
|
+
table78DeltaRecord,
|
|
52
|
+
isSemanticLoaderDelta,
|
|
53
|
+
decodeText,
|
|
54
|
+
entityNameFromType,
|
|
55
|
+
markerTypeIdForTables,
|
|
56
|
+
itemSummary,
|
|
57
|
+
summarizeItemHolder,
|
|
58
|
+
boundedProgress,
|
|
59
|
+
summarizeFabricator,
|
|
60
|
+
summarizeCargoEjector,
|
|
61
|
+
summarizeCannon,
|
|
62
|
+
summarizeThruster,
|
|
63
|
+
summarizeHelm,
|
|
64
|
+
navigationDestinationFromRecord,
|
|
65
|
+
summarizeNavigationUnit,
|
|
66
|
+
summarizeCommsStation,
|
|
67
|
+
summarizeHealth,
|
|
68
|
+
summarizeBot,
|
|
69
|
+
isBoulderBotExclusion,
|
|
70
|
+
botClassFromState,
|
|
71
|
+
isCowardBossState,
|
|
72
|
+
isOrangeFoolBotState,
|
|
73
|
+
isZombieBossBotState,
|
|
74
|
+
isRedSentryBotState,
|
|
75
|
+
isBlueRusherBotState,
|
|
76
|
+
isRedSniperBotState,
|
|
77
|
+
isYellowHunterBotState,
|
|
78
|
+
isAquaShielderBotState,
|
|
79
|
+
isLazerEnthusiastBotState,
|
|
80
|
+
isYellowMineGuardBotState,
|
|
81
|
+
isShieldHelperBotState,
|
|
82
|
+
isShieldMasterBotState,
|
|
83
|
+
botIdentifierFromState,
|
|
84
|
+
summarizeShieldGenerator,
|
|
85
|
+
summarizeShipControl,
|
|
86
|
+
summarizeShipSize,
|
|
87
|
+
summarizeShipWorldMetadata,
|
|
88
|
+
summarizeItemCrate,
|
|
89
|
+
scaledSizeSummary,
|
|
90
|
+
summarizeExpandoBox,
|
|
91
|
+
summarizeHoverOutline,
|
|
92
|
+
summarizeMapMarker,
|
|
93
|
+
summarizeDockingSpring,
|
|
94
|
+
summarizeHugeThruster,
|
|
95
|
+
summarizeLoader,
|
|
96
|
+
summarizeCargoHatch,
|
|
97
|
+
summarizePusher,
|
|
98
|
+
summarizePusherBeam,
|
|
99
|
+
normalizeDegrees,
|
|
100
|
+
summarizeItemLauncher,
|
|
101
|
+
summarizeSign,
|
|
102
|
+
summarizeShieldProjector,
|
|
103
|
+
summarizeSpawnPoint,
|
|
104
|
+
summarizeDoor,
|
|
105
|
+
enumValueName,
|
|
106
|
+
colorToCss,
|
|
107
|
+
previewActionName,
|
|
108
|
+
bitOffsets,
|
|
109
|
+
summarizeBlueprintPreview,
|
|
110
|
+
summarizePlayerPreview,
|
|
111
|
+
summarizePlayer,
|
|
112
|
+
patronTierName,
|
|
113
|
+
mergeContents,
|
|
114
|
+
MAX_HOVER_OUTLINE_FOOTPRINT_CELLS,
|
|
115
|
+
entityFootprint,
|
|
116
|
+
entityLabel,
|
|
117
|
+
entityCategory,
|
|
118
|
+
BLOCK_KEY_OFFSET,
|
|
119
|
+
BLOCK_KEY_STRIDE,
|
|
120
|
+
blockKey,
|
|
121
|
+
EMPTY_TABLE,
|
|
122
|
+
MAX_RETAINED_REMOVALS,
|
|
123
|
+
MAX_RETAINED_ERRORS,
|
|
124
|
+
pushCapped,
|
|
125
|
+
defineLazyProperty,
|
|
126
|
+
} = modelDecoder;
|
|
127
|
+
|
|
128
|
+
export class ModelState {
|
|
129
|
+
#loaderConfig = new LoaderConfigTracker();
|
|
130
|
+
#blueprintItems = null;
|
|
131
|
+
#helmOccupied = new Map();
|
|
132
|
+
#commsStationOccupied = new Map();
|
|
133
|
+
#navigationUnitAutoWarp = new Map();
|
|
134
|
+
|
|
135
|
+
constructor({isOverworld = null} = {}) {
|
|
136
|
+
this.isOverworld = isOverworld == null ? null : Boolean(isOverworld);
|
|
137
|
+
this.generation = null;
|
|
138
|
+
this.tables = new Map();
|
|
139
|
+
this.removedEntities = [];
|
|
140
|
+
this.lastUpdate = null;
|
|
141
|
+
this.errors = [];
|
|
142
|
+
// Retained arrays are capped; the totals remain exact.
|
|
143
|
+
this.totalRemovedCount = 0;
|
|
144
|
+
this.totalErrorCount = 0;
|
|
145
|
+
this._derived = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setWorldKind(isOverworld) {
|
|
149
|
+
const next = isOverworld == null ? null : Boolean(isOverworld);
|
|
150
|
+
if (this.isOverworld === next) return;
|
|
151
|
+
this.isOverworld = next;
|
|
152
|
+
this.#invalidateDerived();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
table(id) {
|
|
156
|
+
// Shared empty map: #summarizeEntity performs ~32 record() lookups per
|
|
157
|
+
// entity, most against absent tables, and each miss used to allocate.
|
|
158
|
+
return this.tables.get(Number(id)) || EMPTY_TABLE;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
record(tableId, entityId) {
|
|
162
|
+
return this.table(tableId).get(Number(entityId)) || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
entity(entityId) {
|
|
166
|
+
const id = Number(entityId);
|
|
167
|
+
if (!Number.isFinite(id)) return null;
|
|
168
|
+
return this.#derivedState().entitiesById.get(id) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
entities() {
|
|
172
|
+
return this.#derivedState().entities.slice();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
blocks() {
|
|
176
|
+
this.#derivedState();
|
|
177
|
+
return this.#derivedBlocks().slice();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// O(1) point lookup. Callers previously had to materialise and scan the whole
|
|
181
|
+
// block list to answer this.
|
|
182
|
+
blockAt(x, y) {
|
|
183
|
+
this.#derivedState();
|
|
184
|
+
return this.#blocksIndex().get(blockKey(x, y)) || null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
apply(bytes, {full = false} = {}) {
|
|
188
|
+
this.#blueprintItems = null;
|
|
189
|
+
const reader = new ModelReader(bytes);
|
|
190
|
+
const update = {
|
|
191
|
+
generation: null,
|
|
192
|
+
full: Boolean(full),
|
|
193
|
+
changedEntities: new Set(),
|
|
194
|
+
sections: [],
|
|
195
|
+
removals: [],
|
|
196
|
+
unknownTags: [],
|
|
197
|
+
error: null
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
update.generation = reader.readStreamInt();
|
|
202
|
+
this.generation = update.generation;
|
|
203
|
+
|
|
204
|
+
while (reader.remaining > 0) {
|
|
205
|
+
if (reader.trailingZeroOnly()) break;
|
|
206
|
+
let tag;
|
|
207
|
+
try {
|
|
208
|
+
tag = reader.readStreamInt();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error(`model section tag offset ${reader.offset}: ${error.message}`);
|
|
211
|
+
}
|
|
212
|
+
if (tag === 0) break;
|
|
213
|
+
if (tag === 57005) {
|
|
214
|
+
update.removals.push(...this.#readRemovals(reader));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const tableId = WIRE_TAG_TABLES.get(tag);
|
|
219
|
+
if (tableId == null) {
|
|
220
|
+
update.unknownTags.push({tag, offset: reader.offset});
|
|
221
|
+
// Some live captures end with a terminal, empty section tag we do not
|
|
222
|
+
// yet have a table mapping for. If the rest of the packet is just
|
|
223
|
+
// zero terminators, keep the decoded sections and stop cleanly rather
|
|
224
|
+
// than turning the whole frame into a decode error.
|
|
225
|
+
if (reader.trailingZeroOnly()) break;
|
|
226
|
+
throw new Error(`unsupported model_data section tag ${tag}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const section = this.#readSection(reader, tag, tableId);
|
|
230
|
+
update.sections.push(section);
|
|
231
|
+
for (const record of section.records) update.changedEntities.add(record.entity);
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
update.error = error;
|
|
235
|
+
this.totalErrorCount += 1;
|
|
236
|
+
pushCapped(this.errors, {message: error.message, generation: update.generation}, MAX_RETAINED_ERRORS);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.#remapIndexedLoaderConfig(update);
|
|
240
|
+
this.#updateLoaderConfig(update);
|
|
241
|
+
this.#updateNavigationUnitAutoWarp(update);
|
|
242
|
+
this.#updateHelmOccupancy(update);
|
|
243
|
+
this.#updateCommsStationOccupancy(update);
|
|
244
|
+
this.lastUpdate = update;
|
|
245
|
+
this.#updateDerived(update);
|
|
246
|
+
return update;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// `blocks` is opt-in: materialising and sorting the per-cell index is the most
|
|
250
|
+
// expensive part of a snapshot, and most callers never read it. Mirrors how
|
|
251
|
+
// `includeTiles` already works on WorldState.snapshot().
|
|
252
|
+
snapshot({includeTables = false, includeBlocks = false} = {}) {
|
|
253
|
+
const derived = this.#derivedState();
|
|
254
|
+
return {
|
|
255
|
+
generation: this.generation,
|
|
256
|
+
tableCount: this.tables.size,
|
|
257
|
+
entityCount: derived.entityCount,
|
|
258
|
+
removedEntities: this.removedEntities.slice(-50),
|
|
259
|
+
lastUpdate: this.lastUpdate ? summarizeUpdate(this.lastUpdate) : null,
|
|
260
|
+
errors: this.errors.slice(-10),
|
|
261
|
+
entities: derived.entities.slice(),
|
|
262
|
+
blocks: includeBlocks ? this.#derivedBlocks().slice() : undefined,
|
|
263
|
+
transforms: derived.transforms.slice(),
|
|
264
|
+
players: derived.players.slice(),
|
|
265
|
+
shipControls: derived.shipControls.slice(),
|
|
266
|
+
machines: this.#machinesSnapshot(derived.machines),
|
|
267
|
+
tables: includeTables ? this.tablesSnapshot() : derived.tableSummaries.slice()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
tablesSnapshot() {
|
|
272
|
+
return [...this.tables.entries()].map(([id, records]) => ({
|
|
273
|
+
id,
|
|
274
|
+
name: MODEL_TABLE_SPECS.get(id)?.name || null,
|
|
275
|
+
records: [...records.entries()].map(([entity, record]) => ({entity, ...cloneRecord(record)}))
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
transforms() {
|
|
280
|
+
return this.#derivedState().transforms.slice();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
itemHolders() {
|
|
284
|
+
return this.#records(6).map((entry) => summarizeItemHolder(entry.entity, entry));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fabricators() {
|
|
288
|
+
return this.#records(53).map((entry) => summarizeFabricator(entry.entity, entry, this.record(6, entry.entity), this.record(7, entry.entity)));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
players() {
|
|
292
|
+
return this.#derivedState().players.slice();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
shipControls() {
|
|
296
|
+
return this.#derivedState().shipControls.slice();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
shipMetadata() {
|
|
300
|
+
if (this.isOverworld) return null;
|
|
301
|
+
const lockdownRecord = firstRecord(this.table(11));
|
|
302
|
+
const shipRecord = firstRecord(this.table(16), (record) => record?.blob20 && typeof record?.q32 === "number" && typeof record?.q36 === "number");
|
|
303
|
+
return summarizeShipWorldMetadata(lockdownRecord, shipRecord);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
machines() {
|
|
307
|
+
return this.#machinesSnapshot(this.#derivedState().machines);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#machinesSnapshot(machines) {
|
|
311
|
+
return {
|
|
312
|
+
itemHolders: machines.itemHolders.slice(),
|
|
313
|
+
fabricators: machines.fabricators.slice(),
|
|
314
|
+
cargoEjectors: machines.cargoEjectors.slice(),
|
|
315
|
+
cannons: machines.cannons.slice(),
|
|
316
|
+
thrusters: machines.thrusters.slice(),
|
|
317
|
+
pushers: machines.pushers.slice(),
|
|
318
|
+
pusherBeams: machines.pusherBeams.slice(),
|
|
319
|
+
launchers: machines.launchers.slice(),
|
|
320
|
+
health: machines.health.slice(),
|
|
321
|
+
loaders: machines.loaders.slice(),
|
|
322
|
+
cargoHatches: machines.cargoHatches.slice(),
|
|
323
|
+
navigationUnits: machines.navigationUnits.slice(),
|
|
324
|
+
commsStations: machines.commsStations.slice(),
|
|
325
|
+
fluidTanks: machines.fluidTanks.slice(),
|
|
326
|
+
shieldGenerators: machines.shieldGenerators.slice(),
|
|
327
|
+
shieldProjectors: machines.shieldProjectors.slice(),
|
|
328
|
+
helms: machines.helms.slice(),
|
|
329
|
+
signs: machines.signs.slice(),
|
|
330
|
+
spawnPoints: machines.spawnPoints.slice(),
|
|
331
|
+
doors: machines.doors.slice(),
|
|
332
|
+
expandoBoxes: machines.expandoBoxes.slice()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#invalidateDerived() {
|
|
337
|
+
this._derived = null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#updateDerived(update) {
|
|
341
|
+
if (!this._derived) return;
|
|
342
|
+
if (!update.removals.length && !update.changedEntities.size) return;
|
|
343
|
+
|
|
344
|
+
for (const section of update.sections) {
|
|
345
|
+
for (const record of section.records || []) this.#addDerivedEntityTable(record.entity, section.table);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (update.sections.some((section) => section.table === 12)) {
|
|
349
|
+
for (const entityId of this.table(55).keys()) update.changedEntities.add(entityId);
|
|
350
|
+
}
|
|
351
|
+
for (const entityId of update.removals) this.#removeDerivedEntity(entityId);
|
|
352
|
+
for (const entityId of update.changedEntities) this.#refreshDerivedEntity(entityId);
|
|
353
|
+
this._derived.summariesDirty = true;
|
|
354
|
+
this._derived.tableSummariesDirty = true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#updateLoaderConfig(update) {
|
|
358
|
+
const seenEntities = new Set();
|
|
359
|
+
for (const section of update.sections || []) {
|
|
360
|
+
if (section.table !== 78) continue;
|
|
361
|
+
for (const changed of section.records || []) {
|
|
362
|
+
if (changed.indexedLoaderConfig) {
|
|
363
|
+
this.#loaderConfig.updateIndexedSnapshotRecord(null, changed.entity, changed.record, changed.mask, changed.cumulativeRecord, changed.configEntity);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const typeId = entityTypeIdFromRecord(this.record(7, changed.entity));
|
|
367
|
+
this.#loaderConfig.updateRecord(null, changed.entity, changed.record ?? this.record(78, changed.entity), changed.mask, changed.previous, {
|
|
368
|
+
repeatedInUpdate: seenEntities.has(changed.entity),
|
|
369
|
+
allowSparseBaseline: typeId === LOADER_TYPE_ID,
|
|
370
|
+
repeatedInSection: changed.repeatedInSection,
|
|
371
|
+
semanticSnapshot: Boolean(update.full),
|
|
372
|
+
semanticDelta: isSemanticLoaderDelta(changed, update.full),
|
|
373
|
+
deltaRecord: table78DeltaRecord(changed)
|
|
374
|
+
});
|
|
375
|
+
seenEntities.add(changed.entity);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#remapIndexedLoaderConfig(update) {
|
|
381
|
+
if (!update.full || update.error) return;
|
|
382
|
+
const rows = [];
|
|
383
|
+
for (const section of update.sections || []) {
|
|
384
|
+
if (section.table !== 78) continue;
|
|
385
|
+
for (const changed of section.records || []) rows.push(changed);
|
|
386
|
+
}
|
|
387
|
+
if (!rows.length) return;
|
|
388
|
+
|
|
389
|
+
const loaderIds = [...this.table(7).entries()]
|
|
390
|
+
.filter(([, record]) => entityTypeIdFromRecord(record) === LOADER_TYPE_ID)
|
|
391
|
+
.map(([entity]) => entity)
|
|
392
|
+
.sort((a, b) => a - b);
|
|
393
|
+
if (loaderIds.length !== rows.length) return;
|
|
394
|
+
if (rows.every((row, index) => row.entity === loaderIds[index])) return;
|
|
395
|
+
|
|
396
|
+
const remappedTable = new Map();
|
|
397
|
+
for (const [index, changed] of rows.entries()) {
|
|
398
|
+
const rawRecord = table78DeltaRecord(changed);
|
|
399
|
+
changed.configEntity = changed.entity;
|
|
400
|
+
changed.entity = loaderIds[index];
|
|
401
|
+
changed.cumulativeRecord = cloneRecord(changed.record || {});
|
|
402
|
+
changed.record = cloneRecord(rawRecord);
|
|
403
|
+
delete changed.previous;
|
|
404
|
+
changed.indexedLoaderConfig = true;
|
|
405
|
+
remappedTable.set(changed.entity, cloneRecord(rawRecord));
|
|
406
|
+
update.changedEntities.add(changed.entity);
|
|
407
|
+
}
|
|
408
|
+
this.tables.set(78, remappedTable);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#updateNavigationUnitAutoWarp(update) {
|
|
412
|
+
const trackedBeforeUpdate = new Set(this.#navigationUnitAutoWarp.keys());
|
|
413
|
+
const initializedThisUpdate = new Set();
|
|
414
|
+
|
|
415
|
+
for (const section of update.sections || []) {
|
|
416
|
+
if (section.table !== 78) continue;
|
|
417
|
+
for (const changed of section.records || []) {
|
|
418
|
+
const typeId = entityTypeIdFromRecord(this.record(7, changed.entity));
|
|
419
|
+
if (typeId !== NAVIGATION_UNIT_TYPE_ID) continue;
|
|
420
|
+
|
|
421
|
+
const record = this.record(78, changed.entity);
|
|
422
|
+
if (!record) continue;
|
|
423
|
+
|
|
424
|
+
if (!trackedBeforeUpdate.has(changed.entity)) {
|
|
425
|
+
if (initializedThisUpdate.has(changed.entity)) continue;
|
|
426
|
+
const q32Destination = navigationDestinationFromEncodedValue(record.q32);
|
|
427
|
+
const q36Destination = navigationDestinationFromEncodedValue(record.q36);
|
|
428
|
+
const q24Destination = navigationDestinationFromEncodedValue(record.q24);
|
|
429
|
+
const q20Destination = navigationDestinationFromEncodedValue(record.q20);
|
|
430
|
+
const q32DestinationRow = (changed.mask & 9) === 9 && record.q20 === 0 && record.q24 == null && q32Destination != null;
|
|
431
|
+
const destinationBase = q32DestinationRow
|
|
432
|
+
? q32Destination
|
|
433
|
+
: (changed.mask & 17) === 17 && record.q20 === 0 && record.q24 == null && record.q32 == null && q36Destination != null
|
|
434
|
+
? q36Destination
|
|
435
|
+
: record.q20 === 0 && q24Destination != null
|
|
436
|
+
? q24Destination
|
|
437
|
+
: null;
|
|
438
|
+
const q36DestinationRow = destinationBase === q36Destination && q36Destination != null;
|
|
439
|
+
const pureDestinationRow = changed.mask === 1 && q20Destination != null;
|
|
440
|
+
const noCaptainsOnlyRow = changed.mask === 16 && record.q36 === 0;
|
|
441
|
+
this.#navigationUnitAutoWarp.set(changed.entity, {
|
|
442
|
+
destination: navigationDestinationFromRecord(record, changed.mask),
|
|
443
|
+
destinationBase,
|
|
444
|
+
shieldFailure: pureDestinationRow || q36DestinationRow || noCaptainsOnlyRow ? true : q32DestinationRow ? false : record.q32 == null ? (record.q24 == null ? null : true) : false,
|
|
445
|
+
noCaptains: q32DestinationRow && changed.mask === 9
|
|
446
|
+
? true
|
|
447
|
+
: pureDestinationRow ? true
|
|
448
|
+
: noCaptainsOnlyRow ? false
|
|
449
|
+
: q36DestinationRow ? false : record.q36 == null ? (record.q24 == null ? null : true) : false
|
|
450
|
+
});
|
|
451
|
+
initializedThisUpdate.add(changed.entity);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const current = this.#navigationUnitAutoWarp.get(changed.entity);
|
|
456
|
+
if (!current) continue;
|
|
457
|
+
const q32Destination = navigationDestinationFromEncodedValue(record.q32);
|
|
458
|
+
const q36Destination = navigationDestinationFromEncodedValue(record.q36);
|
|
459
|
+
const q20Destination = navigationDestinationFromEncodedValue(record.q20);
|
|
460
|
+
const q32DestinationRow = (changed.mask & 9) === 9 && record.q20 === 0 && record.q24 == null && q32Destination != null;
|
|
461
|
+
const q36DestinationRow = (changed.mask & 17) === 17 && record.q20 === 0 && record.q24 == null && record.q32 == null && q36Destination != null;
|
|
462
|
+
const baselineRow = (changed.mask & 27) === 27 && record.q20 === 0 && record.q24 != null && record.q32 != null && record.q36 != null;
|
|
463
|
+
if (q32DestinationRow) {
|
|
464
|
+
current.destination = q32Destination;
|
|
465
|
+
current.destinationBase = q32Destination;
|
|
466
|
+
current.shieldFailure = false;
|
|
467
|
+
if (record.q36 != null) current.noCaptains = record.q36 !== 0;
|
|
468
|
+
else if (changed.mask === 9) current.noCaptains = true;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (q36DestinationRow) {
|
|
472
|
+
current.destination = q36Destination;
|
|
473
|
+
current.destinationBase = q36Destination;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (baselineRow) {
|
|
477
|
+
current.shieldFailure = false;
|
|
478
|
+
current.noCaptains = record.q36 !== 0;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (changed.mask === 1) {
|
|
482
|
+
const relativeDestination = current.destinationBase == null || typeof record.q20 !== "number"
|
|
483
|
+
? null
|
|
484
|
+
: current.destinationBase + record.q20;
|
|
485
|
+
current.destination = q20Destination ?? (isNavigationDestination(relativeDestination) ? relativeDestination : current.destination);
|
|
486
|
+
} else if ((changed.mask & 1) && record.q20 != null && record.q20 !== 0) {
|
|
487
|
+
current.destination = q20Destination ?? (isNavigationDestination(record.q20) ? record.q20 : current.destination);
|
|
488
|
+
}
|
|
489
|
+
if (changed.mask & 8) current.shieldFailure = !current.shieldFailure;
|
|
490
|
+
if (changed.mask & 16) current.noCaptains = !current.noCaptains;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#updateHelmOccupancy(update) {
|
|
496
|
+
const table7 = update.sections.find((section) => section.table === 7);
|
|
497
|
+
if (!table7) return;
|
|
498
|
+
|
|
499
|
+
const changedPilotState = new Map();
|
|
500
|
+
for (const section of update.sections) {
|
|
501
|
+
if (section.table !== 55) continue;
|
|
502
|
+
for (const changed of section.records) {
|
|
503
|
+
if (!(changed.mask & 16)) continue;
|
|
504
|
+
const player = this.record(55, changed.entity);
|
|
505
|
+
changedPilotState.set(changed.entity, Boolean(player?.q107));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const activePilotCount = [...this.table(55).values()].filter((record) => record?.q107).length;
|
|
510
|
+
for (const changed of table7.records) {
|
|
511
|
+
const record = this.record(7, changed.entity);
|
|
512
|
+
const typeId = entityTypeIdFromRecord(record);
|
|
513
|
+
if (!HELM_TYPE_IDS.has(typeId)) continue;
|
|
514
|
+
|
|
515
|
+
if (activePilotCount > 0 && record?.q20 === 0 && HELM_TYPE_IDS.has(Number(record?.q32))) {
|
|
516
|
+
this.#helmOccupied.set(changed.entity, true);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!(changed.mask & 8) || changedPilotState.size === 0) continue;
|
|
520
|
+
this.#helmOccupied.set(changed.entity, [...changedPilotState.values()].some(Boolean));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#updateCommsStationOccupancy(update) {
|
|
525
|
+
const table7 = update.sections.find((section) => section.table === 7);
|
|
526
|
+
if (!table7) return;
|
|
527
|
+
|
|
528
|
+
for (const changed of table7.records) {
|
|
529
|
+
const record = this.record(7, changed.entity);
|
|
530
|
+
const typeId = entityTypeIdFromRecord(record);
|
|
531
|
+
if (typeId !== COMMS_STATION_TYPE_ID) continue;
|
|
532
|
+
|
|
533
|
+
const hasAbsoluteTypeState = update.full || (changed.mask & 1);
|
|
534
|
+
if (hasAbsoluteTypeState && record?.q20 === 0 && Number(record?.q32) === COMMS_STATION_TYPE_ID) {
|
|
535
|
+
this.#commsStationOccupied.set(changed.entity, true);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (hasAbsoluteTypeState && Number(record?.q20) === COMMS_STATION_TYPE_ID) {
|
|
540
|
+
this.#commsStationOccupied.set(changed.entity, false);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (changed.mask & 8) {
|
|
544
|
+
this.#commsStationOccupied.set(changed.entity, !this.#commsStationOccupied.get(changed.entity));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#derivedState() {
|
|
550
|
+
if (this._derived) {
|
|
551
|
+
this.#ensureDerivedSummaries();
|
|
552
|
+
return this._derived;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const entityIds = [];
|
|
556
|
+
const seenEntities = new Set();
|
|
557
|
+
const entityTableIds = new Map();
|
|
558
|
+
const tableSummaries = [];
|
|
559
|
+
|
|
560
|
+
for (const [tableId, records] of this.tables.entries()) {
|
|
561
|
+
const name = MODEL_TABLE_SPECS.get(tableId)?.name || null;
|
|
562
|
+
tableSummaries.push({id: tableId, name, count: records.size});
|
|
563
|
+
for (const entity of records.keys()) {
|
|
564
|
+
if (!entityTableIds.has(entity)) entityTableIds.set(entity, []);
|
|
565
|
+
entityTableIds.get(entity).push(tableId);
|
|
566
|
+
if (!seenEntities.has(entity)) {
|
|
567
|
+
seenEntities.add(entity);
|
|
568
|
+
entityIds.push(entity);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
entityIds.sort((a, b) => a - b);
|
|
574
|
+
const entities = entityIds.map((entityId) => this.#summarizeEntity(entityId, this.#tableRowsForEntity(entityId)));
|
|
575
|
+
const entitiesById = new Map(entities.map((entity) => [entity.entity, entity]));
|
|
576
|
+
|
|
577
|
+
this._derived = {
|
|
578
|
+
entityCount: entityIds.length,
|
|
579
|
+
entityIds,
|
|
580
|
+
entityTableIds,
|
|
581
|
+
entitiesById,
|
|
582
|
+
blocksByKey: null,
|
|
583
|
+
blocks: null,
|
|
584
|
+
blocksDirty: true,
|
|
585
|
+
tableSummaries,
|
|
586
|
+
summariesDirty: true,
|
|
587
|
+
tableSummariesDirty: false
|
|
588
|
+
};
|
|
589
|
+
this.#refreshDerivedSummaries();
|
|
590
|
+
return this._derived;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#tableRowsForEntity(entityId) {
|
|
594
|
+
const rows = [];
|
|
595
|
+
const tableIds = this._derived?.entityTableIds?.get(entityId) || this.tables.keys();
|
|
596
|
+
for (const tableId of tableIds) {
|
|
597
|
+
const record = this.tables.get(tableId)?.get(entityId);
|
|
598
|
+
if (record) rows.push({tableId, name: MODEL_TABLE_SPECS.get(tableId)?.name || null, record});
|
|
599
|
+
}
|
|
600
|
+
return rows;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#addDerivedEntityTable(entityId, tableId) {
|
|
604
|
+
const derived = this._derived;
|
|
605
|
+
if (!derived) return;
|
|
606
|
+
let tableIds = derived.entityTableIds.get(entityId);
|
|
607
|
+
if (!tableIds) {
|
|
608
|
+
tableIds = [];
|
|
609
|
+
derived.entityTableIds.set(entityId, tableIds);
|
|
610
|
+
}
|
|
611
|
+
if (!tableIds.includes(tableId)) insertSorted(tableIds, tableId);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#removeDerivedEntity(entityId) {
|
|
615
|
+
const derived = this._derived;
|
|
616
|
+
const old = derived.entitiesById.get(entityId);
|
|
617
|
+
if (old) this.#removeEntityFromDerivedBlocks(derived, old);
|
|
618
|
+
derived.entitiesById.delete(entityId);
|
|
619
|
+
derived.entityTableIds.delete(entityId);
|
|
620
|
+
const index = derived.entityIds.indexOf(entityId);
|
|
621
|
+
if (index >= 0) derived.entityIds.splice(index, 1);
|
|
622
|
+
derived.entityCount = derived.entityIds.length;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#refreshDerivedEntity(entityId) {
|
|
626
|
+
const rows = this.#tableRowsForEntity(entityId);
|
|
627
|
+
if (!rows.length) {
|
|
628
|
+
this.#removeDerivedEntity(entityId);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const derived = this._derived;
|
|
633
|
+
const old = derived.entitiesById.get(entityId);
|
|
634
|
+
if (old) this.#removeEntityFromDerivedBlocks(derived, old);
|
|
635
|
+
|
|
636
|
+
const next = this.#summarizeEntity(entityId, rows);
|
|
637
|
+
derived.entitiesById.set(entityId, next);
|
|
638
|
+
if (!derived.entityIds.includes(entityId)) insertSorted(derived.entityIds, entityId);
|
|
639
|
+
this.#addEntityToDerivedBlocks(derived, next);
|
|
640
|
+
derived.entityCount = derived.entityIds.length;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#tableSummaries() {
|
|
644
|
+
return [...this.tables.entries()].map(([id, records]) => ({
|
|
645
|
+
id,
|
|
646
|
+
name: MODEL_TABLE_SPECS.get(id)?.name || null,
|
|
647
|
+
count: records.size
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// The per-cell block index is the most expensive derived product to build --
|
|
652
|
+
// it walks every occupied cell of every entity. Most consumers never ask for
|
|
653
|
+
// blocks, so it is constructed on first use rather than during the rebuild.
|
|
654
|
+
#blocksIndex() {
|
|
655
|
+
const derived = this._derived;
|
|
656
|
+
if (derived.blocksByKey) return derived.blocksByKey;
|
|
657
|
+
const blocks = new Map();
|
|
658
|
+
for (const entityId of derived.entityIds) {
|
|
659
|
+
const entity = derived.entitiesById.get(entityId);
|
|
660
|
+
if (entity) this.#addEntityToBlockMap(blocks, entity);
|
|
661
|
+
}
|
|
662
|
+
derived.blocksByKey = blocks;
|
|
663
|
+
derived.blocksDirty = true;
|
|
664
|
+
return blocks;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#addEntityToDerivedBlocks(derived, entity) {
|
|
668
|
+
// Nothing to maintain until something has asked for the index; it will be
|
|
669
|
+
// built from current entities on demand.
|
|
670
|
+
if (!derived.blocksByKey) return;
|
|
671
|
+
this.#addEntityToBlockMap(derived.blocksByKey, entity);
|
|
672
|
+
derived.blocksDirty = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#addEntityToBlockMap(blocks, entity) {
|
|
676
|
+
if (!entity.transform || !Number.isFinite(entity.transform.x) || !Number.isFinite(entity.transform.y)) return;
|
|
677
|
+
const footprint = entity.footprint || {width: 1, height: 1};
|
|
678
|
+
const startX = Math.floor(entity.transform.x);
|
|
679
|
+
const startY = Math.floor(entity.transform.y);
|
|
680
|
+
for (let dx = 0; dx < footprint.width; dx++) {
|
|
681
|
+
for (let dy = 0; dy < footprint.height; dy++) {
|
|
682
|
+
const x = startX + dx;
|
|
683
|
+
const y = startY + dy;
|
|
684
|
+
const key = blockKey(x, y);
|
|
685
|
+
let block = blocks.get(key);
|
|
686
|
+
if (block === undefined) {
|
|
687
|
+
block = {x, y, entities: []};
|
|
688
|
+
blocks.set(key, block);
|
|
689
|
+
}
|
|
690
|
+
block.entities.push(entity);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#removeEntityFromDerivedBlocks(derived, entity) {
|
|
696
|
+
if (!derived.blocksByKey) return;
|
|
697
|
+
const id = entity.entity;
|
|
698
|
+
for (const cell of entity.occupies || []) {
|
|
699
|
+
const key = blockKey(cell.x, cell.y);
|
|
700
|
+
const block = derived.blocksByKey.get(key);
|
|
701
|
+
if (!block) continue;
|
|
702
|
+
// Splice in place rather than allocating a filtered array per cell.
|
|
703
|
+
const list = block.entities;
|
|
704
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
705
|
+
if (list[i].entity === id) list.splice(i, 1);
|
|
706
|
+
}
|
|
707
|
+
if (!list.length) derived.blocksByKey.delete(key);
|
|
708
|
+
derived.blocksDirty = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#ensureDerivedSummaries() {
|
|
713
|
+
const derived = this._derived;
|
|
714
|
+
if (!derived) return;
|
|
715
|
+
if (derived.tableSummariesDirty) {
|
|
716
|
+
derived.tableSummaries = this.#tableSummaries();
|
|
717
|
+
derived.tableSummariesDirty = false;
|
|
718
|
+
}
|
|
719
|
+
if (derived.summariesDirty) this.#refreshDerivedSummaries();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#refreshDerivedSummaries() {
|
|
723
|
+
const derived = this._derived;
|
|
724
|
+
const entities = derived.entityIds.map((entityId) => derived.entitiesById.get(entityId)).filter(Boolean);
|
|
725
|
+
const machines = {
|
|
726
|
+
itemHolders: [],
|
|
727
|
+
fabricators: [],
|
|
728
|
+
cargoEjectors: [],
|
|
729
|
+
cannons: [],
|
|
730
|
+
thrusters: [],
|
|
731
|
+
pushers: [],
|
|
732
|
+
pusherBeams: [],
|
|
733
|
+
launchers: [],
|
|
734
|
+
health: [],
|
|
735
|
+
loaders: [],
|
|
736
|
+
cargoHatches: [],
|
|
737
|
+
navigationUnits: [],
|
|
738
|
+
commsStations: [],
|
|
739
|
+
fluidTanks: [],
|
|
740
|
+
shieldGenerators: [],
|
|
741
|
+
shieldProjectors: [],
|
|
742
|
+
helms: [],
|
|
743
|
+
signs: [],
|
|
744
|
+
spawnPoints: [],
|
|
745
|
+
doors: [],
|
|
746
|
+
expandoBoxes: []
|
|
747
|
+
};
|
|
748
|
+
const players = [];
|
|
749
|
+
const shipControls = [];
|
|
750
|
+
const transforms = [];
|
|
751
|
+
|
|
752
|
+
for (const entity of entities) {
|
|
753
|
+
if (entity.transform) transforms.push(entity.transform);
|
|
754
|
+
const contents = entity.contents;
|
|
755
|
+
if (!contents) continue;
|
|
756
|
+
if (contents.itemHolder) machines.itemHolders.push(contents.itemHolder);
|
|
757
|
+
if (contents.fabricator) machines.fabricators.push(contents.fabricator);
|
|
758
|
+
if (contents.cargoEjector) machines.cargoEjectors.push(contents.cargoEjector);
|
|
759
|
+
if (contents.cannon) machines.cannons.push(contents.cannon);
|
|
760
|
+
if (contents.thruster) machines.thrusters.push(contents.thruster);
|
|
761
|
+
if (contents.pusher) machines.pushers.push(contents.pusher);
|
|
762
|
+
if (contents.pusherBeam) machines.pusherBeams.push(contents.pusherBeam);
|
|
763
|
+
if (contents.launcher) machines.launchers.push(contents.launcher);
|
|
764
|
+
if (contents.health) machines.health.push(contents.health);
|
|
765
|
+
if (contents.loader) machines.loaders.push(contents.loader);
|
|
766
|
+
if (contents.cargoHatch) machines.cargoHatches.push(contents.cargoHatch);
|
|
767
|
+
if (contents.navigationUnit) machines.navigationUnits.push(contents.navigationUnit);
|
|
768
|
+
if (contents.commsStation) machines.commsStations.push(contents.commsStation);
|
|
769
|
+
if (contents.fluidTank) machines.fluidTanks.push(contents.fluidTank);
|
|
770
|
+
if (contents.shieldGenerator) machines.shieldGenerators.push(contents.shieldGenerator);
|
|
771
|
+
if (contents.shieldProjector) machines.shieldProjectors.push(contents.shieldProjector);
|
|
772
|
+
if (contents.helm) machines.helms.push(contents.helm);
|
|
773
|
+
if (contents.sign) machines.signs.push(contents.sign);
|
|
774
|
+
if (contents.spawnPoint) machines.spawnPoints.push(contents.spawnPoint);
|
|
775
|
+
if (contents.door) machines.doors.push(contents.door);
|
|
776
|
+
if (contents.expandoBox) machines.expandoBoxes.push(contents.expandoBox);
|
|
777
|
+
if (contents.player) players.push(contents.player);
|
|
778
|
+
if (contents.shipControl) shipControls.push(contents.shipControl);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
derived.entities = entities;
|
|
782
|
+
derived.transforms = transforms;
|
|
783
|
+
derived.players = players;
|
|
784
|
+
derived.shipControls = shipControls;
|
|
785
|
+
derived.machines = machines;
|
|
786
|
+
derived.summariesDirty = false;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// The sorted block list is the single most expensive derived product (a sort
|
|
790
|
+
// over every occupied cell in the world). Build it only when a caller actually
|
|
791
|
+
// asks for blocks, rather than as a side effect of every summary refresh.
|
|
792
|
+
#derivedBlocks() {
|
|
793
|
+
const derived = this._derived;
|
|
794
|
+
const index = this.#blocksIndex();
|
|
795
|
+
if (derived.blocksDirty || !derived.blocks) {
|
|
796
|
+
derived.blocks = [...index.values()].sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
797
|
+
derived.blocksDirty = false;
|
|
798
|
+
}
|
|
799
|
+
return derived.blocks;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Identical for every entity in a refresh pass, but was rebuilt inside each
|
|
803
|
+
// #summarizeEntity call -- O(entities x table12). Table 12 only changes during
|
|
804
|
+
// apply(), which clears this cache.
|
|
805
|
+
#blueprintPreviewItems() {
|
|
806
|
+
if (this.#blueprintItems) return this.#blueprintItems;
|
|
807
|
+
const items = [];
|
|
808
|
+
for (const [entity, record] of this.table(12)) {
|
|
809
|
+
const summary = summarizeBlueprintPreview(entity, record, this.record(0, entity));
|
|
810
|
+
if (summary) items.push(summary);
|
|
811
|
+
}
|
|
812
|
+
this.#blueprintItems = items;
|
|
813
|
+
return items;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
#summarizeEntity(entityId, tableRows = []) {
|
|
817
|
+
// tableRows already lists exactly the tables this entity appears in -- the
|
|
818
|
+
// same source the public `tables` field is built from. Reading through it
|
|
819
|
+
// avoids ~32 lookups against tables the entity is not in.
|
|
820
|
+
const byTable = new Map();
|
|
821
|
+
for (const row of tableRows) byTable.set(row.tableId, row.record);
|
|
822
|
+
const rec = (tableId) => byTable.get(tableId) ?? null;
|
|
823
|
+
const transformRecord = rec(0);
|
|
824
|
+
const itemHolderRecord = rec(6);
|
|
825
|
+
const healthRecord = rec(5);
|
|
826
|
+
const fabricatorRecord = rec(53);
|
|
827
|
+
const cargoEjectorRecord = rec(49);
|
|
828
|
+
const cannonRecord = rec(54);
|
|
829
|
+
const pusherRecord = rec(72);
|
|
830
|
+
const pusherBeamRecord = rec(42);
|
|
831
|
+
const launcherRecord = rec(44);
|
|
832
|
+
const loaderRecord = rec(78);
|
|
833
|
+
const loaderFilterRecord = rec(76);
|
|
834
|
+
const loaderFilterSlotsRecord = rec(77);
|
|
835
|
+
const commsStationRecord = rec(39);
|
|
836
|
+
const fluidTankRecord = rec(60);
|
|
837
|
+
const shieldRecord = rec(61);
|
|
838
|
+
const shieldGeneratorBoostRecord = rec(75);
|
|
839
|
+
const shieldProjectorRecord = rec(62);
|
|
840
|
+
const playerRecord = rec(55);
|
|
841
|
+
const shipControlRecord = rec(20);
|
|
842
|
+
const signRecord = rec(41);
|
|
843
|
+
const spawnPointRecord = rec(8);
|
|
844
|
+
const doorRecord = rec(47);
|
|
845
|
+
const thrusterRecord = rec(50);
|
|
846
|
+
const labelRecord = rec(9);
|
|
847
|
+
const zoneLabelRecord = rec(25);
|
|
848
|
+
const dockingSpringRecord = rec(26);
|
|
849
|
+
const hugeThrusterRecord = rec(23);
|
|
850
|
+
const bodyStateRecord = rec(1);
|
|
851
|
+
const typeRecord = rec(7);
|
|
852
|
+
const crateSizeRecord = rec(3);
|
|
853
|
+
const blueprintPreviewRecord = rec(12);
|
|
854
|
+
const expandoSizeRecord = rec(51);
|
|
855
|
+
const crateItemRecord = rec(19);
|
|
856
|
+
const markerTableIds = [73].filter((tableId) => this.record(tableId, entityId));
|
|
857
|
+
const markerTypeId = markerTypeIdForTables(markerTableIds);
|
|
858
|
+
const markerTypeName = entityNameFromType(markerTypeId);
|
|
859
|
+
const looseItemMarker = Boolean(rec(37));
|
|
860
|
+
const dynamicBody = bodyStateRecord?.q20 === -4;
|
|
861
|
+
const typeId = entityTypeIdFromRecord(typeRecord);
|
|
862
|
+
const itemHolder = summarizeItemHolder(entityId, itemHolderRecord);
|
|
863
|
+
const isExpandoBox = typeId === EXPANDO_BOX_TYPE_ID || markerTypeId === EXPANDO_BOX_TYPE_ID;
|
|
864
|
+
const health = summarizeHealth(entityId, healthRecord);
|
|
865
|
+
const fabricator = summarizeFabricator(entityId, fabricatorRecord, itemHolderRecord, typeRecord);
|
|
866
|
+
const cargoEjector = summarizeCargoEjector(entityId, typeId, cargoEjectorRecord);
|
|
867
|
+
const cannon = summarizeCannon(entityId, cannonRecord, typeId);
|
|
868
|
+
const thruster = summarizeThruster(entityId, typeId, thrusterRecord);
|
|
869
|
+
const pusher = summarizePusher(entityId, pusherRecord, loaderFilterSlotsRecord);
|
|
870
|
+
const pusherBeam = summarizePusherBeam(entityId, pusherBeamRecord);
|
|
871
|
+
const launcher = summarizeItemLauncher(entityId, typeId, launcherRecord);
|
|
872
|
+
const loader = (typeId === LOADER_TYPE_ID || (typeId == null && (loaderRecord || loaderFilterRecord || loaderFilterSlotsRecord)))
|
|
873
|
+
? summarizeLoader(entityId, loaderRecord, loaderFilterRecord, loaderFilterSlotsRecord, this.#loaderConfig, typeId === LOADER_TYPE_ID, itemHolderRecord)
|
|
874
|
+
: null;
|
|
875
|
+
const cargoHatch = summarizeCargoHatch(entityId, typeId, loaderFilterRecord, loaderFilterSlotsRecord, rec(45));
|
|
876
|
+
const navigationUnit = summarizeNavigationUnit(entityId, typeId, loaderRecord, this.#navigationUnitAutoWarp.get(entityId));
|
|
877
|
+
const fluidTank = fluidTankRecord ? {
|
|
878
|
+
entity: entityId,
|
|
879
|
+
amount: fluidTankRecord.q24 ?? null,
|
|
880
|
+
state: cloneRecord(fluidTankRecord)
|
|
881
|
+
} : null;
|
|
882
|
+
const shieldGenerator = typeId === 256 ? summarizeShieldGenerator(entityId, shieldRecord, itemHolderRecord, shieldGeneratorBoostRecord) : null;
|
|
883
|
+
const shieldProjector = typeId === 257 ? summarizeShieldProjector(entityId, shieldProjectorRecord) : null;
|
|
884
|
+
const blueprintPreview = summarizeBlueprintPreview(entityId, blueprintPreviewRecord, transformRecord);
|
|
885
|
+
const player = summarizePlayer(entityId, playerRecord, rec(14), this.#blueprintPreviewItems());
|
|
886
|
+
const shipControl = summarizeShipControl(entityId, shipControlRecord);
|
|
887
|
+
const helm = summarizeHelm(entityId, typeId, this.#helmOccupied.get(entityId));
|
|
888
|
+
const commsStation = summarizeCommsStation(entityId, typeId, commsStationRecord, this.#commsStationOccupied.get(entityId));
|
|
889
|
+
const sign = typeId === 218 ? summarizeSign(entityId, signRecord) : null;
|
|
890
|
+
const spawnPoint = typeId === 219 ? summarizeSpawnPoint(entityId, spawnPointRecord) : null;
|
|
891
|
+
const door = typeId === 220 ? summarizeDoor(entityId, spawnPointRecord, doorRecord) : null;
|
|
892
|
+
const shipSize = shipControl && this.isOverworld ? summarizeShipSize(entityId, rec(3)) : null;
|
|
893
|
+
const hoverOutline = summarizeHoverOutline(entityId, crateSizeRecord);
|
|
894
|
+
const expandoBox = isExpandoBox ? summarizeExpandoBox(entityId, itemHolderRecord, expandoSizeRecord) : null;
|
|
895
|
+
const mapMarker = this.isOverworld ? summarizeMapMarker(entityId, labelRecord, zoneLabelRecord, crateSizeRecord) : null;
|
|
896
|
+
const dockingSpring = this.isOverworld
|
|
897
|
+
? summarizeDockingSpring(entityId, dockingSpringRecord, bodyStateRecord, crateSizeRecord)
|
|
898
|
+
: null;
|
|
899
|
+
const hugeThruster = this.isOverworld
|
|
900
|
+
? summarizeHugeThruster(entityId, hugeThrusterRecord, crateSizeRecord)
|
|
901
|
+
: null;
|
|
902
|
+
const itemCrate = this.isOverworld && !rec(2) && !rec(18) && health && crateSizeRecord
|
|
903
|
+
? summarizeItemCrate(entityId, crateSizeRecord, crateItemRecord, healthRecord)
|
|
904
|
+
: null;
|
|
905
|
+
const bot = this.isOverworld
|
|
906
|
+
? summarizeBot(entityId, {
|
|
907
|
+
health,
|
|
908
|
+
table2Record: rec(2),
|
|
909
|
+
smallRecord: crateSizeRecord,
|
|
910
|
+
combatRecord: rec(18),
|
|
911
|
+
motionRecord: crateItemRecord,
|
|
912
|
+
table10Record: rec(10),
|
|
913
|
+
table51Record: rec(51),
|
|
914
|
+
itemCrate,
|
|
915
|
+
player,
|
|
916
|
+
shipControl
|
|
917
|
+
})
|
|
918
|
+
: null;
|
|
919
|
+
const transform = transformRecord ? {
|
|
920
|
+
entity: entityId,
|
|
921
|
+
x: numberOrNull(transformRecord.q20, 40),
|
|
922
|
+
y: numberOrNull(transformRecord.q24, 40),
|
|
923
|
+
rot: numberOrNull(transformRecord.q28, 127.324),
|
|
924
|
+
flags: [transformRecord.q33, transformRecord.q34, transformRecord.q35].filter((value) => value != null)
|
|
925
|
+
} : null;
|
|
926
|
+
const contents = mergeContents({
|
|
927
|
+
itemHolder, expandoBox, hoverOutline, itemCrate, mapMarker, dockingSpring,
|
|
928
|
+
hugeThruster, blueprintPreview, health, bot, fabricator, cargoEjector,
|
|
929
|
+
cannon, thruster, pusher, pusherBeam, launcher, loader, cargoHatch,
|
|
930
|
+
navigationUnit, commsStation, fluidTank, shieldGenerator, shieldProjector,
|
|
931
|
+
helm, player, shipControl, sign, spawnPoint, door, shipSize
|
|
932
|
+
});
|
|
933
|
+
const footprint = entityFootprint({
|
|
934
|
+
entity: entityId,
|
|
935
|
+
typeId,
|
|
936
|
+
markerTypeId,
|
|
937
|
+
itemHolder,
|
|
938
|
+
expandoBox,
|
|
939
|
+
hoverOutline,
|
|
940
|
+
itemCrate,
|
|
941
|
+
hugeThruster,
|
|
942
|
+
fabricator,
|
|
943
|
+
cargoEjector,
|
|
944
|
+
cannon,
|
|
945
|
+
thruster,
|
|
946
|
+
pusher,
|
|
947
|
+
launcher,
|
|
948
|
+
loader,
|
|
949
|
+
cargoHatch,
|
|
950
|
+
navigationUnit,
|
|
951
|
+
commsStation,
|
|
952
|
+
fluidTank,
|
|
953
|
+
shieldGenerator,
|
|
954
|
+
shieldProjector,
|
|
955
|
+
helm,
|
|
956
|
+
player,
|
|
957
|
+
shipControl
|
|
958
|
+
});
|
|
959
|
+
const typeName = entityNameFromType(typeId);
|
|
960
|
+
const category = entityCategory({
|
|
961
|
+
typeId,
|
|
962
|
+
markerTypeId,
|
|
963
|
+
looseItemMarker,
|
|
964
|
+
dynamicBody,
|
|
965
|
+
transform,
|
|
966
|
+
itemHolder,
|
|
967
|
+
itemCrate,
|
|
968
|
+
mapMarker,
|
|
969
|
+
dockingSpring,
|
|
970
|
+
hugeThruster,
|
|
971
|
+
blueprintPreview,
|
|
972
|
+
fabricator,
|
|
973
|
+
cargoEjector,
|
|
974
|
+
cannon,
|
|
975
|
+
pusher,
|
|
976
|
+
launcher,
|
|
977
|
+
loader,
|
|
978
|
+
cargoHatch,
|
|
979
|
+
navigationUnit,
|
|
980
|
+
commsStation,
|
|
981
|
+
fluidTank,
|
|
982
|
+
shieldGenerator,
|
|
983
|
+
shieldProjector,
|
|
984
|
+
helm,
|
|
985
|
+
player,
|
|
986
|
+
shipControl
|
|
987
|
+
});
|
|
988
|
+
const summary = {
|
|
989
|
+
entity: entityId,
|
|
990
|
+
category,
|
|
991
|
+
typeId,
|
|
992
|
+
typeName,
|
|
993
|
+
markerTypeId,
|
|
994
|
+
markerTypeName,
|
|
995
|
+
label: entityLabel({
|
|
996
|
+
category,
|
|
997
|
+
typeId,
|
|
998
|
+
typeName,
|
|
999
|
+
markerTypeName,
|
|
1000
|
+
mapMarker,
|
|
1001
|
+
dockingSpring,
|
|
1002
|
+
hugeThruster,
|
|
1003
|
+
blueprintPreview,
|
|
1004
|
+
expandoBox,
|
|
1005
|
+
hoverOutline,
|
|
1006
|
+
itemHolder,
|
|
1007
|
+
itemCrate,
|
|
1008
|
+
fabricator,
|
|
1009
|
+
cargoEjector,
|
|
1010
|
+
cannon,
|
|
1011
|
+
thruster,
|
|
1012
|
+
pusher,
|
|
1013
|
+
pusherBeam,
|
|
1014
|
+
launcher,
|
|
1015
|
+
loader,
|
|
1016
|
+
cargoHatch,
|
|
1017
|
+
navigationUnit,
|
|
1018
|
+
fluidTank,
|
|
1019
|
+
shieldGenerator,
|
|
1020
|
+
shieldProjector,
|
|
1021
|
+
player,
|
|
1022
|
+
shipControl,
|
|
1023
|
+
sign,
|
|
1024
|
+
spawnPoint,
|
|
1025
|
+
door,
|
|
1026
|
+
isOverworld: this.isOverworld
|
|
1027
|
+
}),
|
|
1028
|
+
kind: [
|
|
1029
|
+
transform ? "transform" : null,
|
|
1030
|
+
dynamicBody ? "dynamic_body" : null,
|
|
1031
|
+
itemHolder ? "item_holder" : null,
|
|
1032
|
+
health ? "health" : null,
|
|
1033
|
+
bot ? "bot" : null,
|
|
1034
|
+
itemCrate ? "item_crate" : null,
|
|
1035
|
+
expandoBox ? "expando_box" : null,
|
|
1036
|
+
hoverOutline ? "hover_outline" : null,
|
|
1037
|
+
mapMarker ? "map_marker" : null,
|
|
1038
|
+
dockingSpring ? "docking_spring" : null,
|
|
1039
|
+
hugeThruster ? "huge_thruster" : null,
|
|
1040
|
+
blueprintPreview ? "blueprint_preview" : null,
|
|
1041
|
+
markerTableIds.length ? "marker" : null,
|
|
1042
|
+
looseItemMarker ? "loose_item_marker" : null,
|
|
1043
|
+
fabricator ? "fabricator" : null,
|
|
1044
|
+
cargoEjector ? "cargo_ejector" : null,
|
|
1045
|
+
cannon ? "cannon" : null,
|
|
1046
|
+
thruster ? "thruster" : null,
|
|
1047
|
+
pusher ? "pusher" : null,
|
|
1048
|
+
pusherBeam ? "pusher_beam" : null,
|
|
1049
|
+
launcher ? "launcher" : null,
|
|
1050
|
+
loader ? "loader" : null,
|
|
1051
|
+
cargoHatch ? "cargo_hatch" : null,
|
|
1052
|
+
navigationUnit ? "navigation_unit" : null,
|
|
1053
|
+
commsStation ? "comms_station" : null,
|
|
1054
|
+
fluidTank ? "fluid_tank" : null,
|
|
1055
|
+
shieldGenerator ? "shield_generator" : null,
|
|
1056
|
+
shieldProjector ? "shield_projector" : null,
|
|
1057
|
+
helm ? "helm" : null,
|
|
1058
|
+
player ? "player" : null,
|
|
1059
|
+
shipControl ? "ship_control" : null,
|
|
1060
|
+
sign ? "sign" : null,
|
|
1061
|
+
spawnPoint ? "spawn_point" : null,
|
|
1062
|
+
door ? "door" : null,
|
|
1063
|
+
shipControl && this.isOverworld ? "overworld_ship" : null
|
|
1064
|
+
].filter(Boolean),
|
|
1065
|
+
transform,
|
|
1066
|
+
footprint,
|
|
1067
|
+
contents,
|
|
1068
|
+
tables: tableRows.map(({tableId, name, record}) => ({
|
|
1069
|
+
tableId,
|
|
1070
|
+
name,
|
|
1071
|
+
record: cloneRecord(record)
|
|
1072
|
+
}))
|
|
1073
|
+
};
|
|
1074
|
+
// Lazy: most entities are never asked for their occupied cells, and a large
|
|
1075
|
+
// footprint materialises hundreds of objects per refresh. Defined as an
|
|
1076
|
+
// enumerable memoising accessor so spread, JSON and structuredClone all see
|
|
1077
|
+
// the same array they saw before.
|
|
1078
|
+
defineLazyProperty(summary, "occupies", transform ? () => this.#occupiedBlocks(transform, footprint) : () => []);
|
|
1079
|
+
return summary;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
#occupiedBlocks(transform, footprint) {
|
|
1083
|
+
const startX = Math.floor(transform.x ?? 0);
|
|
1084
|
+
const startY = Math.floor(transform.y ?? 0);
|
|
1085
|
+
const cells = [];
|
|
1086
|
+
for (let dx = 0; dx < footprint.width; dx++) {
|
|
1087
|
+
for (let dy = 0; dy < footprint.height; dy++) {
|
|
1088
|
+
cells.push({x: startX + dx, y: startY + dy});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return cells;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
#records(tableId) {
|
|
1095
|
+
return [...this.table(tableId).entries()].map(([entity, record]) => ({entity, ...record}));
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
#readRemovals(reader) {
|
|
1099
|
+
const removals = [];
|
|
1100
|
+
let entity = 0;
|
|
1101
|
+
while (reader.remaining > 0) {
|
|
1102
|
+
const delta = reader.readStreamInt();
|
|
1103
|
+
if (delta === 0) break;
|
|
1104
|
+
entity += delta;
|
|
1105
|
+
removals.push(entity);
|
|
1106
|
+
// Delete only from tables the entity actually appears in when the derived
|
|
1107
|
+
// index is available; fall back to a full scan otherwise.
|
|
1108
|
+
const known = this._derived?.entityTableIds?.get(entity);
|
|
1109
|
+
if (known) {
|
|
1110
|
+
for (const tableId of known) this.tables.get(tableId)?.delete(entity);
|
|
1111
|
+
} else {
|
|
1112
|
+
for (const records of this.tables.values()) records.delete(entity);
|
|
1113
|
+
}
|
|
1114
|
+
this.#loaderConfig.delete(null, entity);
|
|
1115
|
+
this.#helmOccupied.delete(entity);
|
|
1116
|
+
this.#commsStationOccupied.delete(entity);
|
|
1117
|
+
this.#navigationUnitAutoWarp.delete(entity);
|
|
1118
|
+
}
|
|
1119
|
+
this.totalRemovedCount += removals.length;
|
|
1120
|
+
for (const id of removals) pushCapped(this.removedEntities, id, MAX_RETAINED_REMOVALS);
|
|
1121
|
+
return removals;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
#readSection(reader, tag, tableId) {
|
|
1125
|
+
const spec = MODEL_TABLE_SPECS.get(tableId);
|
|
1126
|
+
return this.#readSectionRecords(reader, tag, tableId, spec);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
#readSectionRecords(reader, tag, tableId, spec) {
|
|
1130
|
+
const section = {tag, table: tableId, name: spec?.name || null, records: []};
|
|
1131
|
+
let entity = 0;
|
|
1132
|
+
const seenEntities = tableId === 78 ? new Set() : null;
|
|
1133
|
+
while (reader.remaining > 0) {
|
|
1134
|
+
const recordOffset = reader.offset;
|
|
1135
|
+
let delta;
|
|
1136
|
+
try {
|
|
1137
|
+
delta = reader.readStreamInt();
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
throw new Error(`model table ${tableId} tag ${tag} record offset ${recordOffset}: ${error.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
if (delta === 0) break;
|
|
1142
|
+
entity += delta;
|
|
1143
|
+
let mask;
|
|
1144
|
+
try {
|
|
1145
|
+
mask = reader.readUnsigned();
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
throw new Error(`model table ${tableId} tag ${tag} entity ${entity} mask offset ${reader.offset}: ${error.message}`);
|
|
1148
|
+
}
|
|
1149
|
+
const record = this.#getRecord(tableId, entity);
|
|
1150
|
+
const previous = tableId === 78 ? cloneRecord(record) : null;
|
|
1151
|
+
record.lastMask = mask;
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
if (spec) this.#applyRecordSpec(reader, record, mask, spec);
|
|
1155
|
+
else if (!MASK_ONLY_TABLES.has(tableId)) throw new Error(`missing model table spec ${tableId}`);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
throw new Error(`model table ${tableId} tag ${tag} entity ${entity} mask ${mask} offset ${reader.offset}: ${error.message}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const changed = {entity, mask};
|
|
1161
|
+
if (previous) changed.previous = previous;
|
|
1162
|
+
if (seenEntities?.has(entity)) changed.repeatedInSection = true;
|
|
1163
|
+
seenEntities?.add(entity);
|
|
1164
|
+
if (tableId === 78) changed.record = cloneRecord(record);
|
|
1165
|
+
section.records.push(changed);
|
|
1166
|
+
}
|
|
1167
|
+
return section;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#getRecord(tableId, entity) {
|
|
1171
|
+
if (!this.tables.has(tableId)) this.tables.set(tableId, new Map());
|
|
1172
|
+
const records = this.tables.get(tableId);
|
|
1173
|
+
if (!records.has(entity)) records.set(entity, {});
|
|
1174
|
+
return records.get(entity);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
#applyRecordSpec(reader, record, mask, spec) {
|
|
1178
|
+
if (typeof spec.read === "function") {
|
|
1179
|
+
spec.read(reader, record, mask);
|
|
1180
|
+
if (spec.scale) record.scaled = spec.scale(record);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const hasPacked = Boolean(
|
|
1185
|
+
(spec.packedMask && (mask & spec.packedMask)) ||
|
|
1186
|
+
spec.packedBits?.some((item) => mask & item.bit)
|
|
1187
|
+
);
|
|
1188
|
+
let packed = null;
|
|
1189
|
+
if (hasPacked && spec.packedBeforeValues) packed = reader.readUnsigned();
|
|
1190
|
+
|
|
1191
|
+
let packedIndex = 0;
|
|
1192
|
+
if (spec.orderedValues) {
|
|
1193
|
+
for (const item of spec.orderedItems) {
|
|
1194
|
+
if (!(mask & item.bit)) continue;
|
|
1195
|
+
if (item.kind === "blob") record[item.key] = reader.readBlob();
|
|
1196
|
+
else record[item.key] = (record[item.key] || 0) + reader.readFieldDelta();
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
for (const blob of spec.blobs || []) {
|
|
1200
|
+
if (mask & blob.bit) record[blob.key] = reader.readBlob();
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (hasPacked && !spec.packedBeforeValues) packed = reader.readUnsigned();
|
|
1205
|
+
|
|
1206
|
+
for (const item of spec.packedBits || []) {
|
|
1207
|
+
if (mask & item.bit) record[item.key] = (packed >> packedIndex++) & 1;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (packed != null && spec.packedOffsetKeys) {
|
|
1211
|
+
for (const key of spec.packedOffsetKeys) {
|
|
1212
|
+
record[key] = (packed >> packedIndex++) & 1;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (!spec.orderedValues) {
|
|
1217
|
+
for (const field of spec.fields || []) {
|
|
1218
|
+
if (!(mask & field.bit)) continue;
|
|
1219
|
+
record[field.key] = (record[field.key] || 0) + reader.readFieldDelta();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (spec.scale) record.scaled = spec.scale(record);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function summarizeUpdate(update) {
|
|
1228
|
+
return {
|
|
1229
|
+
generation: update.generation,
|
|
1230
|
+
sectionCount: update.sections.length,
|
|
1231
|
+
removals: update.removals.length,
|
|
1232
|
+
unknownTags: update.unknownTags,
|
|
1233
|
+
error: update.error?.message || null,
|
|
1234
|
+
sections: update.sections.map((section) => ({
|
|
1235
|
+
tag: section.tag,
|
|
1236
|
+
table: section.table,
|
|
1237
|
+
name: section.name,
|
|
1238
|
+
records: section.records.length
|
|
1239
|
+
}))
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function insertSorted(values, value) {
|
|
1244
|
+
let low = 0;
|
|
1245
|
+
let high = values.length;
|
|
1246
|
+
while (low < high) {
|
|
1247
|
+
const mid = (low + high) >> 1;
|
|
1248
|
+
if (values[mid] < value) low = mid + 1;
|
|
1249
|
+
else high = mid;
|
|
1250
|
+
}
|
|
1251
|
+
values.splice(low, 0, value);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
export function decodeModelData(bytes) {
|
|
1255
|
+
const state = new ModelState();
|
|
1256
|
+
return state.apply(bytes);
|
|
1257
|
+
}
|