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
@@ -2,8 +2,7 @@ import {decoder} from "../../constants.js";
2
2
  import {
3
3
  LOADER_FILTER_MODE_NAMES,
4
4
  LOADER_POSITION_NAMES,
5
- LOADER_PRIORITY_NAMES,
6
- LoaderConfigTracker
5
+ LOADER_PRIORITY_NAMES
7
6
  } from "./loader-config.js";
8
7
  import {navigationDestinationFromEncodedValue, navigationZoneFromBaseId} from "../overworld.js";
9
8
  import {maybeSolveGeneratorMazeSeed} from "../generator-maze.js";
@@ -254,6 +253,7 @@ export const MODEL_TABLE_SPECS = new Map([
254
253
  }],
255
254
  [55, {
256
255
  name: "player",
256
+ defaults: {q20: 1, q60: 10, q64: 10, q68: 10},
257
257
  packedBeforeValues: true,
258
258
  fields: numericFields({
259
259
  256: 20, 512: 24, 1024: 28, 2048: 32, 4096: 36, 8192: 40,
@@ -274,8 +274,10 @@ export const MODEL_TABLE_SPECS = new Map([
274
274
  ]
275
275
  }],
276
276
  [56, {
277
- name: "numeric_snapshot",
278
- 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})
279
281
  }],
280
282
  [59, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
281
283
  [60, {name: "fluid_tank", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
@@ -284,7 +286,11 @@ export const MODEL_TABLE_SPECS = new Map([
284
286
  [63, {name: "numeric_pair", ...TWO_FIELD_SPEC}],
285
287
  [67, {name: "numeric_single", fields: numericFields({1: 20})}],
286
288
  [69, {
287
- name: "rare_snapshot",
289
+ name: "loader_config",
290
+ // The official loader component starts with these values. Wire section
291
+ // 162 sends a packed absolute flag word before the five numeric deltas.
292
+ defaults: {q20: 3, q24: 4, q28: 1, q32: 16, q36: 20, q41: 0, q42: 0},
293
+ packedBeforeValues: true,
288
294
  packedBits: [{bit: 32, offset: 41}, {bit: 64, offset: 42}],
289
295
  fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})
290
296
  }],
@@ -407,7 +413,7 @@ export const WIRE_TAG_TABLES = new Map([
407
413
  [158, 75],
408
414
  [160, 76],
409
415
  [161, 77],
410
- [162, 78],
416
+ [162, 69], // Loader configuration; distinct from navigation state (168).
411
417
  [163, 72],
412
418
  [164, 73],
413
419
  [165, 74],
@@ -574,32 +580,6 @@ export function firstRecord(records, predicate = null) {
574
580
  return null;
575
581
  }
576
582
 
577
- export const TABLE_78_FIELD_BITS = [
578
- ["q20", 1],
579
- ["q24", 2],
580
- ["q28", 4],
581
- ["q32", 8],
582
- ["q36", 16],
583
- ["q40", 32],
584
- ["q44", 64]
585
- ];
586
-
587
- export function table78DeltaRecord(changed) {
588
- const record = {lastMask: changed.mask};
589
- for (const [field, bit] of TABLE_78_FIELD_BITS) {
590
- if (!(changed.mask & bit)) continue;
591
- if (field === "q44" && ((changed.mask & 32) || changed.record?.q44 == null)) continue;
592
- record[field] = (changed.record?.[field] ?? 0) - (changed.previous?.[field] ?? 0);
593
- }
594
- return record;
595
- }
596
-
597
- export function isSemanticLoaderDelta(changed, full) {
598
- return !full &&
599
- changed.previous != null &&
600
- (changed.mask & 96) === 96;
601
- }
602
-
603
583
  export function decodeText(blob) {
604
584
  if (!(blob instanceof Uint8Array)) return null;
605
585
  try {
@@ -679,10 +659,18 @@ export function summarizeCargoEjector(entity, typeId, record) {
679
659
  };
680
660
  }
681
661
 
682
- export function summarizeCannon(entity, record, typeId = null) {
662
+ export function summarizeCannon(entity, record, typeId = null, transformRecord = null) {
683
663
  if (!record || !CANNON_TYPE_IDS.has(typeId)) return null;
664
+ const relativeAngleRadians = numberOrNull(record.q32, 200);
665
+ const mountingAngleRadians = numberOrNull(transformRecord?.q28, 127.324);
666
+ const tau = 2 * Math.PI;
667
+ const absoluteAngleRadians = relativeAngleRadians == null || mountingAngleRadians == null
668
+ ? null
669
+ : (((mountingAngleRadians + relativeAngleRadians) % tau) + tau) % tau;
684
670
  const ammoItemId = record.q24 == null ? null : CANNON_AMMO_COLOR_ITEM_IDS.get(record.q24) ?? null;
685
- const charge = record.q40 ?? null;
671
+ const indicatorValue = (record.q40 ?? 0) / 50;
672
+ const indicatorVisible = indicatorValue >= 1;
673
+ const indicatorBlinking = indicatorValue === 4;
686
674
  const recoil = record.q36 ?? null;
687
675
  const recoil2 = record.q48 ?? null;
688
676
  return {
@@ -692,18 +680,29 @@ export function summarizeCannon(entity, record, typeId = null) {
692
680
  ammoItemId,
693
681
  ammoName: entityNameFromType(ammoItemId),
694
682
  ammoCount: record.q28 ?? 0,
695
- aim: record.q32 ?? null,
683
+ relativeAngleRadians,
684
+ absoluteAngleRadians,
696
685
  recoil,
697
686
  recoil2,
698
687
  recoils: [recoil, recoil2],
699
- charge,
700
- 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,
701
693
  spin: record.q52 ?? null,
702
- coolingCellCount: record.q56 ?? 0,
694
+ resourceCount: record.q56 ?? 0,
703
695
  state: cloneRecord(record)
704
696
  };
705
697
  }
706
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
+
707
706
  export function summarizeThruster(entity, typeId, record) {
708
707
  if (!THRUSTER_TYPE_IDS.has(typeId)) return null;
709
708
  const facing = Number.isFinite(Number(record?.q20)) ? Number(record.q20) : 0;
@@ -1142,15 +1141,14 @@ export function summarizeHugeThruster(entity, markerRecord, sizeRecord = null) {
1142
1141
  };
1143
1142
  }
1144
1143
 
1145
- export function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null, filterSlotsRecord = null, tracker = null, includeDefaults = false, itemHolderRecord = null) {
1144
+ export function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null, filterSlotsRecord = null, includeDefaults = false, itemHolderRecord = null) {
1146
1145
  if (!loaderRecord && !loaderFilterRecord && !includeDefaults) return null;
1147
- const config = tracker?.getConfig(null, entity, loaderRecord, loaderFilterRecord, filterSlotsRecord) ?? {};
1148
- const hasLoaderState = Boolean(loaderRecord);
1149
- const hasPositionConfig = Boolean(tracker?.hasPositionConfig(null, entity));
1150
- const pick = hasLoaderState && hasPositionConfig ? config.pick ?? null : null;
1151
- const place = hasLoaderState && hasPositionConfig ? config.place ?? null : null;
1152
- const priority = config.priority ?? 0;
1153
- const filterMode = config.filterMode ?? null;
1146
+ // Match the official component defaults, then apply the already decoded state.
1147
+ const loader = {...MODEL_TABLE_SPECS.get(69).defaults, ...loaderRecord};
1148
+ const pick = loader.q20;
1149
+ const place = loader.q24;
1150
+ const priority = loader.q28 - 1;
1151
+ const filterMode = loaderFilterRecord?.q20 ?? 0;
1154
1152
  const heldItem = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24 ?? null);
1155
1153
  return {
1156
1154
  entity,
@@ -1160,13 +1158,15 @@ export function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null,
1160
1158
  placeName: enumValueName(LOADER_POSITION_NAMES, place),
1161
1159
  priority,
1162
1160
  priorityName: enumValueName(LOADER_PRIORITY_NAMES, priority),
1163
- requireOutput: config.requireOutput ?? false,
1164
- waitForStack: config.waitForStack ?? false,
1165
- stack: config.stack ?? 16,
1166
- cycle: Math.max(1, config.cycle ?? 1),
1161
+ requireOutput: Boolean(loader.q41),
1162
+ waitForStack: Boolean(loader.q42),
1163
+ stack: loader.q32,
1164
+ cycle: loader.q36 / 20,
1167
1165
  filterMode,
1168
1166
  filterModeName: enumValueName(LOADER_FILTER_MODE_NAMES, filterMode),
1169
- filterSlots: config.filterSlots ?? null,
1167
+ filterSlots: filterSlotsRecord
1168
+ ? [filterSlotsRecord.q20, filterSlotsRecord.q24, filterSlotsRecord.q28].map(value => value || null)
1169
+ : null,
1170
1170
  heldItemId: heldItem.itemId,
1171
1171
  heldItemName: heldItem.itemName,
1172
1172
  heldCount: heldItem.count,
@@ -1206,7 +1206,7 @@ export function summarizePusher(entity, pusherRecord, filterSlotsRecord = null)
1206
1206
  filteredModeName: enumValueName(PUSHER_MODE_NAMES, filteredMode),
1207
1207
  angle: numberOrNull(pusherRecord.q28, 10),
1208
1208
  speed: pusherRecord.q32 == null ? 20 : (pusherRecord.q32 / 100) + 20,
1209
- length: pusherRecord.q40 == null ? 1000 : ((pusherRecord.q40 + 50) / 10) + 995,
1209
+ length: 1000 + (pusherRecord.q40 ?? 0) / 10,
1210
1210
  filterInventory: pusherRecord.q36 === 1,
1211
1211
  filterSlots: filterSlotsRecord ? [filterSlotsRecord.q20 ?? null, filterSlotsRecord.q24 ?? null, filterSlotsRecord.q28 ?? null] : null,
1212
1212
  state: cloneRecord(pusherRecord),
@@ -1372,7 +1372,37 @@ export function summarizePlayerPreview(entity, record, blueprintItems = []) {
1372
1372
  };
1373
1373
  }
1374
1374
 
1375
- 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) {
1376
1406
  if (!record) return null;
1377
1407
  const rawHeldItemId = record.q28 == null ? null : Number(record.q28);
1378
1408
  const heldItemId = Number.isFinite(rawHeldItemId) && rawHeldItemId !== 0 ? rawHeldItemId : null;
@@ -1383,14 +1413,29 @@ export function summarizePlayer(entity, record, previewRecord = null, blueprintI
1383
1413
  name: decodeText(record.blob92),
1384
1414
  heldItemId,
1385
1415
  heldItemName: entityNameFromType(heldItemId),
1386
- repairTargetDistance: record.q56 == null ? null : record.q56 / 10,
1387
- 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),
1388
1420
  shipRank: PLAYER_SHIP_RANKS.get(teamRank) || null,
1389
1421
  patronTier: patronTierName(gameRank),
1390
1422
  isDeveloper: gameRank === 1,
1391
1423
  isPatron: gameRank >= 2 && gameRank <= 6,
1392
- piloting: Boolean(record.q107),
1424
+ isLocal: entity === localPlayerEntity,
1425
+ zoomedOut: Boolean(record.q107),
1393
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),
1394
1439
  actionPreview: summarizePlayerPreview(entity, previewRecord, blueprintItems),
1395
1440
  state: cloneRecord(record)
1396
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,