dredless 0.2.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/LICENSE +21 -0
- package/README.md +48 -0
- package/docs/API_REFERENCE.md +2481 -0
- package/docs/BROWSER_API.md +98 -0
- package/docs/NODE_API.md +359 -0
- package/package.json +173 -0
- package/src/browser.d.ts +212 -0
- package/src/browser.js +133 -0
- package/src/client/core.js +1202 -0
- package/src/client/domains/index.js +1818 -0
- package/src/client/index.d.ts +2 -0
- package/src/client/index.js +1 -0
- package/src/compression/lz4.d.ts +1 -0
- package/src/compression/lz4.js +112 -0
- package/src/constants.js +74 -0
- package/src/crypto/chacha-native.d.ts +9 -0
- package/src/crypto/chacha-native.js +48 -0
- package/src/crypto/chacha.d.ts +25 -0
- package/src/crypto/chacha.js +274 -0
- package/src/data/item-schema.js +2235 -0
- package/src/data/official-client-data.js +156 -0
- package/src/events.js +39 -0
- package/src/index.d.ts +2277 -0
- package/src/index.js +125 -0
- package/src/network/browser-session.js +291 -0
- package/src/network/connection.d.ts +1 -0
- package/src/network/connection.js +31 -0
- package/src/network/cookies.js +85 -0
- package/src/network/http.js +34 -0
- package/src/network/index.d.ts +4 -0
- package/src/network/index.js +4 -0
- package/src/network/servers.d.ts +7 -0
- package/src/network/servers.js +133 -0
- package/src/network/session.d.ts +7 -0
- package/src/network/session.js +320 -0
- package/src/network/ships.d.ts +12 -0
- package/src/network/ships.js +73 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +8 -0
- package/src/protocol/codec/binary.d.ts +3 -0
- package/src/protocol/codec/binary.js +52 -0
- package/src/protocol/codec/msgpack.d.ts +3 -0
- package/src/protocol/codec/msgpack.js +555 -0
- package/src/protocol/inbound/frame.d.ts +1 -0
- package/src/protocol/inbound/frame.js +8 -0
- package/src/protocol/inbound/world-payload.d.ts +3 -0
- package/src/protocol/inbound/world-payload.js +21 -0
- package/src/protocol/index.d.ts +11 -0
- package/src/protocol/index.js +19 -0
- package/src/protocol/outbound/blueprint.d.ts +22 -0
- package/src/protocol/outbound/blueprint.js +36 -0
- package/src/protocol/outbound/commands.d.ts +3 -0
- package/src/protocol/outbound/commands.js +149 -0
- package/src/protocol/outbound/comms.d.ts +28 -0
- package/src/protocol/outbound/comms.js +40 -0
- package/src/protocol/outbound/inventory.d.ts +64 -0
- package/src/protocol/outbound/inventory.js +147 -0
- package/src/protocol/outbound/ship-management.d.ts +84 -0
- package/src/protocol/outbound/ship-management.js +221 -0
- package/src/protocol/outbound/sign.d.ts +18 -0
- package/src/protocol/outbound/sign.js +45 -0
- package/src/protocol/outbound/ui-config.d.ts +157 -0
- package/src/protocol/outbound/ui-config.js +578 -0
- package/src/runtime.js +36 -0
- package/src/state/generator-maze.d.ts +32 -0
- package/src/state/generator-maze.js +370 -0
- package/src/state/index.d.ts +3 -0
- package/src/state/index.js +3 -0
- package/src/state/items.js +20 -0
- package/src/state/model/decoder.js +1554 -0
- package/src/state/model/index.d.ts +33 -0
- package/src/state/model/index.js +2 -0
- package/src/state/model/loader-config.js +1020 -0
- package/src/state/model/reader.js +69 -0
- package/src/state/model/state.js +1257 -0
- package/src/state/overworld.js +72 -0
- package/src/state/tilesets.js +53 -0
- package/src/state/world/index.d.ts +29 -0
- package/src/state/world/index.js +180 -0
- package/src/state/world/state.js +297 -0
|
@@ -0,0 +1,1554 @@
|
|
|
1
|
+
import {decoder} from "../../constants.js";
|
|
2
|
+
import {
|
|
3
|
+
LOADER_FILTER_MODE_NAMES,
|
|
4
|
+
LOADER_POSITION_NAMES,
|
|
5
|
+
LOADER_PRIORITY_NAMES,
|
|
6
|
+
LoaderConfigTracker
|
|
7
|
+
} from "./loader-config.js";
|
|
8
|
+
import {navigationDestinationFromEncodedValue, navigationZoneFromBaseId} from "../overworld.js";
|
|
9
|
+
import {maybeSolveGeneratorMazeSeed} from "../generator-maze.js";
|
|
10
|
+
import {itemNameFromId} from "../items.js";
|
|
11
|
+
import {ModelReader} from "./reader.js";
|
|
12
|
+
|
|
13
|
+
export {ModelReader} from "./reader.js";
|
|
14
|
+
|
|
15
|
+
export function defineLazyProperty(target, key, compute) {
|
|
16
|
+
let cached = null;
|
|
17
|
+
let resolved = false;
|
|
18
|
+
Object.defineProperty(target, key, {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
get() {
|
|
22
|
+
if (!resolved) {
|
|
23
|
+
cached = compute();
|
|
24
|
+
resolved = true;
|
|
25
|
+
}
|
|
26
|
+
return cached;
|
|
27
|
+
},
|
|
28
|
+
set(value) {
|
|
29
|
+
cached = value;
|
|
30
|
+
resolved = true;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// string on every field of every record while decoding.
|
|
37
|
+
export function numericFields(bitsToOffsets) {
|
|
38
|
+
return Object.entries(bitsToOffsets)
|
|
39
|
+
.map(([bit, offset]) => ({bit: Number(bit), offset, key: qKey(offset)}))
|
|
40
|
+
.sort((a, b) => a.bit - b.bit);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const TWO_FIELD_SPEC = {
|
|
44
|
+
fields: numericFields({1: 20, 2: 24})
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const LABEL_STATE_SPEC = {
|
|
48
|
+
name: "label_state",
|
|
49
|
+
read(reader, record, mask) {
|
|
50
|
+
if (mask & 1) record.blob20 = reader.readBlob();
|
|
51
|
+
if (mask & 2) {
|
|
52
|
+
if (mask & 1) record.blob24 = reader.readBlob();
|
|
53
|
+
else record.q20 = (record.q20 || 0) + reader.readFieldDelta();
|
|
54
|
+
}
|
|
55
|
+
if (mask & 4) record.q28 = (record.q28 || 0) + reader.readFieldDelta();
|
|
56
|
+
if (mask & 8) {
|
|
57
|
+
if (mask & 1) record.q32 = (record.q32 || 0) + reader.readFieldDelta();
|
|
58
|
+
else record.blob28 = reader.readBlob();
|
|
59
|
+
}
|
|
60
|
+
if (mask & 16) record.q36 = (record.q36 || 0) + reader.readFieldDelta();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const SIMPLE_LABEL_STATE_SPEC = {
|
|
65
|
+
name: "label_state",
|
|
66
|
+
read(reader, record, mask) {
|
|
67
|
+
if (mask & 1) record.blob20 = reader.readBlob();
|
|
68
|
+
if (mask & 2) record.q24 = (record.q24 || 0) + reader.readFieldDelta();
|
|
69
|
+
if (mask & 4) record.q28 = (record.q28 || 0) + reader.readFieldDelta();
|
|
70
|
+
if (mask & 8) record.q32 = (record.q32 || 0) + reader.readFieldDelta();
|
|
71
|
+
if (mask & 16) record.q36 = (record.q36 || 0) + reader.readFieldDelta();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function peekStreamInt(reader) {
|
|
76
|
+
let result = 0;
|
|
77
|
+
let shift = 0;
|
|
78
|
+
let offset = reader.offset;
|
|
79
|
+
while (offset < reader.bytes.length) {
|
|
80
|
+
const byte = reader.bytes[offset++];
|
|
81
|
+
result += (byte & 0x7f) * (2 ** shift);
|
|
82
|
+
if ((byte & 0x80) === 0) {
|
|
83
|
+
return {
|
|
84
|
+
value: (result & 1) === 0 ? result / 2 : -((result + 1) / 2),
|
|
85
|
+
offset
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
shift += 7;
|
|
89
|
+
if (shift > 53) return null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function nextValueLooksLikeTextBlob(reader) {
|
|
95
|
+
const peek = peekStreamInt(reader);
|
|
96
|
+
if (!peek || peek.value <= 0) return false;
|
|
97
|
+
const end = peek.offset + peek.value;
|
|
98
|
+
if (end > reader.bytes.length) return false;
|
|
99
|
+
const blob = reader.bytes.slice(peek.offset, end);
|
|
100
|
+
try {
|
|
101
|
+
const text = decoder.decode(blob);
|
|
102
|
+
return text.length > 0 && !/[\u0000-\u0008\u000e-\u001f]/.test(text);
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const MODEL_TABLE_SPECS = new Map([
|
|
109
|
+
[0, {
|
|
110
|
+
name: "transform",
|
|
111
|
+
packedBits: [{bit: 8, offset: 33}, {bit: 16, offset: 34}, {bit: 32, offset: 35}],
|
|
112
|
+
fields: numericFields({1: 20, 2: 24, 4: 28}),
|
|
113
|
+
scale(record) {
|
|
114
|
+
return {
|
|
115
|
+
x: numberOrNull(record.q20, 40),
|
|
116
|
+
y: numberOrNull(record.q24, 40),
|
|
117
|
+
rot: numberOrNull(record.q28, 127.324)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}],
|
|
121
|
+
[1, {
|
|
122
|
+
name: "body_state",
|
|
123
|
+
fields: numericFields({
|
|
124
|
+
1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48,
|
|
125
|
+
256: 52, 512: 56, 1024: 60, 2048: 64, 4096: 68, 8192: 72,
|
|
126
|
+
16384: 76, 32768: 80, 65536: 84, 131072: 88, 262144: 92,
|
|
127
|
+
524288: 96, 1048576: 100, 2097152: 104
|
|
128
|
+
})
|
|
129
|
+
}],
|
|
130
|
+
[2, {name: "numeric_pair", ...TWO_FIELD_SPEC}],
|
|
131
|
+
[3, {name: "gate_width", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40})}],
|
|
132
|
+
[4, {name: "motion_aux", packedBits: [{bit: 8, offset: 33}], fields: numericFields({1: 20, 2: 24, 4: 28})}],
|
|
133
|
+
[5, {name: "entity_health", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32})}],
|
|
134
|
+
[6, {name: "item_holder", ...TWO_FIELD_SPEC}],
|
|
135
|
+
[7, {
|
|
136
|
+
name: "entity_type",
|
|
137
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52})
|
|
138
|
+
}],
|
|
139
|
+
[8, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40})}],
|
|
140
|
+
[9, LABEL_STATE_SPEC],
|
|
141
|
+
[10, SIMPLE_LABEL_STATE_SPEC],
|
|
142
|
+
[11, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
|
|
143
|
+
[12, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32})}],
|
|
144
|
+
[14, {name: "size_state", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44})}],
|
|
145
|
+
[16, {
|
|
146
|
+
name: "label_numeric_state",
|
|
147
|
+
read(reader, record, mask) {
|
|
148
|
+
if ((mask & 1) && nextValueLooksLikeTextBlob(reader)) {
|
|
149
|
+
SIMPLE_LABEL_STATE_SPEC.read(reader, record, mask);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (mask & 1) record.q20 = (record.q20 || 0) + reader.readFieldDelta();
|
|
153
|
+
if (mask & 2) {
|
|
154
|
+
if (mask & 1) record.blob24 = reader.readBlob();
|
|
155
|
+
else record.q24 = (record.q24 || 0) + reader.readFieldDelta();
|
|
156
|
+
}
|
|
157
|
+
if (mask & 4) record.q28 = (record.q28 || 0) + reader.readFieldDelta();
|
|
158
|
+
if (mask & 8) record.q32 = (record.q32 || 0) + reader.readFieldDelta();
|
|
159
|
+
if (mask & 16) record.q36 = (record.q36 || 0) + reader.readFieldDelta();
|
|
160
|
+
}
|
|
161
|
+
}],
|
|
162
|
+
[17, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
|
|
163
|
+
[18, {
|
|
164
|
+
name: "mob_combat_state",
|
|
165
|
+
fields: numericFields({
|
|
166
|
+
2: 20,
|
|
167
|
+
4: 24,
|
|
168
|
+
8: 28,
|
|
169
|
+
16: 32,
|
|
170
|
+
32: 36,
|
|
171
|
+
64: 40,
|
|
172
|
+
128: 44,
|
|
173
|
+
256: 48,
|
|
174
|
+
512: 52,
|
|
175
|
+
1024: 56,
|
|
176
|
+
2048: 60,
|
|
177
|
+
4096: 64,
|
|
178
|
+
8192: 68,
|
|
179
|
+
16384: 72,
|
|
180
|
+
32768: 76,
|
|
181
|
+
65536: 80,
|
|
182
|
+
131072: 84,
|
|
183
|
+
524288: 88,
|
|
184
|
+
4194304: 92
|
|
185
|
+
})
|
|
186
|
+
}],
|
|
187
|
+
[19, {name: "motion_state", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
|
|
188
|
+
[20, {
|
|
189
|
+
name: "ship_control",
|
|
190
|
+
packedBeforeValues: true,
|
|
191
|
+
orderedValues: true,
|
|
192
|
+
packedBits: [{bit: 0x1000, offset: 153}],
|
|
193
|
+
fields: numericFields({
|
|
194
|
+
1: 20, 2: 24, 4: 28, 32: 32, 64: 36, 128: 40, 256: 44, 512: 48,
|
|
195
|
+
1024: 52, 2048: 56, 8192: 60, 16384: 64, 32768: 68, 65536: 72,
|
|
196
|
+
131072: 76, 262144: 80, 524288: 84, 1048576: 88, 2097152: 92,
|
|
197
|
+
4194304: 96, 8388608: 100, 16777216: 104, 67108864: 108, 134217728: 112
|
|
198
|
+
}),
|
|
199
|
+
blobs: [
|
|
200
|
+
{bit: 8, offset: 116},
|
|
201
|
+
{bit: 16, offset: 128},
|
|
202
|
+
{bit: 33554432, offset: 140}
|
|
203
|
+
],
|
|
204
|
+
scale(record) {
|
|
205
|
+
return {
|
|
206
|
+
thrustX: numberOrNull(record.q24, 20),
|
|
207
|
+
thrustY: numberOrNull(record.q28, 20),
|
|
208
|
+
value52: numberOrNull(record.q52, 20)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}],
|
|
212
|
+
[21, {
|
|
213
|
+
name: "gate_portal",
|
|
214
|
+
orderedValues: true,
|
|
215
|
+
fields: numericFields({1: 20, 2: 24}),
|
|
216
|
+
blobs: [{bit: 4, offset: 28}, {bit: 8, offset: 40}]
|
|
217
|
+
}],
|
|
218
|
+
[24, {name: "projectile_state", packedBits: [{bit: 8, offset: 33}], fields: numericFields({1: 20, 2: 24, 4: 28})}],
|
|
219
|
+
[25, {
|
|
220
|
+
name: "zone_label",
|
|
221
|
+
orderedValues: true,
|
|
222
|
+
fields: numericFields({1: 20, 2: 24}),
|
|
223
|
+
blobs: [{bit: 4, offset: 28}, {bit: 8, offset: 40}]
|
|
224
|
+
}],
|
|
225
|
+
[26, {name: "numeric_single", fields: numericFields({1: 20})}],
|
|
226
|
+
[31, {name: "numeric_single", fields: numericFields({1: 20})}],
|
|
227
|
+
[37, {name: "loose_item_marker", packedBits: [{bit: 2, offset: 29}], fields: numericFields({1: 20, 4: 24})}],
|
|
228
|
+
[38, {name: "flag_state", packedBits: [{bit: 1, offset: 21}], fields: []}],
|
|
229
|
+
[39, {name: "comms_transmit", ...TWO_FIELD_SPEC}],
|
|
230
|
+
[41, {name: "blob_state", fields: numericFields({2: 20}), blobs: [{bit: 1, offset: 24}]}],
|
|
231
|
+
[42, {name: "numeric_pair", ...TWO_FIELD_SPEC}],
|
|
232
|
+
[43, {name: "numeric_single", fields: numericFields({1: 20})}],
|
|
233
|
+
[44, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32})}],
|
|
234
|
+
[45, {name: "numeric_flag", packedBits: [{bit: 1, offset: 25}], fields: numericFields({2: 20})}],
|
|
235
|
+
[47, {name: "flag_state", packedBits: [{bit: 1, offset: 21}], fields: []}],
|
|
236
|
+
[49, {name: "processor_cycle", packedBits: [{bit: 2, offset: 33}], fields: numericFields({1: 20, 4: 24, 8: 28})}],
|
|
237
|
+
[50, {
|
|
238
|
+
name: "rare_snapshot",
|
|
239
|
+
packedBits: [{bit: 2, offset: 33}, {bit: 8, offset: 34}, {bit: 32, offset: 35}],
|
|
240
|
+
fields: numericFields({1: 20, 4: 24, 16: 28})
|
|
241
|
+
}],
|
|
242
|
+
[51, {
|
|
243
|
+
name: "numeric_sparse",
|
|
244
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52})
|
|
245
|
+
}],
|
|
246
|
+
[53, {
|
|
247
|
+
name: "fabricator",
|
|
248
|
+
packedBits: [{bit: 2, offset: 61}],
|
|
249
|
+
fields: numericFields({1: 20, 4: 24, 8: 28, 16: 32, 32: 36, 64: 40, 128: 44, 256: 48, 512: 52, 1024: 56})
|
|
250
|
+
}],
|
|
251
|
+
[54, {
|
|
252
|
+
name: "starter_cannon",
|
|
253
|
+
fields: numericFields({1: 20, 2: 32, 4: 24, 8: 28, 16: 44, 32: 36, 64: 48, 128: 52, 256: 40, 512: 56, 1024: 60})
|
|
254
|
+
}],
|
|
255
|
+
[55, {
|
|
256
|
+
name: "player",
|
|
257
|
+
packedBeforeValues: true,
|
|
258
|
+
fields: numericFields({
|
|
259
|
+
256: 20, 512: 24, 1024: 28, 2048: 32, 4096: 36, 8192: 40,
|
|
260
|
+
16384: 44, 32768: 48, 65536: 52, 131072: 56, 262144: 60,
|
|
261
|
+
524288: 64, 1048576: 68, 2097152: 72, 4194304: 76, 8388608: 80,
|
|
262
|
+
33554432: 84, 67108864: 88
|
|
263
|
+
}),
|
|
264
|
+
blobs: [{bit: 1, offset: 92}],
|
|
265
|
+
packedBits: [
|
|
266
|
+
{bit: 2, offset: 105},
|
|
267
|
+
{bit: 4, offset: 106},
|
|
268
|
+
{bit: 16, offset: 107},
|
|
269
|
+
{bit: 32, offset: 108},
|
|
270
|
+
{bit: 64, offset: 109},
|
|
271
|
+
{bit: 128, offset: 110},
|
|
272
|
+
{bit: 16777216, offset: 111},
|
|
273
|
+
{bit: 134217728, offset: 112}
|
|
274
|
+
]
|
|
275
|
+
}],
|
|
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})
|
|
279
|
+
}],
|
|
280
|
+
[59, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
|
|
281
|
+
[60, {name: "fluid_tank", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
|
|
282
|
+
[61, {name: "shield_charge", ...TWO_FIELD_SPEC}],
|
|
283
|
+
[62, {name: "flag_state", packedBits: [{bit: 1, offset: 21}], fields: []}],
|
|
284
|
+
[63, {name: "numeric_pair", ...TWO_FIELD_SPEC}],
|
|
285
|
+
[67, {name: "numeric_single", fields: numericFields({1: 20})}],
|
|
286
|
+
[69, {
|
|
287
|
+
name: "rare_snapshot",
|
|
288
|
+
packedBits: [{bit: 32, offset: 41}, {bit: 64, offset: 42}],
|
|
289
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})
|
|
290
|
+
}],
|
|
291
|
+
[70, {
|
|
292
|
+
name: "rare_snapshot",
|
|
293
|
+
packedBits: [{bit: 16, offset: 40}],
|
|
294
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 32: 36})
|
|
295
|
+
}],
|
|
296
|
+
[72, {
|
|
297
|
+
name: "pusher_config",
|
|
298
|
+
packedBits: [{bit: 16, offset: 36}],
|
|
299
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 32: 40})
|
|
300
|
+
}],
|
|
301
|
+
[73, {name: "expando_box_marker", fields: []}],
|
|
302
|
+
[74, {name: "processor_marker", fields: []}],
|
|
303
|
+
[75, {name: "rare_snapshot", fields: numericFields({1: 20, 8: 24, 16: 28})}],
|
|
304
|
+
[76, {name: "numeric_single", fields: numericFields({1: 20})}],
|
|
305
|
+
[77, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28})}],
|
|
306
|
+
[78, {
|
|
307
|
+
name: "local_session_marker",
|
|
308
|
+
read(reader, record, mask) {
|
|
309
|
+
for (const {bit, key} of TABLE_78_NUMERIC_FIELDS) {
|
|
310
|
+
if (!(mask & bit)) continue;
|
|
311
|
+
record[key] = (record[key] || 0) + reader.readFieldDelta();
|
|
312
|
+
}
|
|
313
|
+
if ((mask & 64) && !(mask & 32)) {
|
|
314
|
+
record.q44 = (record.q44 || 0) + reader.readFieldDelta();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}]
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
// Precompute everything about a spec that is fixed at module load. Decoding a
|
|
321
|
+
// full model packet runs #applyRecordSpec thousands of times, and each run used
|
|
322
|
+
// to rebuild record key strings and -- for ordered specs -- an entire merged,
|
|
323
|
+
// sorted item array.
|
|
324
|
+
for (const spec of MODEL_TABLE_SPECS.values()) {
|
|
325
|
+
for (const blob of spec.blobs || []) blob.key = blobKey(blob.offset);
|
|
326
|
+
for (const item of spec.packedBits || []) item.key = qKey(item.offset);
|
|
327
|
+
if (spec.packedOffsets) spec.packedOffsetKeys = spec.packedOffsets.map(qKey);
|
|
328
|
+
if (spec.orderedValues) {
|
|
329
|
+
spec.orderedItems = [
|
|
330
|
+
...(spec.blobs || []).map((item) => ({bit: item.bit, offset: item.offset, key: item.key, kind: "blob"})),
|
|
331
|
+
...(spec.fields || []).map((item) => ({bit: item.bit, offset: item.offset, key: item.key, kind: "field"}))
|
|
332
|
+
].sort((a, b) => a.bit - b.bit);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const TABLE_78_NUMERIC_FIELDS = numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40});
|
|
337
|
+
|
|
338
|
+
export const WIRE_TAG_TABLES = new Map([
|
|
339
|
+
[1, 0],
|
|
340
|
+
[2, 1],
|
|
341
|
+
[3, 2],
|
|
342
|
+
[4, 3],
|
|
343
|
+
[5, 4],
|
|
344
|
+
[26, 9],
|
|
345
|
+
[40, 5],
|
|
346
|
+
[42, 6],
|
|
347
|
+
[43, 7],
|
|
348
|
+
[44, 8],
|
|
349
|
+
[45, 9],
|
|
350
|
+
[46, 10],
|
|
351
|
+
[47, 11],
|
|
352
|
+
[48, 12],
|
|
353
|
+
[49, 13],
|
|
354
|
+
[50, 14],
|
|
355
|
+
[51, 51],
|
|
356
|
+
[52, 16],
|
|
357
|
+
[74, 17],
|
|
358
|
+
[80, 17],
|
|
359
|
+
[81, 18],
|
|
360
|
+
[82, 19],
|
|
361
|
+
[83, 20],
|
|
362
|
+
[84, 21],
|
|
363
|
+
[85, 22],
|
|
364
|
+
[86, 23],
|
|
365
|
+
[87, 24],
|
|
366
|
+
[88, 25],
|
|
367
|
+
[89, 26],
|
|
368
|
+
[90, 27],
|
|
369
|
+
[91, 28],
|
|
370
|
+
[93, 30],
|
|
371
|
+
[94, 31],
|
|
372
|
+
[95, 32],
|
|
373
|
+
[99, 36],
|
|
374
|
+
[120, 37],
|
|
375
|
+
[121, 38],
|
|
376
|
+
[122, 39],
|
|
377
|
+
[123, 40],
|
|
378
|
+
[124, 41],
|
|
379
|
+
[125, 42],
|
|
380
|
+
[126, 43],
|
|
381
|
+
[127, 44],
|
|
382
|
+
[128, 45],
|
|
383
|
+
[129, 46],
|
|
384
|
+
[130, 47],
|
|
385
|
+
[131, 48],
|
|
386
|
+
[132, 49],
|
|
387
|
+
[133, 50],
|
|
388
|
+
[134, 51],
|
|
389
|
+
[135, 52],
|
|
390
|
+
[136, 53],
|
|
391
|
+
[137, 54],
|
|
392
|
+
[138, 55],
|
|
393
|
+
[139, 56],
|
|
394
|
+
[141, 58],
|
|
395
|
+
[142, 59],
|
|
396
|
+
[143, 60],
|
|
397
|
+
[144, 61],
|
|
398
|
+
[145, 62],
|
|
399
|
+
[146, 63],
|
|
400
|
+
[147, 64],
|
|
401
|
+
[148, 65],
|
|
402
|
+
[149, 66],
|
|
403
|
+
[150, 67],
|
|
404
|
+
[151, 68],
|
|
405
|
+
[152, 69],
|
|
406
|
+
[153, 70],
|
|
407
|
+
[158, 75],
|
|
408
|
+
[160, 76],
|
|
409
|
+
[161, 77],
|
|
410
|
+
[162, 78],
|
|
411
|
+
[163, 72],
|
|
412
|
+
[164, 73],
|
|
413
|
+
[165, 74],
|
|
414
|
+
[166, 75],
|
|
415
|
+
[167, 76],
|
|
416
|
+
[168, 78],
|
|
417
|
+
[169, 77]
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
export const MASK_ONLY_TABLES = new Set([13, 22, 23, 27, 28, 30, 32, 36, 40, 46, 48, 52, 58, 64, 65, 66, 68, 73, 74]);
|
|
421
|
+
|
|
422
|
+
export const ENTITY_FOOTPRINTS = new Map([
|
|
423
|
+
[240, {width: 2, height: 2}],
|
|
424
|
+
[247, {width: 2, height: 2}],
|
|
425
|
+
[248, {width: 2, height: 2}],
|
|
426
|
+
[249, {width: 2, height: 2}],
|
|
427
|
+
[251, {width: 2, height: 2}],
|
|
428
|
+
[256, {width: 2, height: 2}],
|
|
429
|
+
[261, {width: 2, height: 2}]
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
export const PLACED_ENTITY_TYPE_IDS = new Set([
|
|
433
|
+
215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 228, 229,
|
|
434
|
+
230, 231, 240, 241, 242, 243, 245, 246, 247, 248, 249, 250, 251, 252,
|
|
435
|
+
253, 255, 256, 257, 258, 259, 260, 261, 263, 264, 265
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
export const FabricatorType = Object.freeze({
|
|
439
|
+
Starter: "starter",
|
|
440
|
+
Munitions: "munitions",
|
|
441
|
+
Engineering: "engineering",
|
|
442
|
+
Equipment: "equipment"
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
export const FABRICATOR_TYPES = new Map([
|
|
446
|
+
[0, FabricatorType.Starter],
|
|
447
|
+
[1, FabricatorType.Munitions],
|
|
448
|
+
[2, FabricatorType.Engineering],
|
|
449
|
+
[3, FabricatorType.Equipment]
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
export const HELM_TYPE_IDS = new Set([215, 216]);
|
|
453
|
+
export const COMMS_STATION_TYPE_ID = 217;
|
|
454
|
+
export const COMMS_STATION_MAX_CHARGES = 5;
|
|
455
|
+
export const CARGO_HATCH_TYPE_IDS = new Set([221, 222]);
|
|
456
|
+
export const THRUSTER_TYPE_IDS = new Set([230, 231]);
|
|
457
|
+
export const EXPANDO_BOX_TYPE_ID = 240;
|
|
458
|
+
export const ITEM_LAUNCHER_TYPE_ID = 243;
|
|
459
|
+
export const LOADER_TYPE_ID = 252;
|
|
460
|
+
export const NAVIGATION_UNIT_TYPE_ID = 261;
|
|
461
|
+
|
|
462
|
+
export function entityTypeIdFromRecord(record) {
|
|
463
|
+
if (!record) return null;
|
|
464
|
+
const candidates = [
|
|
465
|
+
record.q20,
|
|
466
|
+
record.q24,
|
|
467
|
+
record.q28,
|
|
468
|
+
record.q32,
|
|
469
|
+
record.q36,
|
|
470
|
+
record.q40,
|
|
471
|
+
record.q44,
|
|
472
|
+
record.q48,
|
|
473
|
+
record.q52
|
|
474
|
+
].filter((value) => Number.isFinite(Number(value)) && Number(value) !== 0).map(Number);
|
|
475
|
+
return candidates.find((value) => PLACED_ENTITY_TYPE_IDS.has(value)) ?? candidates[0] ?? null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export const CANNON_AMMO_COLOR_ITEM_IDS = new Map([
|
|
479
|
+
[0xff9600, 150]
|
|
480
|
+
]);
|
|
481
|
+
|
|
482
|
+
export const CANNON_TYPE_IDS = new Set([226, 227, 228, 229, 263, 265]);
|
|
483
|
+
|
|
484
|
+
export const THRUSTER_FACING_NAMES = new Map([
|
|
485
|
+
[0, "bottom"],
|
|
486
|
+
[1, "top"],
|
|
487
|
+
[2, "right"],
|
|
488
|
+
[3, "left"],
|
|
489
|
+
[4, "bottom-right"],
|
|
490
|
+
[5, "bottom-left"],
|
|
491
|
+
[6, "top-right"],
|
|
492
|
+
[7, "top-left"]
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
export const DEFAULT_OVERWORLD_WARP_DURATION_SECONDS = 120;
|
|
496
|
+
export const OVERWORLD_WARP_TICKS_PER_SECOND = 20;
|
|
497
|
+
|
|
498
|
+
export const MARKER_TYPE_IDS = new Map([
|
|
499
|
+
[73, 240]
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
export const TEAM_RANK_NAMES = new Map([
|
|
503
|
+
[0, "Guest"],
|
|
504
|
+
[1, "Crew"],
|
|
505
|
+
[2, "CrewInvitePending_DEPRECATED"],
|
|
506
|
+
[3, "Captain"],
|
|
507
|
+
[4, "Banned"]
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
export const PLAYER_SHIP_RANKS = new Map([
|
|
511
|
+
[0, "guest"],
|
|
512
|
+
[1, "crew"],
|
|
513
|
+
[2, "crew-invite-pending-deprecated"],
|
|
514
|
+
[3, "captain"],
|
|
515
|
+
[4, "banned"]
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
export const PUSHER_MODE_NAMES = new Map([
|
|
519
|
+
[0, "Push"],
|
|
520
|
+
[1, "Pull"],
|
|
521
|
+
[2, "Do Nothing"]
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
export const SIGN_DISPLAY_MODE_NAMES = new Map([
|
|
525
|
+
[0, "always"],
|
|
526
|
+
[1, "when-near"],
|
|
527
|
+
[2, "on-hover"]
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
export const SHIELD_GENERATOR_BOOST_STATE_NAMES = new Map([
|
|
531
|
+
[0, "inactive"],
|
|
532
|
+
[1, "boosted"],
|
|
533
|
+
[2, "failed"]
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
export const NAVIGATION_DEFAULT_DESTINATION = 10;
|
|
537
|
+
|
|
538
|
+
export function navigationDestinationName(destination) {
|
|
539
|
+
return navigationZoneFromBaseId(destination)?.key ?? null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function isNavigationDestination(destination) {
|
|
543
|
+
return navigationDestinationName(destination) != null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function numberOrNull(value, divisor = 1) {
|
|
547
|
+
return typeof value === "number" ? value / divisor : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function qKey(offset) {
|
|
551
|
+
return `q${offset}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function blobKey(offset) {
|
|
555
|
+
return `blob${offset}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Blob fields stay expanded to plain arrays: that shape is part of the public
|
|
559
|
+
// record surface. Avoiding the Object.entries pair array is the win available
|
|
560
|
+
// without changing what callers see.
|
|
561
|
+
export function cloneRecord(record) {
|
|
562
|
+
const out = {};
|
|
563
|
+
for (const key in record) {
|
|
564
|
+
const value = record[key];
|
|
565
|
+
out[key] = value instanceof Uint8Array ? Array.from(value) : value;
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function firstRecord(records, predicate = null) {
|
|
571
|
+
for (const record of records.values()) {
|
|
572
|
+
if (!predicate || predicate(record)) return record;
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
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
|
+
export function decodeText(blob) {
|
|
604
|
+
if (!(blob instanceof Uint8Array)) return null;
|
|
605
|
+
try {
|
|
606
|
+
return decoder.decode(blob);
|
|
607
|
+
} catch (_) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function entityNameFromType(typeId) {
|
|
613
|
+
return itemNameFromId(typeId);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function markerTypeIdForTables(tableIds) {
|
|
617
|
+
for (const tableId of tableIds) {
|
|
618
|
+
if (MARKER_TYPE_IDS.has(tableId)) return MARKER_TYPE_IDS.get(tableId);
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function itemSummary(itemId, count = null) {
|
|
624
|
+
const id = itemId == null ? null : Number(itemId);
|
|
625
|
+
const normalizedId = Number.isFinite(id) && id !== 0 ? id : null;
|
|
626
|
+
return {
|
|
627
|
+
itemId: normalizedId,
|
|
628
|
+
itemName: normalizedId == null ? null : entityNameFromType(normalizedId),
|
|
629
|
+
count: count === 0 ? null : count
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function summarizeItemHolder(entity, record) {
|
|
634
|
+
if (!record) return null;
|
|
635
|
+
return {
|
|
636
|
+
entity,
|
|
637
|
+
...itemSummary(record.q20, record.q24 ?? null)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function boundedProgress(value) {
|
|
642
|
+
if (value == null) return null;
|
|
643
|
+
const number = Number(value);
|
|
644
|
+
if (!Number.isFinite(number)) return null;
|
|
645
|
+
return Math.max(0, Math.min(100, number));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export function summarizeFabricator(entity, record, itemHolderRecord = null, typeRecord = null) {
|
|
649
|
+
if (!record) return null;
|
|
650
|
+
const progressRaw = record.q24 ?? null;
|
|
651
|
+
const craftingItem = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24 ?? null);
|
|
652
|
+
return {
|
|
653
|
+
entity,
|
|
654
|
+
type: FABRICATOR_TYPES.get(typeRecord?.q32) ?? null,
|
|
655
|
+
typeIndex: typeRecord?.q32 ?? null,
|
|
656
|
+
state: cloneRecord(record),
|
|
657
|
+
rows: [
|
|
658
|
+
itemSummary(record.q28, record.q40 ?? null),
|
|
659
|
+
itemSummary(record.q32, record.q44 ?? null),
|
|
660
|
+
itemSummary(record.q36, record.q48 ?? null)
|
|
661
|
+
],
|
|
662
|
+
progress: boundedProgress(progressRaw),
|
|
663
|
+
progressRaw,
|
|
664
|
+
active: Boolean(record.q61) || (typeof progressRaw === "number" && progressRaw > 0),
|
|
665
|
+
craftingItemId: craftingItem.itemId,
|
|
666
|
+
craftingItemName: craftingItem.itemName,
|
|
667
|
+
craftingCount: craftingItem.count
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function summarizeCargoEjector(entity, typeId, record) {
|
|
672
|
+
if (typeId !== 223) return null;
|
|
673
|
+
return {
|
|
674
|
+
entity,
|
|
675
|
+
typeId,
|
|
676
|
+
typeName: entityNameFromType(typeId),
|
|
677
|
+
progress: boundedProgress(record?.q24 ?? null),
|
|
678
|
+
active: record?.q33 == null ? null : Boolean(record.q33)
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function summarizeCannon(entity, record, typeId = null) {
|
|
683
|
+
if (!record || !CANNON_TYPE_IDS.has(typeId)) return null;
|
|
684
|
+
const ammoItemId = record.q24 == null ? null : CANNON_AMMO_COLOR_ITEM_IDS.get(record.q24) ?? null;
|
|
685
|
+
const charge = record.q40 ?? null;
|
|
686
|
+
const recoil = record.q36 ?? null;
|
|
687
|
+
const recoil2 = record.q48 ?? null;
|
|
688
|
+
return {
|
|
689
|
+
entity,
|
|
690
|
+
typeId,
|
|
691
|
+
typeName: entityNameFromType(typeId),
|
|
692
|
+
ammoItemId,
|
|
693
|
+
ammoName: entityNameFromType(ammoItemId),
|
|
694
|
+
ammoCount: record.q28 ?? 0,
|
|
695
|
+
aim: record.q32 ?? null,
|
|
696
|
+
recoil,
|
|
697
|
+
recoil2,
|
|
698
|
+
recoils: [recoil, recoil2],
|
|
699
|
+
charge,
|
|
700
|
+
charged: charge == null ? null : charge >= 50,
|
|
701
|
+
spin: record.q52 ?? null,
|
|
702
|
+
coolingCellCount: record.q56 ?? 0,
|
|
703
|
+
state: cloneRecord(record)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function summarizeThruster(entity, typeId, record) {
|
|
708
|
+
if (!THRUSTER_TYPE_IDS.has(typeId)) return null;
|
|
709
|
+
const facing = Number.isFinite(Number(record?.q20)) ? Number(record.q20) : 0;
|
|
710
|
+
return {
|
|
711
|
+
entity,
|
|
712
|
+
typeId,
|
|
713
|
+
typeName: entityNameFromType(typeId),
|
|
714
|
+
facing,
|
|
715
|
+
facingName: enumValueName(THRUSTER_FACING_NAMES, facing),
|
|
716
|
+
fuel: record?.q24 ?? null,
|
|
717
|
+
state: cloneRecord(record || {})
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function summarizeHelm(entity, typeId, occupied = false) {
|
|
722
|
+
if (!HELM_TYPE_IDS.has(typeId)) return null;
|
|
723
|
+
return {
|
|
724
|
+
entity,
|
|
725
|
+
typeId,
|
|
726
|
+
typeName: entityNameFromType(typeId),
|
|
727
|
+
occupied: Boolean(occupied)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function navigationDestinationFromRecord(record, changedMask = record?.lastMask ?? 0) {
|
|
732
|
+
if (!record) return NAVIGATION_DEFAULT_DESTINATION;
|
|
733
|
+
const q32Destination = navigationDestinationFromEncodedValue(record.q32);
|
|
734
|
+
const q36Destination = navigationDestinationFromEncodedValue(record.q36);
|
|
735
|
+
const q24Destination = navigationDestinationFromEncodedValue(record.q24);
|
|
736
|
+
const q20Destination = navigationDestinationFromEncodedValue(record.q20);
|
|
737
|
+
if ((changedMask & 9) === 9 && record.q20 === 0 && record.q24 == null && q32Destination != null) return q32Destination;
|
|
738
|
+
if ((changedMask & 25) === 25 && record.q20 === 0 && record.q24 == null && q32Destination != null) return q32Destination;
|
|
739
|
+
if ((changedMask & 17) === 17 && record.q20 === 0 && record.q24 == null && record.q32 == null && q36Destination != null) return q36Destination;
|
|
740
|
+
if (changedMask === 1 && q20Destination != null) return q20Destination;
|
|
741
|
+
if ((changedMask & 1) && q20Destination != null && record.q20 !== 0) return q20Destination;
|
|
742
|
+
if (record.q20 === 0 && q24Destination != null) return q24Destination;
|
|
743
|
+
return q20Destination ?? NAVIGATION_DEFAULT_DESTINATION;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function summarizeNavigationUnit(entity, typeId, record, trackedState = null) {
|
|
747
|
+
if (typeId !== NAVIGATION_UNIT_TYPE_ID || !record) return null;
|
|
748
|
+
const destination = trackedState?.destination ?? navigationDestinationFromRecord(record);
|
|
749
|
+
return {
|
|
750
|
+
entity,
|
|
751
|
+
destination,
|
|
752
|
+
destinationName: navigationDestinationName(destination),
|
|
753
|
+
autoWarpOnShieldFailure: trackedState?.shieldFailure ?? null,
|
|
754
|
+
autoWarpOnNoCaptains: trackedState?.noCaptains ?? null,
|
|
755
|
+
state: cloneRecord(record)
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function summarizeCommsStation(entity, typeId, record, occupied = false) {
|
|
760
|
+
if (typeId !== COMMS_STATION_TYPE_ID) return null;
|
|
761
|
+
const charges = record?.q20 ?? null;
|
|
762
|
+
return {
|
|
763
|
+
entity,
|
|
764
|
+
typeId,
|
|
765
|
+
typeName: entityNameFromType(typeId),
|
|
766
|
+
charges,
|
|
767
|
+
maxCharges: COMMS_STATION_MAX_CHARGES,
|
|
768
|
+
chargeRatio: typeof charges === "number" ? charges / COMMS_STATION_MAX_CHARGES : null,
|
|
769
|
+
occupied: Boolean(occupied),
|
|
770
|
+
state: record ? cloneRecord(record) : {}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function summarizeHealth(entity, record) {
|
|
775
|
+
if (!record) return null;
|
|
776
|
+
const maxHp = record.q20 ?? null;
|
|
777
|
+
const hp = record.q24 ?? null;
|
|
778
|
+
return {
|
|
779
|
+
entity,
|
|
780
|
+
hp,
|
|
781
|
+
maxHp,
|
|
782
|
+
ratio: typeof hp === "number" && typeof maxHp === "number" && maxHp !== 0 ? hp / maxHp : null,
|
|
783
|
+
state: cloneRecord(record)
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function summarizeBot(entity, {
|
|
788
|
+
health,
|
|
789
|
+
table2Record,
|
|
790
|
+
smallRecord,
|
|
791
|
+
combatRecord,
|
|
792
|
+
motionRecord,
|
|
793
|
+
table10Record,
|
|
794
|
+
table51Record,
|
|
795
|
+
itemCrate,
|
|
796
|
+
player,
|
|
797
|
+
shipControl
|
|
798
|
+
}) {
|
|
799
|
+
if (!health || itemCrate || player || shipControl || isBoulderBotExclusion(health, table2Record, combatRecord)) return null;
|
|
800
|
+
const className = botClassFromState({
|
|
801
|
+
health,
|
|
802
|
+
table2Record,
|
|
803
|
+
smallRecord,
|
|
804
|
+
combatRecord,
|
|
805
|
+
table10Record,
|
|
806
|
+
table51Record
|
|
807
|
+
});
|
|
808
|
+
return {
|
|
809
|
+
entity,
|
|
810
|
+
className,
|
|
811
|
+
identifier: botIdentifierFromState({combatRecord, motionRecord, smallRecord}),
|
|
812
|
+
typeA: health.state?.q28 ?? null,
|
|
813
|
+
typeB: health.state?.q32 ?? null
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function isBoulderBotExclusion(health, table2Record, combatRecord) {
|
|
818
|
+
return Boolean(
|
|
819
|
+
health?.maxHp === 2000 &&
|
|
820
|
+
health?.state?.q28 === 30 &&
|
|
821
|
+
health?.state?.q32 != null &&
|
|
822
|
+
table2Record?.q20 != null &&
|
|
823
|
+
table2Record?.q24 == null &&
|
|
824
|
+
!combatRecord
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function botClassFromState({health, table2Record, smallRecord, combatRecord, table10Record, table51Record}) {
|
|
829
|
+
if (isYellowMineGuardBotState({health, smallRecord, combatRecord, table2Record})) return "yellow-mine-guard";
|
|
830
|
+
if (isShieldHelperBotState({health, table2Record})) return "shield-helper";
|
|
831
|
+
if (isShieldMasterBotState({health, table2Record})) return "shield-master";
|
|
832
|
+
if (isLazerEnthusiastBotState({health, table2Record, table10Record, table51Record})) return "lazer-enthusiast";
|
|
833
|
+
if (isCowardBossState({health})) return "the-coward";
|
|
834
|
+
if (isOrangeFoolBotState({combatRecord})) return "orange-fool";
|
|
835
|
+
if (isZombieBossBotState({health, table2Record, combatRecord})) return "zombie-boss";
|
|
836
|
+
if (combatRecord?.q20 === 762523904) return "zombie";
|
|
837
|
+
if (combatRecord?.q20 === 1967883008) return "zombie-hunter";
|
|
838
|
+
if (combatRecord?.q20 === 945371904) return "zombie-tank";
|
|
839
|
+
if (isRedSentryBotState({health, table2Record, combatRecord})) return "red-sentry";
|
|
840
|
+
if (isRedSniperBotState({health, table2Record})) return "red-sniper";
|
|
841
|
+
if (isBlueRusherBotState({health, table2Record, combatRecord})) return "blue-rusher";
|
|
842
|
+
if (isYellowHunterBotState({health, table2Record, combatRecord})) return "yellow-hunter";
|
|
843
|
+
if (isAquaShielderBotState({health, table2Record, combatRecord})) return "aqua-shielder";
|
|
844
|
+
if (table2Record) return "table2-bot";
|
|
845
|
+
if (combatRecord) return "combat-bot";
|
|
846
|
+
return "bot";
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function isCowardBossState({health}) {
|
|
850
|
+
return health?.state?.q28 === 70 && health?.state?.q32 === 78;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function isOrangeFoolBotState({combatRecord}) {
|
|
854
|
+
return combatRecord?.q20 === -427551232 && combatRecord?.q24 === 1 && combatRecord?.q72 === 3;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function isZombieBossBotState({health, table2Record, combatRecord}) {
|
|
858
|
+
return (
|
|
859
|
+
combatRecord?.q20 === -430233088 &&
|
|
860
|
+
health?.state?.q28 === 30 &&
|
|
861
|
+
health?.state?.q32 === 166 &&
|
|
862
|
+
table2Record?.q20 === 320 &&
|
|
863
|
+
table2Record?.q24 === -900
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export function isRedSentryBotState({health, table2Record, combatRecord}) {
|
|
868
|
+
return (
|
|
869
|
+
combatRecord?.q20 === 508251904 && combatRecord?.q72 === 2 ||
|
|
870
|
+
health?.state?.q28 === 70 && health?.state?.q32 === 166 && table2Record?.q20 === 320 && table2Record?.q24 === -900
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function isBlueRusherBotState({health, table2Record, combatRecord}) {
|
|
875
|
+
return (
|
|
876
|
+
combatRecord?.q20 === 1113052928 && combatRecord?.q72 === 2 ||
|
|
877
|
+
health?.state?.q28 === 30 && health?.state?.q32 === 34 && table2Record?.q20 === 80 && table2Record?.q24 === -900
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function isRedSniperBotState({health, table2Record}) {
|
|
882
|
+
return health?.state?.q28 === 35 && health?.state?.q32 === 89 && table2Record?.q20 === 180 && table2Record?.q24 === -900;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function isYellowHunterBotState({health, table2Record, combatRecord}) {
|
|
886
|
+
return (
|
|
887
|
+
combatRecord?.q20 === -775665152 && combatRecord?.q72 === 4 && combatRecord?.q44 === -944515328 ||
|
|
888
|
+
health?.state?.q28 === 30 && health?.state?.q32 === 56 && table2Record?.q20 === 120 && table2Record?.q24 === -900
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export function isAquaShielderBotState({health, table2Record, combatRecord}) {
|
|
893
|
+
return (
|
|
894
|
+
combatRecord?.q20 === 13697024 && combatRecord?.q24 === 1 ||
|
|
895
|
+
health?.state?.q28 === 50 && health?.state?.q32 === 122 && table2Record?.q20 === 240 && table2Record?.q24 === -900
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function isLazerEnthusiastBotState({health, table2Record, table10Record, table51Record}) {
|
|
900
|
+
return (
|
|
901
|
+
health?.state?.q28 === 140 &&
|
|
902
|
+
health?.state?.q32 === 155 &&
|
|
903
|
+
table2Record?.q20 === 300 &&
|
|
904
|
+
table2Record?.q24 === -900 &&
|
|
905
|
+
Boolean(table10Record) &&
|
|
906
|
+
Boolean(table51Record)
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function isYellowMineGuardBotState({health, smallRecord, combatRecord, table2Record}) {
|
|
911
|
+
return (
|
|
912
|
+
health?.state?.q28 === 30 &&
|
|
913
|
+
health?.state?.q32 === 33 &&
|
|
914
|
+
smallRecord?.q20 === 78 &&
|
|
915
|
+
smallRecord?.q24 === 78 &&
|
|
916
|
+
combatRecord?.q24 === -995542016 &&
|
|
917
|
+
!table2Record
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function isShieldHelperBotState({health, table2Record}) {
|
|
922
|
+
return health?.state?.q28 === 30 && health?.state?.q32 === 33 && health?.maxHp === 1000 && !table2Record;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export function isShieldMasterBotState({health, table2Record}) {
|
|
926
|
+
return health?.state?.q28 === 190 && health?.state?.q32 === 210 && health?.maxHp === 15000 && Boolean(table2Record);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function botIdentifierFromState({combatRecord, motionRecord, smallRecord}) {
|
|
930
|
+
if (combatRecord) return `t18:${combatRecord.q20 ?? "-"}:${combatRecord.q24 ?? "-"}:${combatRecord.q72 ?? "-"}`;
|
|
931
|
+
if (motionRecord) return `t19:${motionRecord.q20 ?? "-"}:${motionRecord.q24 ?? "-"}`;
|
|
932
|
+
if (smallRecord) return `t3:${smallRecord.q20 ?? "-"}:${smallRecord.q24 ?? "-"}`;
|
|
933
|
+
return "-";
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function summarizeShieldGenerator(entity, shieldRecord = null, itemHolderRecord = null, boostRecord = null) {
|
|
937
|
+
const charge = shieldRecord?.q20 ?? 0;
|
|
938
|
+
const maxCharge = 5000;
|
|
939
|
+
const efficiencyPercent = shieldRecord?.q24 ?? null;
|
|
940
|
+
const storedItem = itemSummary(itemHolderRecord?.q20 ?? null, itemHolderRecord?.q24 ?? null);
|
|
941
|
+
const boostState = boostRecord?.q24 ?? 0;
|
|
942
|
+
const boostTimer = boostRecord?.q28 ?? 0;
|
|
943
|
+
const puzzleSeed = boostRecord?.q20 ?? null;
|
|
944
|
+
const summary = {
|
|
945
|
+
entity,
|
|
946
|
+
charge,
|
|
947
|
+
maxCharge,
|
|
948
|
+
chargeRatio: typeof charge === "number" ? charge / maxCharge : null,
|
|
949
|
+
efficiencyPercent,
|
|
950
|
+
efficiency: typeof efficiencyPercent === "number" ? efficiencyPercent / 100 : null,
|
|
951
|
+
storedItemId: storedItem.itemId,
|
|
952
|
+
storedItemName: storedItem.itemName,
|
|
953
|
+
storedItemCount: storedItem.count,
|
|
954
|
+
hasShieldCore: storedItem.itemId === 123,
|
|
955
|
+
boostState,
|
|
956
|
+
boostStateName: enumValueName(SHIELD_GENERATOR_BOOST_STATE_NAMES, boostState),
|
|
957
|
+
boostTimer,
|
|
958
|
+
boostActive: boostState !== 0 || boostTimer > 0,
|
|
959
|
+
puzzleSeed,
|
|
960
|
+
// Replaced in place below by a lazy accessor; declared here so the key keeps
|
|
961
|
+
// its original position in the object.
|
|
962
|
+
puzzleSolution: null,
|
|
963
|
+
state: cloneRecord(shieldRecord || {}),
|
|
964
|
+
itemState: cloneRecord(itemHolderRecord || {}),
|
|
965
|
+
boostStateRaw: cloneRecord(boostRecord || {})
|
|
966
|
+
};
|
|
967
|
+
// Solving the maze costs ~190us and was by far the most expensive step of a
|
|
968
|
+
// model rebuild, run for every shield generator whether or not the caller
|
|
969
|
+
// ever looks at the solution. Defer it to first read.
|
|
970
|
+
return defineLazyProperty(summary, "puzzleSolution", () => maybeSolveGeneratorMazeSeed(puzzleSeed));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export function summarizeShipControl(entity, record) {
|
|
974
|
+
if (!record) return null;
|
|
975
|
+
const color = record.q32 == null ? null : Number(record.q32);
|
|
976
|
+
const shieldMaxHpOffset = typeof record.q68 === "number" ? record.q68 : null;
|
|
977
|
+
const shieldMaxHp = shieldMaxHpOffset == null ? null : shieldMaxHpOffset + 2000;
|
|
978
|
+
const shieldBaseHp = typeof record.q72 === "number" ? record.q72 : null;
|
|
979
|
+
const activeTankHp = typeof record.q76 === "number" ? record.q76 : null;
|
|
980
|
+
const inactiveTankHp = typeof record.q80 === "number" ? record.q80 : null;
|
|
981
|
+
const tankValues = [activeTankHp, inactiveTankHp].filter((value) => typeof value === "number");
|
|
982
|
+
const warpTicks = typeof record.q84 === "number" ? record.q84 : null;
|
|
983
|
+
const warpElapsedSeconds = warpTicks == null ? null : warpTicks / OVERWORLD_WARP_TICKS_PER_SECOND;
|
|
984
|
+
const warpDurationSeconds = warpTicks == null ? null : record.q88 ?? DEFAULT_OVERWORLD_WARP_DURATION_SECONDS;
|
|
985
|
+
const warpRemainingSeconds = warpElapsedSeconds == null ? null : Math.max(0, warpDurationSeconds - warpElapsedSeconds);
|
|
986
|
+
return {
|
|
987
|
+
entity,
|
|
988
|
+
name: decodeText(record.blob116),
|
|
989
|
+
hexCode: decodeText(record.blob128),
|
|
990
|
+
shipWorldId: record.q20 ?? null,
|
|
991
|
+
color: Number.isFinite(color) ? color : null,
|
|
992
|
+
colorCss: Number.isFinite(color) ? colorToCss(color) : null,
|
|
993
|
+
thrustX: numberOrNull(record.q24, 20),
|
|
994
|
+
thrustY: numberOrNull(record.q28, 20),
|
|
995
|
+
value52: numberOrNull(record.q52, 20),
|
|
996
|
+
value84: numberOrNull(record.q84, 20),
|
|
997
|
+
value96: numberOrNull(record.q96, 1000),
|
|
998
|
+
shield: shieldMaxHp == null && shieldBaseHp == null && !tankValues.length ? null : {
|
|
999
|
+
maxHp: shieldMaxHp,
|
|
1000
|
+
baseHp: shieldBaseHp,
|
|
1001
|
+
activeTankHp,
|
|
1002
|
+
inactiveTankHp,
|
|
1003
|
+
tankValues
|
|
1004
|
+
},
|
|
1005
|
+
warp: warpTicks == null ? null : {
|
|
1006
|
+
active: record.q28 === 3,
|
|
1007
|
+
ticks: warpTicks,
|
|
1008
|
+
elapsedSeconds: warpElapsedSeconds,
|
|
1009
|
+
durationSeconds: warpDurationSeconds,
|
|
1010
|
+
remainingSeconds: warpRemainingSeconds
|
|
1011
|
+
},
|
|
1012
|
+
state: cloneRecord(record)
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function summarizeShipSize(entity, record) {
|
|
1017
|
+
if (!record) return null;
|
|
1018
|
+
return {
|
|
1019
|
+
entity,
|
|
1020
|
+
width: record.q20 == null ? null : Math.round(record.q20 / 10),
|
|
1021
|
+
height: record.q24 == null ? null : Math.round(record.q24 / 10),
|
|
1022
|
+
rawWidth: record.q20 ?? null,
|
|
1023
|
+
rawHeight: record.q24 ?? null,
|
|
1024
|
+
state: cloneRecord(record)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export function summarizeShipWorldMetadata(lockdownRecord, shipRecord) {
|
|
1029
|
+
if (!lockdownRecord && !shipRecord) return null;
|
|
1030
|
+
const color = shipRecord?.q24 == null ? null : Number(shipRecord.q24);
|
|
1031
|
+
const lockdownCountdownSeconds = typeof lockdownRecord?.q20 === "number" ? lockdownRecord.q20 : null;
|
|
1032
|
+
const onlineShipOwnerCount = typeof lockdownRecord?.q24 === "number" ? lockdownRecord.q24 : null;
|
|
1033
|
+
const requiredShipOwnerCount = typeof lockdownRecord?.q28 === "number" ? lockdownRecord.q28 : null;
|
|
1034
|
+
return {
|
|
1035
|
+
name: decodeText(shipRecord?.blob20),
|
|
1036
|
+
color: Number.isFinite(color) ? color : null,
|
|
1037
|
+
colorCss: Number.isFinite(color) ? colorToCss(color) : null,
|
|
1038
|
+
width: typeof shipRecord?.q32 === "number" ? shipRecord.q32 : null,
|
|
1039
|
+
height: typeof shipRecord?.q36 === "number" ? shipRecord.q36 : null,
|
|
1040
|
+
lockdownTimerSeconds: lockdownCountdownSeconds,
|
|
1041
|
+
lockdownCountdownSeconds,
|
|
1042
|
+
onlineShipOwnerCount,
|
|
1043
|
+
requiredShipOwnerCount,
|
|
1044
|
+
allShipOwnersOnline: onlineShipOwnerCount == null || requiredShipOwnerCount == null ? null : onlineShipOwnerCount >= requiredShipOwnerCount,
|
|
1045
|
+
lockdownEngaged: lockdownCountdownSeconds == null ? null : lockdownCountdownSeconds > 0,
|
|
1046
|
+
lockdownState: cloneRecord(lockdownRecord || {}),
|
|
1047
|
+
shipState: cloneRecord(shipRecord || {})
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function summarizeItemCrate(entity, sizeRecord, itemRecord = null, healthRecord = null) {
|
|
1052
|
+
if (!sizeRecord) return null;
|
|
1053
|
+
const itemId = itemRecord?.q20 ?? null;
|
|
1054
|
+
const count = itemRecord?.q24 ?? null;
|
|
1055
|
+
return {
|
|
1056
|
+
entity,
|
|
1057
|
+
...itemSummary(itemId, count),
|
|
1058
|
+
width: sizeRecord.q20 ?? null,
|
|
1059
|
+
height: sizeRecord.q24 ?? null,
|
|
1060
|
+
itemState: cloneRecord(itemRecord || healthRecord || {}),
|
|
1061
|
+
sizeState: cloneRecord(sizeRecord)
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export function scaledSizeSummary(record) {
|
|
1066
|
+
if (!record) return null;
|
|
1067
|
+
const rawWidth = record.q20 ?? null;
|
|
1068
|
+
const rawHeight = record.q24 ?? null;
|
|
1069
|
+
return {
|
|
1070
|
+
width: rawWidth == null ? null : rawWidth / 10,
|
|
1071
|
+
height: rawHeight == null ? null : rawHeight / 10,
|
|
1072
|
+
rawWidth,
|
|
1073
|
+
rawHeight,
|
|
1074
|
+
state: cloneRecord(record)
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export function summarizeExpandoBox(entity, itemHolderRecord, sizeRecord) {
|
|
1079
|
+
if (!itemHolderRecord && !sizeRecord) return null;
|
|
1080
|
+
const size = scaledSizeSummary(sizeRecord);
|
|
1081
|
+
return {
|
|
1082
|
+
entity,
|
|
1083
|
+
...itemSummary(itemHolderRecord?.q20 ?? null, itemHolderRecord?.q24 ?? null),
|
|
1084
|
+
width: size?.width ?? null,
|
|
1085
|
+
height: size?.height ?? null,
|
|
1086
|
+
rawWidth: size?.rawWidth ?? null,
|
|
1087
|
+
rawHeight: size?.rawHeight ?? null,
|
|
1088
|
+
itemState: cloneRecord(itemHolderRecord || {}),
|
|
1089
|
+
sizeState: cloneRecord(sizeRecord || {})
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function summarizeHoverOutline(entity, record) {
|
|
1094
|
+
const size = scaledSizeSummary(record);
|
|
1095
|
+
if (!size) return null;
|
|
1096
|
+
return {entity, ...size};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export function summarizeMapMarker(entity, labelRecord, zoneRecord = null, sizeRecord = null) {
|
|
1100
|
+
if (!labelRecord && !zoneRecord) return null;
|
|
1101
|
+
const title = decodeText(zoneRecord?.blob28) ?? decodeText(labelRecord?.blob20);
|
|
1102
|
+
const key = decodeText(labelRecord?.blob24) ?? decodeText(labelRecord?.blob28);
|
|
1103
|
+
const description = decodeText(zoneRecord?.blob40);
|
|
1104
|
+
const color = zoneRecord?.q20 ?? labelRecord?.q28 ?? null;
|
|
1105
|
+
const accentColor = zoneRecord?.q24 ?? labelRecord?.q32 ?? null;
|
|
1106
|
+
const kind = key === "mine" ? "mining_zone" : key === "dock" ? "dock" : zoneRecord ? "portal" : "marker";
|
|
1107
|
+
return {
|
|
1108
|
+
entity,
|
|
1109
|
+
kind,
|
|
1110
|
+
title,
|
|
1111
|
+
key,
|
|
1112
|
+
description,
|
|
1113
|
+
color,
|
|
1114
|
+
colorCss: color != null && Number.isFinite(Number(color)) ? colorToCss(Number(color)) : null,
|
|
1115
|
+
accentColor,
|
|
1116
|
+
accentColorCss: accentColor != null && Number.isFinite(Number(accentColor)) ? colorToCss(Number(accentColor)) : null,
|
|
1117
|
+
width: sizeRecord?.q20 ?? null,
|
|
1118
|
+
height: sizeRecord?.q24 ?? null,
|
|
1119
|
+
labelState: cloneRecord(labelRecord || {}),
|
|
1120
|
+
zoneState: cloneRecord(zoneRecord || {})
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export function summarizeDockingSpring(entity, springRecord, bodyRecord = null, sizeRecord = null) {
|
|
1125
|
+
if (!springRecord || bodyRecord?.q32 !== -1 || sizeRecord?.q20 !== 160 || sizeRecord?.q24 !== 160) return null;
|
|
1126
|
+
return {
|
|
1127
|
+
entity,
|
|
1128
|
+
id: springRecord.q20 ?? null,
|
|
1129
|
+
width: sizeRecord.q20,
|
|
1130
|
+
height: sizeRecord.q24,
|
|
1131
|
+
state: cloneRecord(springRecord)
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function summarizeHugeThruster(entity, markerRecord, sizeRecord = null) {
|
|
1136
|
+
if (!markerRecord || sizeRecord?.q20 !== 320 || sizeRecord?.q24 !== 160) return null;
|
|
1137
|
+
return {
|
|
1138
|
+
entity,
|
|
1139
|
+
width: sizeRecord.q20,
|
|
1140
|
+
height: sizeRecord.q24,
|
|
1141
|
+
state: cloneRecord(markerRecord)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null, filterSlotsRecord = null, tracker = null, includeDefaults = false, itemHolderRecord = null) {
|
|
1146
|
+
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;
|
|
1154
|
+
const heldItem = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24 ?? null);
|
|
1155
|
+
return {
|
|
1156
|
+
entity,
|
|
1157
|
+
pick,
|
|
1158
|
+
pickName: enumValueName(LOADER_POSITION_NAMES, pick),
|
|
1159
|
+
place,
|
|
1160
|
+
placeName: enumValueName(LOADER_POSITION_NAMES, place),
|
|
1161
|
+
priority,
|
|
1162
|
+
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),
|
|
1167
|
+
filterMode,
|
|
1168
|
+
filterModeName: enumValueName(LOADER_FILTER_MODE_NAMES, filterMode),
|
|
1169
|
+
filterSlots: config.filterSlots ?? null,
|
|
1170
|
+
heldItemId: heldItem.itemId,
|
|
1171
|
+
heldItemName: heldItem.itemName,
|
|
1172
|
+
heldCount: heldItem.count,
|
|
1173
|
+
active: heldItem.itemId != null,
|
|
1174
|
+
progress: null,
|
|
1175
|
+
state: cloneRecord(loaderRecord || {}),
|
|
1176
|
+
filterState: cloneRecord(loaderFilterRecord || {}),
|
|
1177
|
+
filterSlotsState: cloneRecord(filterSlotsRecord || {})
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export function summarizeCargoHatch(entity, typeId, filterRecord = null, filterSlotsRecord = null, animationRecord = null) {
|
|
1182
|
+
if (!CARGO_HATCH_TYPE_IDS.has(typeId)) return null;
|
|
1183
|
+
const filterMode = filterRecord?.q20 ?? 0;
|
|
1184
|
+
return {
|
|
1185
|
+
entity,
|
|
1186
|
+
typeId,
|
|
1187
|
+
typeName: entityNameFromType(typeId),
|
|
1188
|
+
filterMode,
|
|
1189
|
+
filterModeName: enumValueName(LOADER_FILTER_MODE_NAMES, filterMode),
|
|
1190
|
+
filterSlots: filterSlotsRecord ? [filterSlotsRecord.q20 ?? null, filterSlotsRecord.q24 ?? null, filterSlotsRecord.q28 ?? null] : null,
|
|
1191
|
+
openFraction: animationRecord?.q20 == null ? null : boundedProgress(animationRecord.q20) / 100,
|
|
1192
|
+
filterState: cloneRecord(filterRecord || {}),
|
|
1193
|
+
filterSlotsState: cloneRecord(filterSlotsRecord || {})
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export function summarizePusher(entity, pusherRecord, filterSlotsRecord = null) {
|
|
1198
|
+
if (!pusherRecord) return null;
|
|
1199
|
+
const mode = pusherRecord.q20 == null ? 2 : pusherRecord.q20 + 2;
|
|
1200
|
+
const filteredMode = pusherRecord.q24 ?? 0;
|
|
1201
|
+
return {
|
|
1202
|
+
entity,
|
|
1203
|
+
mode,
|
|
1204
|
+
modeName: enumValueName(PUSHER_MODE_NAMES, mode),
|
|
1205
|
+
filteredMode,
|
|
1206
|
+
filteredModeName: enumValueName(PUSHER_MODE_NAMES, filteredMode),
|
|
1207
|
+
angle: numberOrNull(pusherRecord.q28, 10),
|
|
1208
|
+
speed: pusherRecord.q32 == null ? 20 : (pusherRecord.q32 / 100) + 20,
|
|
1209
|
+
length: pusherRecord.q40 == null ? 1000 : ((pusherRecord.q40 + 50) / 10) + 995,
|
|
1210
|
+
filterInventory: pusherRecord.q36 === 1,
|
|
1211
|
+
filterSlots: filterSlotsRecord ? [filterSlotsRecord.q20 ?? null, filterSlotsRecord.q24 ?? null, filterSlotsRecord.q28 ?? null] : null,
|
|
1212
|
+
state: cloneRecord(pusherRecord),
|
|
1213
|
+
filterSlotsState: cloneRecord(filterSlotsRecord || {})
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function summarizePusherBeam(entity, beamRecord) {
|
|
1218
|
+
if (!beamRecord) return null;
|
|
1219
|
+
const lengthRaw = beamRecord.q20 ?? 0;
|
|
1220
|
+
const mode = beamRecord.q24 ?? 2;
|
|
1221
|
+
return {
|
|
1222
|
+
entity,
|
|
1223
|
+
active: mode !== 2,
|
|
1224
|
+
mode,
|
|
1225
|
+
modeName: enumValueName(PUSHER_MODE_NAMES, mode),
|
|
1226
|
+
lengthRaw,
|
|
1227
|
+
length: lengthRaw / 10,
|
|
1228
|
+
state: cloneRecord(beamRecord)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function normalizeDegrees(value) {
|
|
1233
|
+
if (!Number.isFinite(Number(value))) return null;
|
|
1234
|
+
return ((Number(value) % 360) + 360) % 360;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export function summarizeItemLauncher(entity, typeId, launcherRecord) {
|
|
1238
|
+
if (typeId !== ITEM_LAUNCHER_TYPE_ID || !launcherRecord) return null;
|
|
1239
|
+
const angleRadians = numberOrNull(launcherRecord.q32 ?? 0, 200);
|
|
1240
|
+
return {
|
|
1241
|
+
entity,
|
|
1242
|
+
angleRaw: launcherRecord.q32 ?? null,
|
|
1243
|
+
angleRadians,
|
|
1244
|
+
angleDegrees: angleRadians == null ? null : normalizeDegrees((angleRadians * 180) / Math.PI),
|
|
1245
|
+
state: cloneRecord(launcherRecord)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
export function summarizeSign(entity, signRecord) {
|
|
1250
|
+
if (!signRecord) return null;
|
|
1251
|
+
const displayMode = signRecord.q20 ?? 0;
|
|
1252
|
+
return {
|
|
1253
|
+
entity,
|
|
1254
|
+
text: decodeText(signRecord.blob24) ?? "",
|
|
1255
|
+
displayMode,
|
|
1256
|
+
displayModeName: enumValueName(SIGN_DISPLAY_MODE_NAMES, displayMode),
|
|
1257
|
+
state: cloneRecord(signRecord)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export function summarizeShieldProjector(entity, projectorRecord) {
|
|
1262
|
+
return {
|
|
1263
|
+
entity,
|
|
1264
|
+
active: projectorRecord?.q21 !== 0,
|
|
1265
|
+
state: cloneRecord(projectorRecord || {})
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export function summarizeSpawnPoint(entity, spawnRecord) {
|
|
1270
|
+
const rank = Number.isFinite(Number(spawnRecord?.q20)) ? Number(spawnRecord.q20) : 0;
|
|
1271
|
+
return {
|
|
1272
|
+
entity,
|
|
1273
|
+
rank,
|
|
1274
|
+
rankName: enumValueName(TEAM_RANK_NAMES, rank),
|
|
1275
|
+
state: cloneRecord(spawnRecord || {})
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export function summarizeDoor(entity, rankRecord, doorRecord) {
|
|
1280
|
+
const rank = Number.isFinite(Number(rankRecord?.q20)) ? Number(rankRecord.q20) : 0;
|
|
1281
|
+
return {
|
|
1282
|
+
entity,
|
|
1283
|
+
rank,
|
|
1284
|
+
rankName: enumValueName(TEAM_RANK_NAMES, rank),
|
|
1285
|
+
open: doorRecord?.q21 === 1,
|
|
1286
|
+
rankState: cloneRecord(rankRecord || {}),
|
|
1287
|
+
state: cloneRecord(doorRecord || {})
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
export function enumValueName(map, value) {
|
|
1292
|
+
return value == null ? null : map.get(value) ?? null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
export function colorToCss(color) {
|
|
1296
|
+
return `rgb(${(color >> 16) & 0xff},${(color >> 8) & 0xff},${color & 0xff})`;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export function previewActionName(color) {
|
|
1300
|
+
if (color === 0x00ff00) return "place";
|
|
1301
|
+
if (color === 0xff0000) return "break";
|
|
1302
|
+
if (color === 0x0000ff) return "blueprint";
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export function bitOffsets(value) {
|
|
1307
|
+
if (!Number.isSafeInteger(value) || value < 1) return [];
|
|
1308
|
+
let bits = BigInt(value);
|
|
1309
|
+
const offsets = [];
|
|
1310
|
+
let offset = 0;
|
|
1311
|
+
while (bits > 0n && offset < 64) {
|
|
1312
|
+
if (bits & 1n) offsets.push(offset);
|
|
1313
|
+
bits >>= 1n;
|
|
1314
|
+
offset++;
|
|
1315
|
+
}
|
|
1316
|
+
return offsets;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
export function summarizeBlueprintPreview(entity, record, transformRecord = null) {
|
|
1320
|
+
if (!record || record.q20 == null) return null;
|
|
1321
|
+
const rawBits = Number.isSafeInteger(Number(record.q24)) ? Number(record.q24) : null;
|
|
1322
|
+
const bits = rawBits == null ? 1 : rawBits + 1;
|
|
1323
|
+
const placementOffsets = bitOffsets(bits);
|
|
1324
|
+
const transform = transformRecord ? {
|
|
1325
|
+
entity,
|
|
1326
|
+
x: numberOrNull(transformRecord.q20, 40),
|
|
1327
|
+
y: numberOrNull(transformRecord.q24, 40),
|
|
1328
|
+
rot: numberOrNull(transformRecord.q28, 127.324)
|
|
1329
|
+
} : null;
|
|
1330
|
+
return {
|
|
1331
|
+
entity,
|
|
1332
|
+
itemId: record.q20,
|
|
1333
|
+
itemName: entityNameFromType(record.q20),
|
|
1334
|
+
bits,
|
|
1335
|
+
rawBits,
|
|
1336
|
+
placementOffsets,
|
|
1337
|
+
placementCount: placementOffsets.length,
|
|
1338
|
+
placements: placementOffsets.map((offset) => ({
|
|
1339
|
+
offset,
|
|
1340
|
+
x: transform?.x == null ? null : transform.x + offset,
|
|
1341
|
+
y: transform?.y ?? null,
|
|
1342
|
+
itemId: record.q20,
|
|
1343
|
+
itemName: entityNameFromType(record.q20)
|
|
1344
|
+
})),
|
|
1345
|
+
x: transform?.x ?? null,
|
|
1346
|
+
y: transform?.y ?? null,
|
|
1347
|
+
rot: transform?.rot ?? null,
|
|
1348
|
+
state: cloneRecord(record)
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
export function summarizePlayerPreview(entity, record, blueprintItems = []) {
|
|
1353
|
+
if (!record) return null;
|
|
1354
|
+
const color = record.q40 == null ? null : Number(record.q40);
|
|
1355
|
+
const active = typeof record.q36 === "number" && record.q36 !== 0;
|
|
1356
|
+
if (!active && record.q36 == null) return null;
|
|
1357
|
+
const actionName = Number.isFinite(color) ? previewActionName(color) : null;
|
|
1358
|
+
return {
|
|
1359
|
+
entity,
|
|
1360
|
+
active,
|
|
1361
|
+
x: numberOrNull(record.q20, 10),
|
|
1362
|
+
y: numberOrNull(record.q24, 10),
|
|
1363
|
+
width: numberOrNull(record.q28, 10),
|
|
1364
|
+
height: numberOrNull(record.q32, 10),
|
|
1365
|
+
progress: record.q36 ?? null,
|
|
1366
|
+
color: Number.isFinite(color) ? color : null,
|
|
1367
|
+
colorCss: Number.isFinite(color) ? colorToCss(color) : null,
|
|
1368
|
+
actionName,
|
|
1369
|
+
blueprintId: record.q44 ?? null,
|
|
1370
|
+
blueprintItems: actionName === "blueprint" ? blueprintItems : [],
|
|
1371
|
+
state: cloneRecord(record)
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export function summarizePlayer(entity, record, previewRecord = null, blueprintItems = []) {
|
|
1376
|
+
if (!record) return null;
|
|
1377
|
+
const rawHeldItemId = record.q28 == null ? null : Number(record.q28);
|
|
1378
|
+
const heldItemId = Number.isFinite(rawHeldItemId) && rawHeldItemId !== 0 ? rawHeldItemId : null;
|
|
1379
|
+
const teamRank = Number.isFinite(Number(record.q72)) ? Number(record.q72) : 0;
|
|
1380
|
+
const gameRank = Number.isFinite(Number(record.q76)) ? Number(record.q76) : 0;
|
|
1381
|
+
return {
|
|
1382
|
+
entity,
|
|
1383
|
+
name: decodeText(record.blob92),
|
|
1384
|
+
heldItemId,
|
|
1385
|
+
heldItemName: entityNameFromType(heldItemId),
|
|
1386
|
+
repairTargetDistance: record.q56 == null ? null : record.q56 / 10,
|
|
1387
|
+
repairTargetAngle: record.q80 == null ? null : record.q80 / 100,
|
|
1388
|
+
shipRank: PLAYER_SHIP_RANKS.get(teamRank) || null,
|
|
1389
|
+
patronTier: patronTierName(gameRank),
|
|
1390
|
+
isDeveloper: gameRank === 1,
|
|
1391
|
+
isPatron: gameRank >= 2 && gameRank <= 6,
|
|
1392
|
+
piloting: Boolean(record.q107),
|
|
1393
|
+
muted: Boolean(record.q112),
|
|
1394
|
+
actionPreview: summarizePlayerPreview(entity, previewRecord, blueprintItems),
|
|
1395
|
+
state: cloneRecord(record)
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
export function patronTierName(gameRank) {
|
|
1400
|
+
switch (gameRank) {
|
|
1401
|
+
case 2:
|
|
1402
|
+
return "bronze";
|
|
1403
|
+
case 3:
|
|
1404
|
+
return "silver";
|
|
1405
|
+
case 4:
|
|
1406
|
+
return "gold";
|
|
1407
|
+
case 5:
|
|
1408
|
+
return "plat";
|
|
1409
|
+
case 6:
|
|
1410
|
+
return "flux";
|
|
1411
|
+
default:
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Called once per entity with a fixed set of named components. Taking (key,
|
|
1417
|
+
// value) pairs avoids allocating ~32 single-key object literals per entity and
|
|
1418
|
+
// an Object.entries pair array for each of them.
|
|
1419
|
+
export function mergeContents(components) {
|
|
1420
|
+
const out = {};
|
|
1421
|
+
let any = false;
|
|
1422
|
+
for (const key in components) {
|
|
1423
|
+
const value = components[key];
|
|
1424
|
+
if (value == null) continue;
|
|
1425
|
+
out[key] = value;
|
|
1426
|
+
any = true;
|
|
1427
|
+
}
|
|
1428
|
+
return any ? out : null;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// A hover outline is a highlight rectangle, not a physical footprint. Overworld
|
|
1432
|
+
// boundary entities report outlines as large as 720x32, and indexing those cell
|
|
1433
|
+
// by cell produced ~106k block cells for 183 entities -- the dominant cost in
|
|
1434
|
+
// every derived read. Outlines beyond this budget fall through to the ordinary
|
|
1435
|
+
// marker/crate/type heuristics.
|
|
1436
|
+
export const MAX_HOVER_OUTLINE_FOOTPRINT_CELLS = 1024;
|
|
1437
|
+
|
|
1438
|
+
export function entityFootprint(entity) {
|
|
1439
|
+
if (entity?.hoverOutline && Number.isFinite(Number(entity.hoverOutline.width)) && Number.isFinite(Number(entity.hoverOutline.height))) {
|
|
1440
|
+
const width = Math.ceil(Number(entity.hoverOutline.width));
|
|
1441
|
+
const height = Math.ceil(Number(entity.hoverOutline.height));
|
|
1442
|
+
if (width * height <= MAX_HOVER_OUTLINE_FOOTPRINT_CELLS) {
|
|
1443
|
+
return {width, height, source: "hover_outline"};
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (entity?.markerTypeId != null && ENTITY_FOOTPRINTS.has(entity.markerTypeId)) {
|
|
1447
|
+
const footprint = ENTITY_FOOTPRINTS.get(entity.markerTypeId);
|
|
1448
|
+
return {...footprint, source: "marker"};
|
|
1449
|
+
}
|
|
1450
|
+
if (entity?.hugeThruster) {
|
|
1451
|
+
return {
|
|
1452
|
+
width: entity.hugeThruster.width,
|
|
1453
|
+
height: entity.hugeThruster.height,
|
|
1454
|
+
source: "huge_thruster"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
if (entity?.itemCrate && Number.isFinite(Number(entity.itemCrate.width)) && Number.isFinite(Number(entity.itemCrate.height))) {
|
|
1458
|
+
return {
|
|
1459
|
+
width: Number(entity.itemCrate.width),
|
|
1460
|
+
height: Number(entity.itemCrate.height),
|
|
1461
|
+
source: "crate"
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
if (entity?.typeId != null && ENTITY_FOOTPRINTS.has(entity.typeId)) {
|
|
1465
|
+
const footprint = ENTITY_FOOTPRINTS.get(entity.typeId);
|
|
1466
|
+
return {...footprint, source: "type"};
|
|
1467
|
+
}
|
|
1468
|
+
if (entity?.fabricator || entity?.shieldGenerator) return {width: 2, height: 2, source: "heuristic"};
|
|
1469
|
+
return {width: 1, height: 1, source: "default"};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
export function entityLabel(entity) {
|
|
1473
|
+
if (entity?.category === "metadata" && entity?.typeName) return `Metadata ${entity.typeName}`;
|
|
1474
|
+
if (entity?.blueprintPreview) return `Blueprint Preview (${entity.blueprintPreview.itemName ?? entity.blueprintPreview.itemId ?? "item"})`;
|
|
1475
|
+
if (entity?.category === "loose_item" && entity?.itemHolder?.itemName) return `Loose ${entity.itemHolder.itemName}`;
|
|
1476
|
+
if (entity?.category === "untyped_holder" && entity?.itemHolder?.itemName) return `Untyped Holder (${entity.itemHolder.itemName})`;
|
|
1477
|
+
if (entity?.mapMarker) return `Map Marker (${entity.mapMarker.title ?? entity.mapMarker.key ?? entity.mapMarker.kind ?? "marker"})`;
|
|
1478
|
+
if (entity?.dockingSpring) return "Docking Spring";
|
|
1479
|
+
if (entity?.hugeThruster) return "Huge Thruster";
|
|
1480
|
+
if (entity?.shipControl && entity?.isOverworld) return "Overworld Ship";
|
|
1481
|
+
if (entity?.expandoBox) return "Expando Box";
|
|
1482
|
+
if (entity?.markerTypeName) return entity.markerTypeName;
|
|
1483
|
+
if (entity?.typeName) return entity.typeName;
|
|
1484
|
+
if (entity?.fabricator) return "Fabricator";
|
|
1485
|
+
if (entity?.itemCrate) return "Item Crate";
|
|
1486
|
+
if (entity?.shieldGenerator) return "Shield Generator";
|
|
1487
|
+
if (entity?.fluidTank) return "Fluid Tank";
|
|
1488
|
+
if (entity?.cannon) return "Cannon";
|
|
1489
|
+
if (entity?.pusher) return "Pusher";
|
|
1490
|
+
if (entity?.pusherBeam) return "Pusher Beam";
|
|
1491
|
+
if (entity?.launcher) return "Item Launcher";
|
|
1492
|
+
if (entity?.loader) return "Loader";
|
|
1493
|
+
if (entity?.cargoHatch) return entity.cargoHatch.typeName || "Cargo Hatch";
|
|
1494
|
+
if (entity?.cargoEjector) return entity.cargoEjector.typeName || "Cargo Ejector";
|
|
1495
|
+
if (entity?.itemHolder && !entity?.cargoEjector && !entity?.cannon && !entity?.pusher && !entity?.launcher && !entity?.loader && !entity?.fluidTank && !entity?.shieldGenerator && !entity?.shieldProjector) {
|
|
1496
|
+
return entity.itemHolder.itemName || "Item Holder";
|
|
1497
|
+
}
|
|
1498
|
+
if (entity?.player) return "Player";
|
|
1499
|
+
if (entity?.shipControl) return "Ship Control";
|
|
1500
|
+
return entity?.typeId != null ? `Entity ${entity.typeId}` : "Entity";
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
export function entityCategory(entity) {
|
|
1504
|
+
if (entity?.player) return "player";
|
|
1505
|
+
if (entity?.blueprintPreview) return "blueprint_preview";
|
|
1506
|
+
if (entity?.shipControl) return "ship_control";
|
|
1507
|
+
if (entity?.itemCrate) return "item_crate";
|
|
1508
|
+
if (entity?.mapMarker) return "map_marker";
|
|
1509
|
+
if (entity?.dockingSpring) return "docking_spring";
|
|
1510
|
+
if (entity?.hugeThruster) return "huge_thruster";
|
|
1511
|
+
const hasMachineComponent = Boolean(entity?.fabricator || entity?.cargoEjector || entity?.cannon || entity?.pusher || entity?.launcher || entity?.loader || entity?.cargoHatch || entity?.commsStation || entity?.fluidTank || entity?.shieldGenerator || entity?.shieldProjector);
|
|
1512
|
+
const hasValidTransform = Boolean(
|
|
1513
|
+
entity?.transform &&
|
|
1514
|
+
Number.isFinite(entity.transform.x) &&
|
|
1515
|
+
Number.isFinite(entity.transform.y)
|
|
1516
|
+
);
|
|
1517
|
+
const hasPhysicalComponent = Boolean(hasValidTransform || entity?.itemHolder || hasMachineComponent);
|
|
1518
|
+
if (!hasPhysicalComponent) return "metadata";
|
|
1519
|
+
if (entity?.transform && !hasValidTransform && !entity?.itemHolder && !hasMachineComponent) return "metadata";
|
|
1520
|
+
if (entity?.typeId != null && !hasPhysicalComponent) return "metadata";
|
|
1521
|
+
if (entity?.markerTypeId != null) return "placed_entity";
|
|
1522
|
+
if (entity?.looseItemMarker && entity?.itemHolder?.itemId != null) return "loose_item";
|
|
1523
|
+
if (entity?.itemHolder?.itemId != null && !entity?.cargoEjector && !entity?.cannon && !entity?.pusher && !entity?.launcher && !entity?.loader && !entity?.cargoHatch && !entity?.commsStation && !entity?.fluidTank && !entity?.shieldGenerator && !entity?.shieldProjector) {
|
|
1524
|
+
if (entity.typeId != null && (!PLACED_ENTITY_TYPE_IDS.has(Number(entity.typeId)) || entity.typeId === entity.itemHolder.itemId)) return "loose_item";
|
|
1525
|
+
if (entity.typeId == null) return "untyped_holder";
|
|
1526
|
+
}
|
|
1527
|
+
if (entity?.fabricator || entity?.cannon || entity?.pusher || entity?.launcher || entity?.loader || entity?.cargoHatch || entity?.cargoEjector || entity?.commsStation || entity?.fluidTank || entity?.shieldGenerator || entity?.shieldProjector) return "placed_entity";
|
|
1528
|
+
if (entity?.typeId != null && PLACED_ENTITY_TYPE_IDS.has(Number(entity.typeId))) return "placed_entity";
|
|
1529
|
+
if (entity?.typeId != null && !hasPhysicalComponent) return "metadata";
|
|
1530
|
+
if (entity?.itemHolder?.itemId != null && !entity?.cargoEjector && !entity?.cannon && !entity?.pusher && !entity?.launcher && !entity?.loader && !entity?.cargoHatch && !entity?.commsStation && !entity?.fluidTank && !entity?.shieldGenerator && !entity?.shieldProjector) return "untyped_holder";
|
|
1531
|
+
return "entity";
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Block coordinates are packed into a single number so the block index avoids a
|
|
1535
|
+
// template-string key per occupied cell. Covers the coordinate range in use.
|
|
1536
|
+
export const BLOCK_KEY_OFFSET = 1 << 24;
|
|
1537
|
+
export const BLOCK_KEY_STRIDE = 1 << 25;
|
|
1538
|
+
|
|
1539
|
+
export function blockKey(x, y) {
|
|
1540
|
+
return ((y + BLOCK_KEY_OFFSET) * BLOCK_KEY_STRIDE) + (x + BLOCK_KEY_OFFSET);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Returned by ModelState.table() for absent tables. Never mutated.
|
|
1544
|
+
export const EMPTY_TABLE = new Map();
|
|
1545
|
+
|
|
1546
|
+
// snapshot() only ever exposed the last 50 removals and last 10 errors, but both
|
|
1547
|
+
// arrays grew without bound behind that. Retain a little more than is surfaced.
|
|
1548
|
+
export const MAX_RETAINED_REMOVALS = 500;
|
|
1549
|
+
export const MAX_RETAINED_ERRORS = 100;
|
|
1550
|
+
|
|
1551
|
+
export function pushCapped(target, value, limit) {
|
|
1552
|
+
target.push(value);
|
|
1553
|
+
if (target.length > limit) target.splice(0, target.length - limit);
|
|
1554
|
+
}
|