dredless 0.7.0 → 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 (40) hide show
  1. package/README.md +22 -57
  2. package/dist/browser/dredless.global.js +794 -98
  3. package/dist/browser/dredless.global.js.map +4 -4
  4. package/docs/ACCOUNT.md +146 -0
  5. package/docs/API_REFERENCE.md +63 -2563
  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 +299 -13
  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 +70 -10
  38. package/src/state/model/index.d.ts +4 -0
  39. package/src/state/model/state.js +22 -7
  40. package/src/state/world/index.js +27 -1
@@ -253,6 +253,7 @@ export const MODEL_TABLE_SPECS = new Map([
253
253
  }],
254
254
  [55, {
255
255
  name: "player",
256
+ defaults: {q20: 1, q60: 10, q64: 10, q68: 10},
256
257
  packedBeforeValues: true,
257
258
  fields: numericFields({
258
259
  256: 20, 512: 24, 1024: 28, 2048: 32, 4096: 36, 8192: 40,
@@ -273,8 +274,10 @@ export const MODEL_TABLE_SPECS = new Map([
273
274
  ]
274
275
  }],
275
276
  [56, {
276
- name: "numeric_snapshot",
277
- fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 64: 40, 256: 44, 1024: 48})
277
+ name: "player_appearance",
278
+ // Official StateOutfit: four body colors, then four color/style pairs.
279
+ defaults: {q20: 0, q24: 0, q28: 0, q32: 0, q36: 0, q40: 0, q44: 0, q48: 0, q52: 0, q56: 0, q60: 0, q64: 0},
280
+ fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52, 512: 56, 1024: 60, 2048: 64})
278
281
  }],
