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.
Files changed (41) hide show
  1. package/README.md +22 -57
  2. package/dist/browser/dredless.global.js +827 -1058
  3. package/dist/browser/dredless.global.js.map +4 -4
  4. package/docs/ACCOUNT.md +146 -0
  5. package/docs/API_REFERENCE.md +63 -2550
  6. package/docs/BROWSER_API.md +151 -70
  7. package/docs/CLIENT.md +183 -0
  8. package/docs/EVENTS.md +136 -0
  9. package/docs/GETTING_STARTED.md +135 -0
  10. package/docs/MACHINES.md +351 -0
  11. package/docs/MANAGEMENT.md +285 -0
  12. package/docs/NODE_API.md +120 -318
  13. package/docs/PLAYER_INVENTORY.md +235 -0
  14. package/docs/PROTOCOL.md +227 -0
  15. package/docs/README.md +44 -0
  16. package/docs/TROUBLESHOOTING.md +91 -0
  17. package/docs/WORLD_STATE.md +183 -0
  18. package/package.json +1 -2
  19. package/src/browser.d.ts +31 -1
  20. package/src/browser.js +10 -0
  21. package/src/client/core.js +302 -15
  22. package/src/client/domains/index.js +57 -7
  23. package/src/client/index.d.ts +1 -1
  24. package/src/index.d.ts +303 -14
  25. package/src/index.js +10 -0
  26. package/src/network/account.js +182 -0
  27. package/src/network/disconnect.d.ts +2 -0
  28. package/src/network/disconnect.js +53 -0
  29. package/src/network/index.d.ts +1 -0
  30. package/src/network/index.js +1 -0
  31. package/src/network/invites.d.ts +0 -3
  32. package/src/network/invites.js +3 -40
  33. package/src/network/session.d.ts +2 -0
  34. package/src/network/session.js +21 -0
  35. package/src/protocol/outbound/ship-management.d.ts +66 -1
  36. package/src/protocol/outbound/ship-management.js +83 -0
  37. package/src/state/model/decoder.js +101 -56
  38. package/src/state/model/index.d.ts +4 -0
  39. package/src/state/model/loader-config.js +0 -997
  40. package/src/state/model/state.js +28 -80
  41. package/src/state/world/index.js +27 -1
@@ -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
- #loaderConfig = new LoaderConfigTracker();
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
- const changedPilotState = new Map();
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
- changedPilotState.set(changed.entity, Boolean(player?.q107));
459
+ changedZoomState.set(changed.entity, Boolean(player?.q107));
506
460
  }
507
461
  }
508
462
 
509
- const activePilotCount = [...this.table(55).values()].filter((record) => record?.q107).length;
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 (activePilotCount > 0 && record?.q20 === 0 && HELM_TYPE_IDS.has(Number(record?.q32))) {
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) || changedPilotState.size === 0) continue;
520
- this.#helmOccupied.set(changed.entity, [...changedPilotState.values()].some(Boolean));
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(78);
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, this.#loaderConfig, typeId === LOADER_TYPE_ID, itemHolderRecord)
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, loaderRecord, this.#navigationUnitAutoWarp.get(entityId));
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
 
@@ -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) world.model = new ModelState({isOverworld: world.isOverworld});
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;