dredless 0.6.3 → 1.0.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/README.md +22 -57
- package/dist/browser/dredless.global.js +827 -1058
- package/dist/browser/dredless.global.js.map +4 -4
- package/docs/ACCOUNT.md +146 -0
- package/docs/API_REFERENCE.md +63 -2550
- package/docs/BROWSER_API.md +151 -70
- package/docs/CLIENT.md +183 -0
- package/docs/EVENTS.md +136 -0
- package/docs/GETTING_STARTED.md +135 -0
- package/docs/MACHINES.md +351 -0
- package/docs/MANAGEMENT.md +285 -0
- package/docs/NODE_API.md +120 -318
- package/docs/PLAYER_INVENTORY.md +235 -0
- package/docs/PROTOCOL.md +227 -0
- package/docs/README.md +44 -0
- package/docs/TROUBLESHOOTING.md +91 -0
- package/docs/WORLD_STATE.md +183 -0
- package/package.json +1 -2
- package/src/browser.d.ts +31 -1
- package/src/browser.js +10 -0
- package/src/client/core.js +302 -15
- package/src/client/domains/index.js +57 -7
- package/src/client/index.d.ts +1 -1
- package/src/index.d.ts +303 -14
- package/src/index.js +10 -0
- package/src/network/account.js +182 -0
- package/src/network/disconnect.d.ts +2 -0
- package/src/network/disconnect.js +53 -0
- package/src/network/index.d.ts +1 -0
- package/src/network/index.js +1 -0
- package/src/network/invites.d.ts +0 -3
- package/src/network/invites.js +3 -40
- package/src/network/session.d.ts +2 -0
- package/src/network/session.js +21 -0
- package/src/protocol/outbound/ship-management.d.ts +66 -1
- package/src/protocol/outbound/ship-management.js +83 -0
- package/src/state/model/decoder.js +101 -56
- package/src/state/model/index.d.ts +4 -0
- package/src/state/model/loader-config.js +0 -997
- package/src/state/model/state.js +28 -80
- package/src/state/world/index.js +27 -1
package/src/state/model/state.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as modelDecoder from "./decoder.js";
|
|
2
|
-
import {LoaderConfigTracker} from "./loader-config.js";
|
|
3
2
|
import {navigationDestinationFromEncodedValue} from "../overworld.js";
|
|
4
3
|
|
|
5
4
|
const {
|
|
@@ -47,9 +46,6 @@ const {
|
|
|
47
46
|
blobKey,
|
|
48
47
|
cloneRecord,
|
|
49
48
|
firstRecord,
|
|
50
|
-
TABLE_78_FIELD_BITS,
|
|
51
|
-
table78DeltaRecord,
|
|
52
|
-
isSemanticLoaderDelta,
|
|
53
49
|
decodeText,
|
|
54
50
|
entityNameFromType,
|
|
55
51
|
markerTypeIdForTables,
|
|
@@ -126,7 +122,7 @@ const {
|
|
|
126
122
|
} = modelDecoder;
|
|
127
123
|
|
|
128
124
|
export class ModelState {
|
|
129
|
-
#
|
|
125
|
+
#localPlayerEntity = null;
|
|
130
126
|
#blueprintItems = null;
|
|
131
127
|
#helmOccupied = new Map();
|
|
132
128
|
#commsStationOccupied = new Map();
|
|
@@ -152,6 +148,18 @@ export class ModelState {
|
|
|
152
148
|
this.#invalidateDerived();
|
|
153
149
|
}
|
|
154
150
|
|
|
151
|
+
get localPlayerEntity() {
|
|
152
|
+
return this.#localPlayerEntity;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setLocalPlayerEntity(entity) {
|
|
156
|
+
const next = entity == null ? null : Number(entity);
|
|
157
|
+
if (next !== null && (!Number.isInteger(next) || next < 0)) throw new RangeError("local player entity must be a non-negative integer or null");
|
|
158
|
+
if (this.#localPlayerEntity === next) return;
|
|
159
|
+
this.#localPlayerEntity = next;
|
|
160
|
+
this.#invalidateDerived();
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
table(id) {
|
|
156
164
|
// Shared empty map: #summarizeEntity performs ~32 record() lookups per
|
|
157
165
|
// entity, most against absent tables, and each miss used to allocate.
|
|
@@ -236,8 +244,6 @@ export class ModelState {
|
|
|
236
244
|
pushCapped(this.errors, {message: error.message, generation: update.generation}, MAX_RETAINED_ERRORS);
|
|
237
245
|
}
|
|
238
246
|
|
|
239
|
-
this.#remapIndexedLoaderConfig(update);
|
|
240
|
-
this.#updateLoaderConfig(update);
|
|
241
247
|
this.#updateNavigationUnitAutoWarp(update);
|
|
242
248
|
this.#updateHelmOccupancy(update);
|
|
243
249
|
this.#updateCommsStationOccupancy(update);
|
|
@@ -354,60 +360,6 @@ export class ModelState {
|
|
|
354
360
|
this._derived.tableSummariesDirty = true;
|
|
355
361
|
}
|
|
356
362
|
|
|
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
363
|
#updateNavigationUnitAutoWarp(update) {
|
|
412
364
|
const trackedBeforeUpdate = new Set(this.#navigationUnitAutoWarp.keys());
|
|
413
365
|
const initializedThisUpdate = new Set();
|
|
@@ -496,28 +448,30 @@ export class ModelState {
|
|
|
496
448
|
const table7 = update.sections.find((section) => section.table === 7);
|
|
497
449
|
if (!table7) return;
|
|
498
450
|
|
|
499
|
-
|
|
451
|
+
// Zoom state is a hint for helm occupancy when paired with helm updates;
|
|
452
|
+
// the player flag itself does not identify a pilot or an operated machine.
|
|
453
|
+
const changedZoomState = new Map();
|
|
500
454
|
for (const section of update.sections) {
|
|
501
455
|
if (section.table !== 55) continue;
|
|
502
456
|
for (const changed of section.records) {
|
|
503
457
|
if (!(changed.mask & 16)) continue;
|
|
504
458
|
const player = this.record(55, changed.entity);
|
|
505
|
-
|
|
459
|
+
changedZoomState.set(changed.entity, Boolean(player?.q107));
|
|
506
460
|
}
|
|
507
461
|
}
|
|
508
462
|
|
|
509
|
-
const
|
|
463
|
+
const zoomedOutPlayerCount = [...this.table(55).values()].filter((record) => record?.q107).length;
|
|
510
464
|
for (const changed of table7.records) {
|
|
511
465
|
const record = this.record(7, changed.entity);
|
|
512
466
|
const typeId = entityTypeIdFromRecord(record);
|
|
513
467
|
if (!HELM_TYPE_IDS.has(typeId)) continue;
|
|
514
468
|
|
|
515
|
-
if (
|
|
469
|
+
if (zoomedOutPlayerCount > 0 && record?.q20 === 0 && HELM_TYPE_IDS.has(Number(record?.q32))) {
|
|
516
470
|
this.#helmOccupied.set(changed.entity, true);
|
|
517
471
|
}
|
|
518
472
|
|
|
519
|
-
if (!(changed.mask & 8) ||
|
|
520
|
-
this.#helmOccupied.set(changed.entity, [...
|
|
473
|
+
if (!(changed.mask & 8) || changedZoomState.size === 0) continue;
|
|
474
|
+
this.#helmOccupied.set(changed.entity, [...changedZoomState.values()].some(Boolean));
|
|
521
475
|
}
|
|
522
476
|
}
|
|
523
477
|
|
|
@@ -829,7 +783,8 @@ export class ModelState {
|
|
|
829
783
|
const pusherRecord = rec(72);
|
|
830
784
|
const pusherBeamRecord = rec(42);
|
|
831
785
|
const launcherRecord = rec(44);
|
|
832
|
-
const loaderRecord = rec(
|
|
786
|
+
const loaderRecord = rec(69);
|
|
787
|
+
const navigationRecord = rec(78);
|
|
833
788
|
const loaderFilterRecord = rec(76);
|
|
834
789
|
const loaderFilterSlotsRecord = rec(77);
|
|
835
790
|
const commsStationRecord = rec(39);
|
|
@@ -864,16 +819,16 @@ export class ModelState {
|
|
|
864
819
|
const health = summarizeHealth(entityId, healthRecord);
|
|
865
820
|
const fabricator = summarizeFabricator(entityId, fabricatorRecord, itemHolderRecord, typeRecord);
|
|
866
821
|
const cargoEjector = summarizeCargoEjector(entityId, typeId, cargoEjectorRecord);
|
|
867
|
-
const cannon = summarizeCannon(entityId, cannonRecord, typeId);
|
|
822
|
+
const cannon = summarizeCannon(entityId, cannonRecord, typeId, transformRecord);
|
|
868
823
|
const thruster = summarizeThruster(entityId, typeId, thrusterRecord);
|
|
869
824
|
const pusher = summarizePusher(entityId, pusherRecord, loaderFilterSlotsRecord);
|
|
870
825
|
const pusherBeam = summarizePusherBeam(entityId, pusherBeamRecord);
|
|
871
826
|
const launcher = summarizeItemLauncher(entityId, typeId, launcherRecord);
|
|
872
827
|
const loader = (typeId === LOADER_TYPE_ID || (typeId == null && (loaderRecord || loaderFilterRecord || loaderFilterSlotsRecord)))
|
|
873
|
-
? summarizeLoader(entityId, loaderRecord, loaderFilterRecord, loaderFilterSlotsRecord,
|
|
828
|
+
? summarizeLoader(entityId, loaderRecord, loaderFilterRecord, loaderFilterSlotsRecord, typeId === LOADER_TYPE_ID, itemHolderRecord)
|
|
874
829
|
: null;
|
|
875
830
|
const cargoHatch = summarizeCargoHatch(entityId, typeId, loaderFilterRecord, loaderFilterSlotsRecord, rec(45));
|
|
876
|
-
const navigationUnit = summarizeNavigationUnit(entityId, typeId,
|
|
831
|
+
const navigationUnit = summarizeNavigationUnit(entityId, typeId, navigationRecord, this.#navigationUnitAutoWarp.get(entityId));
|
|
877
832
|
const fluidTank = fluidTankRecord ? {
|
|
878
833
|
entity: entityId,
|
|
879
834
|
amount: fluidTankRecord.q24 ?? null,
|
|
@@ -882,7 +837,7 @@ export class ModelState {
|
|
|
882
837
|
const shieldGenerator = typeId === 256 ? summarizeShieldGenerator(entityId, shieldRecord, itemHolderRecord, shieldGeneratorBoostRecord) : null;
|
|
883
838
|
const shieldProjector = typeId === 257 ? summarizeShieldProjector(entityId, shieldProjectorRecord) : null;
|
|
884
839
|
const blueprintPreview = summarizeBlueprintPreview(entityId, blueprintPreviewRecord, transformRecord);
|
|
885
|
-
const player = summarizePlayer(entityId, playerRecord, rec(14), this.#blueprintPreviewItems());
|
|
840
|
+
const player = summarizePlayer(entityId, playerRecord, rec(14), this.#blueprintPreviewItems(), rec(56), this.#localPlayerEntity);
|
|
886
841
|
const shipControl = summarizeShipControl(entityId, shipControlRecord);
|
|
887
842
|
const helm = summarizeHelm(entityId, typeId, this.#helmOccupied.get(entityId));
|
|
888
843
|
const commsStation = summarizeCommsStation(entityId, typeId, commsStationRecord, this.#commsStationOccupied.get(entityId));
|
|
@@ -1111,7 +1066,6 @@ export class ModelState {
|
|
|
1111
1066
|
} else {
|
|
1112
1067
|
for (const records of this.tables.values()) records.delete(entity);
|
|
1113
1068
|
}
|
|
1114
|
-
this.#loaderConfig.delete(null, entity);
|
|
1115
1069
|
this.#helmOccupied.delete(entity);
|
|
1116
1070
|
this.#commsStationOccupied.delete(entity);
|
|
1117
1071
|
this.#navigationUnitAutoWarp.delete(entity);
|
|
@@ -1129,7 +1083,6 @@ export class ModelState {
|
|
|
1129
1083
|
#readSectionRecords(reader, tag, tableId, spec) {
|
|
1130
1084
|
const section = {tag, table: tableId, name: spec?.name || null, records: []};
|
|
1131
1085
|
let entity = 0;
|
|
1132
|
-
const seenEntities = tableId === 78 ? new Set() : null;
|
|
1133
1086
|
while (reader.remaining > 0) {
|
|
1134
1087
|
const recordOffset = reader.offset;
|
|
1135
1088
|
let delta;
|
|
@@ -1147,7 +1100,6 @@ export class ModelState {
|
|
|
1147
1100
|
throw new Error(`model table ${tableId} tag ${tag} entity ${entity} mask offset ${reader.offset}: ${error.message}`);
|
|
1148
1101
|
}
|
|
1149
1102
|
const record = this.#getRecord(tableId, entity);
|
|
1150
|
-
const previous = tableId === 78 ? cloneRecord(record) : null;
|
|
1151
1103
|
record.lastMask = mask;
|
|
1152
1104
|
|
|
1153
1105
|
try {
|
|
@@ -1158,10 +1110,6 @@ export class ModelState {
|
|
|
1158
1110
|
}
|
|
1159
1111
|
|
|
1160
1112
|
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
1113
|
section.records.push(changed);
|
|
1166
1114
|
}
|
|
1167
1115
|
return section;
|
|
@@ -1170,7 +1118,7 @@ export class ModelState {
|
|
|
1170
1118
|
#getRecord(tableId, entity) {
|
|
1171
1119
|
if (!this.tables.has(tableId)) this.tables.set(tableId, new Map());
|
|
1172
1120
|
const records = this.tables.get(tableId);
|
|
1173
|
-
if (!records.has(entity)) records.set(entity, {});
|
|
1121
|
+
if (!records.has(entity)) records.set(entity, {...MODEL_TABLE_SPECS.get(tableId)?.defaults});
|
|
1174
1122
|
return records.get(entity);
|
|
1175
1123
|
}
|
|
1176
1124
|
|
package/src/state/world/index.js
CHANGED
|
@@ -28,6 +28,7 @@ function pushBounded(target, value, limit) {
|
|
|
28
28
|
export class WorldStore {
|
|
29
29
|
#overworldId = null;
|
|
30
30
|
#shipWorldId = null;
|
|
31
|
+
#cameraFocus = null;
|
|
31
32
|
|
|
32
33
|
constructor(limits = {}) {
|
|
33
34
|
this.currentWorldId = null;
|
|
@@ -47,6 +48,17 @@ export class WorldStore {
|
|
|
47
48
|
|
|
48
49
|
apply(packet) {
|
|
49
50
|
if (!packet || packet.world == null) return null;
|
|
51
|
+
if (packet.type === 21) {
|
|
52
|
+
if (this.#cameraFocus && this.#cameraFocus.world !== packet.world) {
|
|
53
|
+
this.worlds.get(Number(this.#cameraFocus.world))?.model.setLocalPlayerEntity(null);
|
|
54
|
+
}
|
|
55
|
+
this.#cameraFocus = packet;
|
|
56
|
+
this.currentWorldId = Number(packet.world);
|
|
57
|
+
const world = this.get(packet.world);
|
|
58
|
+
world.model.setLocalPlayerEntity(null);
|
|
59
|
+
this.#resolveLocalPlayer(world);
|
|
60
|
+
return {type: "camera-focus", world, packet};
|
|
61
|
+
}
|
|
50
62
|
if (packet.type === 22) return this.#applyMeta(packet);
|
|
51
63
|
if (packet.type === 23) return this.#applyTiles(packet);
|
|
52
64
|
if (packet.type === 20) return this.#applyModel(packet);
|
|
@@ -98,16 +110,27 @@ export class WorldStore {
|
|
|
98
110
|
return this.worlds.get(Number(shipWorld.parentWorld))?.entity(shipWorld.parentEntity) || null;
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
#resolveLocalPlayer(world) {
|
|
114
|
+
if (!this.#cameraFocus || Number(this.#cameraFocus.world) !== world.id || world.seed == null) return;
|
|
115
|
+
const focus = world.decodeEncrypted(this.#cameraFocus.data);
|
|
116
|
+
// Official camera message is [player_ent, zero], encrypted with the world seed.
|
|
117
|
+
if (Array.isArray(focus) && focus.length === 2 && Number.isInteger(focus[0]) && focus[0] >= 0 && focus[1] === 0) {
|
|
118
|
+
world.model.setLocalPlayerEntity(focus[0]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
#applyMeta(packet) {
|
|
102
123
|
if (packet.removed) {
|
|
103
124
|
const world = this.worlds.get(Number(packet.world));
|
|
104
125
|
if (world) world.readMeta(packet);
|
|
105
126
|
this.worlds.delete(Number(packet.world));
|
|
127
|
+
if (Number(this.#cameraFocus?.world) === Number(packet.world)) this.#cameraFocus = null;
|
|
106
128
|
if (this.currentWorldId === Number(packet.world)) this.currentWorldId = null;
|
|
107
129
|
return {type: "world-removed", world: world || null, packet};
|
|
108
130
|
}
|
|
109
131
|
const world = this.get(packet.world);
|
|
110
132
|
world.readMeta(packet);
|
|
133
|
+
this.#resolveLocalPlayer(world);
|
|
111
134
|
if (this.currentWorldId == null && !world.isOverworld) this.currentWorldId = world.id;
|
|
112
135
|
return {type: "world", world};
|
|
113
136
|
}
|
|
@@ -157,7 +180,10 @@ export class WorldStore {
|
|
|
157
180
|
if (packet.model_data && world.seed != null) {
|
|
158
181
|
const previousModel = world.model;
|
|
159
182
|
try {
|
|
160
|
-
if (packet.full)
|
|
183
|
+
if (packet.full) {
|
|
184
|
+
world.model = new ModelState({isOverworld: world.isOverworld});
|
|
185
|
+
world.model.setLocalPlayerEntity(previousModel.localPlayerEntity);
|
|
186
|
+
}
|
|
161
187
|
result.decoded = decodeModelPayload(packet.model_data, packet.world, world.seed);
|
|
162
188
|
result.model = world.model.apply(result.decoded, {full: Boolean(packet.full)});
|
|
163
189
|
if (result.model?.error && packet.full && previousModel) world.model = previousModel;
|