279
282
  [59, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
280
283
  [60, {name: "fluid_tank", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
@@ -665,7 +668,9 @@ export function summarizeCannon(entity, record, typeId = null, transformRecord =
665
668
  ? null
666
669
  : (((mountingAngleRadians + relativeAngleRadians) % tau) + tau) % tau;
667
670
  const ammoItemId = record.q24 == null ? null : CANNON_AMMO_COLOR_ITEM_IDS.get(record.q24) ?? null;
668
- const charge = record.q40 ?? null;
671
+ const indicatorValue = (record.q40 ?? 0) / 50;
672
+ const indicatorVisible = indicatorValue >= 1;
673
+ const indicatorBlinking = indicatorValue === 4;
669
674
  const recoil = record.q36 ?? null;
670
675
  const recoil2 = record.q48 ?? null;
671
676
  return {
@@ -680,14 +685,24 @@ export function summarizeCannon(entity, record, typeId = null, transformRecord =
680
685
  recoil,
681
686
  recoil2,
682
687
  recoils: [recoil, recoil2],
683
- charge,
684
- charged: charge == null ? null : charge >= 50,
688
+ indicatorValue,
689
+ indicatorVisible,
690
+ indicatorColor: !indicatorVisible ? null : indicatorBlinking ? 0xff0000 : cannonIndicatorColor(indicatorValue),
691
+ indicatorBlinking,
692
+ indicatorBlinkColor: indicatorBlinking ? 0xffff00 : null,
685
693
  spin: record.q52 ?? null,
686
- coolingCellCount: record.q56 ?? 0,
694
+ resourceCount: record.q56 ?? 0,
687
695
  state: cloneRecord(record)
688
696
  };
689
697
  }
690
698
 
699
+ function cannonIndicatorColor(value) {
700
+ // Match the official renderer's float32 arithmetic before truncating to a byte.
701
+ const distance = Math.abs(Math.fround(2 - Math.fround(value)));
702
+ const channel = Math.max(0, Math.min(255, Math.trunc(Math.fround(Math.fround(1 - distance) * 255))));
703
+ return value < 2 ? (channel << 16) | 0x00ff00 : 0xff0000 | (channel << 8);
704
+ }
705
+
691
706
  export function summarizeThruster(entity, typeId, record) {
692
707
  if (!THRUSTER_TYPE_IDS.has(typeId)) return null;
693
708
  const facing = Number.isFinite(Number(record?.q20)) ? Number(record.q20) : 0;
@@ -1357,7 +1372,37 @@ export function summarizePlayerPreview(entity, record, blueprintItems = []) {
1357
1372
  };
1358
1373
  }
1359
1374
 
1360
- export function summarizePlayer(entity, record, previewRecord = null, blueprintItems = []) {
1375
+ function summarizePlayerAppearance(record, player) {
1376
+ if (!record) return null;
1377
+ const style = (colorOffset, styleOffset) => ({
1378
+ color: record[qKey(colorOffset)] ?? 0,
1379
+ styleId: record[qKey(styleOffset)] ?? 0
1380
+ });
1381
+ const override = (offset) => {
1382
+ const id = player[qKey(offset)] ?? 0;
1383
+ return id === 0 ? null : {itemId: id, itemName: entityNameFromType(id)};
1384
+ };
1385
+ return {
1386
+ bodyColor: record.q20 ?? 0,
1387
+ legsColor: record.q24 ?? 0,
1388
+ skinColor: record.q28 ?? 0,
1389
+ feetColor: record.q32 ?? 0,
1390
+ hair: style(36, 40),
1391
+ hat: style(44, 48),
1392
+ face1: style(52, 56),
1393
+ face2: style(60, 64),
1394
+ itemOverrides: {
1395
+ back: override(36),
1396
+ hands: override(40),
1397
+ feet: override(44),
1398
+ head1: override(48),
1399
+ head2: override(52)
1400
+ },
1401
+ state: cloneRecord(record)
1402
+ };
1403
+ }
1404
+
1405
+ export function summarizePlayer(entity, record, previewRecord = null, blueprintItems = [], appearanceRecord = null, localPlayerEntity = null) {
1361
1406
  if (!record) return null;
1362
1407
  const rawHeldItemId = record.q28 == null ? null : Number(record.q28);
1363
1408
  const heldItemId = Number.isFinite(rawHeldItemId) && rawHeldItemId !== 0 ? rawHeldItemId : null;
@@ -1368,14 +1413,29 @@ export function summarizePlayer(entity, record, previewRecord = null, blueprintI
1368
1413
  name: decodeText(record.blob92),
1369
1414
  heldItemId,
1370
1415
  heldItemName: entityNameFromType(heldItemId),
1371
- repairTargetDistance: record.q56 == null ? null : record.q56 / 10,
1372
- repairTargetAngle: record.q80 == null ? null : record.q80 / 100,
1416
+ itemMode: record.q32 ?? null,
1417
+ buildMultiplier: numberOrNull(record.q68, 10),
1418
+ heldItemBeamDistance: numberOrNull(record.q56, 10),
1419
+ heldItemAngle: numberOrNull(record.q80, 200),
1373
1420
  shipRank: PLAYER_SHIP_RANKS.get(teamRank) || null,
1374
1421
  patronTier: patronTierName(gameRank),
1375
1422
  isDeveloper: gameRank === 1,
1376
1423
  isPatron: gameRank >= 2 && gameRank <= 6,
1377
- piloting: Boolean(record.q107),
1424
+ isLocal: entity === localPlayerEntity,
1425
+ zoomedOut: Boolean(record.q107),
1378
1426
  muted: Boolean(record.q112),
1427
+ invisible: Boolean(record.q106),
1428
+ cheats: {
1429
+ noclip: Boolean(record.q105),
1430
+ sandboxBuild: Boolean(record.q108)
1431
+ },
1432
+ itemEffectActive: Boolean(record.q111),
1433
+ animation: {
1434
+ facing: record.q20 ?? 1,
1435
+ orientation: record.q24 ?? 0,
1436
+ walking: Boolean(record.q109)
1437
+ },
1438
+ appearance: summarizePlayerAppearance(appearanceRecord, record),
1379
1439
  actionPreview: summarizePlayerPreview(entity, previewRecord, blueprintItems),
1380
1440
  state: cloneRecord(record)
1381
1441
  };
@@ -25,6 +25,10 @@ export {
25
25
  type ShieldGeneratorSummary,
26
26
  type HelmSummary,
27
27
  type PlayerSummary,
28
+ type PlayerAnimationSummary,
29
+ type PlayerAppearanceSummary,
30
+ type PlayerAppearanceStyleSummary,
31
+ type PlayerAppearanceItemSummary,
28
32
  type ShipControlSummary,
29
33
  type ShipWorldMetadataSummary,
30
34
  type ShipShieldSummary,
@@ -122,6 +122,7 @@ const {
122
122
  } = modelDecoder;
123
123
 
124
124
  export class ModelState {
125
+ #localPlayerEntity = null;
125
126
  #blueprintItems = null;
126
127
  #helmOccupied = new Map();
127
128
  #commsStationOccupied = new Map();
@@ -147,6 +148,18 @@ export class ModelState {
147
148
  this.#invalidateDerived();
148
149
  }
149
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
+
150
163
  table(id) {
151
164
  // Shared empty map: #summarizeEntity performs ~32 record() lookups per
152
165
  // entity, most against absent tables, and each miss used to allocate.
@@ -435,28 +448,30 @@ export class ModelState {
435
448
  const table7 = update.sections.find((section) => section.table === 7);
436
449
  if (!table7) return;
437
450
 
438
- 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();
439
454
  for (const section of update.sections) {
440
455
  if (section.table !== 55) continue;
441
456
  for (const changed of section.records) {
442
457
  if (!(changed.mask & 16)) continue;
443
458
  const player = this.record(55, changed.entity);
444
- changedPilotState.set(changed.entity, Boolean(player?.q107));
459
+ changedZoomState.set(changed.entity, Boolean(player?.q107));
445
460
  }
446
461
  }
447
462
 
448
- const activePilotCount = [...this.table(55).values()].filter((record) => record?.q107).length;
463
+ const zoomedOutPlayerCount = [...this.table(55).values()].filter((record) => record?.q107).length;
449
464
  for (const changed of table7.records) {
450
465
  const record = this.record(7, changed.entity);
451
466
  const typeId = entityTypeIdFromRecord(record);
452
467
  if (!HELM_TYPE_IDS.has(typeId)) continue;
453
468
 
454
- 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))) {
455
470
  this.#helmOccupied.set(changed.entity, true);
456
471
  }
457
472
 
458
- if (!(changed.mask & 8) || changedPilotState.size === 0) continue;
459
- 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));
460
475
  }
461
476
  }
462
477
 
@@ -822,7 +837,7 @@ export class ModelState {
822
837
  const shieldGenerator = typeId === 256 ? summarizeShieldGenerator(entityId, shieldRecord, itemHolderRecord, shieldGeneratorBoostRecord) : null;
823
838
  const shieldProjector = typeId === 257 ? summarizeShieldProjector(entityId, shieldProjectorRecord) : null;
824
839
  const blueprintPreview = summarizeBlueprintPreview(entityId, blueprintPreviewRecord, transformRecord);
825
- const player = summarizePlayer(entityId, playerRecord, rec(14), this.#blueprintPreviewItems());
840
+ const player = summarizePlayer(entityId, playerRecord, rec(14), this.#blueprintPreviewItems(), rec(56), this.#localPlayerEntity);
826
841
  const shipControl = summarizeShipControl(entityId, shipControlRecord);
827
842
  const helm = summarizeHelm(entityId, typeId, this.#helmOccupied.get(entityId));
828
843
  const commsStation = summarizeCommsStation(entityId, typeId, commsStationRecord, this.#commsStationOccupied.get(entityId));
@@ -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;