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.
- package/README.md +22 -57
- package/dist/browser/dredless.global.js +794 -98
- package/dist/browser/dredless.global.js.map +4 -4
- package/docs/ACCOUNT.md +146 -0
- package/docs/API_REFERENCE.md +63 -2563
- 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 +299 -13
- 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 +70 -10
- package/src/state/model/index.d.ts +4 -0
- package/src/state/model/state.js +22 -7
- 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: "
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
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,
|
package/src/state/model/state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
459
|
+
changedZoomState.set(changed.entity, Boolean(player?.q107));
|
|
445
460
|
}
|
|
446
461
|
}
|
|
447
462
|
|
|
448
|
-
const
|
|
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 (
|
|
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) ||
|
|
459
|
-
this.#helmOccupied.set(changed.entity, [...
|
|
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));
|
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;
|