t2-demo-parser 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/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/BitStream.d.ts +143 -0
- package/dist/BitStream.d.ts.map +1 -0
- package/dist/BitStream.js +369 -0
- package/dist/BitStream.js.map +1 -0
- package/dist/BitStream.test.d.ts +2 -0
- package/dist/BitStream.test.d.ts.map +1 -0
- package/dist/BitStream.test.js +419 -0
- package/dist/BitStream.test.js.map +1 -0
- package/dist/ClassRegistry.d.ts +100 -0
- package/dist/ClassRegistry.d.ts.map +1 -0
- package/dist/ClassRegistry.js +118 -0
- package/dist/ClassRegistry.js.map +1 -0
- package/dist/ClassRegistry.test.d.ts +2 -0
- package/dist/ClassRegistry.test.d.ts.map +1 -0
- package/dist/ClassRegistry.test.js +83 -0
- package/dist/ClassRegistry.test.js.map +1 -0
- package/dist/DataBlockParsers.d.ts +3 -0
- package/dist/DataBlockParsers.d.ts.map +1 -0
- package/dist/DataBlockParsers.js +2041 -0
- package/dist/DataBlockParsers.js.map +1 -0
- package/dist/DemoParser.d.ts +155 -0
- package/dist/DemoParser.d.ts.map +1 -0
- package/dist/DemoParser.js +943 -0
- package/dist/DemoParser.js.map +1 -0
- package/dist/DemoParser.test.d.ts +2 -0
- package/dist/DemoParser.test.d.ts.map +1 -0
- package/dist/DemoParser.test.js +472 -0
- package/dist/DemoParser.test.js.map +1 -0
- package/dist/EventParsers.d.ts +3 -0
- package/dist/EventParsers.d.ts.map +1 -0
- package/dist/EventParsers.js +603 -0
- package/dist/EventParsers.js.map +1 -0
- package/dist/GhostManager.d.ts +13 -0
- package/dist/GhostManager.d.ts.map +1 -0
- package/dist/GhostManager.js +2002 -0
- package/dist/GhostManager.js.map +1 -0
- package/dist/GhostTracker.test.d.ts +2 -0
- package/dist/GhostTracker.test.d.ts.map +1 -0
- package/dist/GhostTracker.test.js +64 -0
- package/dist/GhostTracker.test.js.map +1 -0
- package/dist/HuffmanProcessor.d.ts +12 -0
- package/dist/HuffmanProcessor.d.ts.map +1 -0
- package/dist/HuffmanProcessor.js +163 -0
- package/dist/HuffmanProcessor.js.map +1 -0
- package/dist/LiveParser.d.ts +16 -0
- package/dist/LiveParser.d.ts.map +1 -0
- package/dist/LiveParser.js +29 -0
- package/dist/LiveParser.js.map +1 -0
- package/dist/PacketParser.d.ts +104 -0
- package/dist/PacketParser.d.ts.map +1 -0
- package/dist/PacketParser.js +713 -0
- package/dist/PacketParser.js.map +1 -0
- package/dist/Timeline.d.ts +101 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +251 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +372 -0
- package/dist/cli.js.map +1 -0
- package/dist/dataBlockDataTypes.d.ts +877 -0
- package/dist/dataBlockDataTypes.d.ts.map +1 -0
- package/dist/dataBlockDataTypes.js +2 -0
- package/dist/dataBlockDataTypes.js.map +1 -0
- package/dist/dataTypes.d.ts +31 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +5 -0
- package/dist/dataTypes.js.map +1 -0
- package/dist/dump.d.ts +3 -0
- package/dist/dump.d.ts.map +1 -0
- package/dist/dump.js +36 -0
- package/dist/dump.js.map +1 -0
- package/dist/eventDataTypes.d.ts +199 -0
- package/dist/eventDataTypes.d.ts.map +1 -0
- package/dist/eventDataTypes.js +2 -0
- package/dist/eventDataTypes.js.map +1 -0
- package/dist/ghostDataTypes.d.ts +573 -0
- package/dist/ghostDataTypes.d.ts.map +1 -0
- package/dist/ghostDataTypes.js +2 -0
- package/dist/ghostDataTypes.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +261 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +204 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,2002 @@
|
|
|
1
|
+
import { MaxTriggerKeys } from "./types.js";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// Ghost Tracker — tracks ghost lifecycle (create/update/delete)
|
|
4
|
+
// ============================================================
|
|
5
|
+
export class GhostTracker {
|
|
6
|
+
ghosts = new Map();
|
|
7
|
+
getGhost(index) {
|
|
8
|
+
return this.ghosts.get(index);
|
|
9
|
+
}
|
|
10
|
+
hasGhost(index) {
|
|
11
|
+
return this.ghosts.has(index);
|
|
12
|
+
}
|
|
13
|
+
createGhost(index, classId, className) {
|
|
14
|
+
const entry = { classId, className, state: {} };
|
|
15
|
+
this.ghosts.set(index, entry);
|
|
16
|
+
return entry;
|
|
17
|
+
}
|
|
18
|
+
deleteGhost(index) {
|
|
19
|
+
this.ghosts.delete(index);
|
|
20
|
+
}
|
|
21
|
+
getAllGhosts() {
|
|
22
|
+
return this.ghosts;
|
|
23
|
+
}
|
|
24
|
+
size() {
|
|
25
|
+
return this.ghosts.size;
|
|
26
|
+
}
|
|
27
|
+
clear() {
|
|
28
|
+
this.ghosts.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Shared parsing helpers
|
|
33
|
+
// ============================================================
|
|
34
|
+
function readMove(bs) {
|
|
35
|
+
const pyaw = bs.readFlag() ? bs.readInt(16) : 0;
|
|
36
|
+
const ppitch = bs.readFlag() ? bs.readInt(16) : 0;
|
|
37
|
+
const proll = bs.readFlag() ? bs.readInt(16) : 0;
|
|
38
|
+
const px = bs.readInt(6);
|
|
39
|
+
const py = bs.readInt(6);
|
|
40
|
+
const pz = bs.readInt(6);
|
|
41
|
+
const freeLook = bs.readFlag();
|
|
42
|
+
const trigger = [];
|
|
43
|
+
for (let i = 0; i < MaxTriggerKeys; i++) {
|
|
44
|
+
trigger.push(bs.readFlag());
|
|
45
|
+
}
|
|
46
|
+
return { pyaw, ppitch, proll, px, py, pz, freeLook, trigger };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read a packed ColorF: 4 × U8 (32 bits total), each converted to [0..1] float.
|
|
50
|
+
* From decompiled binary: FUN_0043f040 reads 4 bytes, each × (1/255).
|
|
51
|
+
*/
|
|
52
|
+
function readPackedColorF(bs) {
|
|
53
|
+
return {
|
|
54
|
+
r: bs.readInt(8) / 255,
|
|
55
|
+
g: bs.readInt(8) / 255,
|
|
56
|
+
b: bs.readInt(8) / 255,
|
|
57
|
+
a: bs.readInt(8) / 255,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Tribes 2 object/datablock references are serialized via FUN_00436ce0/FUN_00436d10
|
|
62
|
+
* as raw 11-bit object ids (nextPow2(0x800) -> bitCount 11).
|
|
63
|
+
*/
|
|
64
|
+
function readObjectRef11(bs) {
|
|
65
|
+
return bs.readInt(11);
|
|
66
|
+
}
|
|
67
|
+
// ============================================================
|
|
68
|
+
// GameBase::unpackUpdate (base layer for all game objects)
|
|
69
|
+
// ============================================================
|
|
70
|
+
function readGameBaseUpdate(bs, _isInitial, _conn) {
|
|
71
|
+
const result = {};
|
|
72
|
+
// GameBase::unpackUpdate (FUN_005e3360)
|
|
73
|
+
// DataBlockMask
|
|
74
|
+
if (bs.readFlag()) {
|
|
75
|
+
result.dataBlockId = readObjectRef11(bs);
|
|
76
|
+
}
|
|
77
|
+
// TargetMask
|
|
78
|
+
if (bs.readFlag()) {
|
|
79
|
+
// Optional explicit target id; otherwise remains -1.
|
|
80
|
+
const hasTargetId = bs.readFlag();
|
|
81
|
+
result.targetId = hasTargetId ? bs.readInt(9) : -1;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
// ============================================================
|
|
86
|
+
// ShapeBase::unpackUpdate (layer 2 — damage, threads, images, etc.)
|
|
87
|
+
// ============================================================
|
|
88
|
+
function readShapeBaseUpdate(bs, isInitial, conn) {
|
|
89
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
90
|
+
// ShapeBase::unpackUpdate (FUN_005ef0e0)
|
|
91
|
+
// Combined gate for all remaining ShapeBase masks.
|
|
92
|
+
if (!bs.readFlag()) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
// DamageMask
|
|
96
|
+
if (bs.readFlag()) {
|
|
97
|
+
result.damageLevel = bs.readFloat(6); // DamageLevelBits=6
|
|
98
|
+
result.damageState = bs.readInt(2); // NumDamageStateBits=2
|
|
99
|
+
result.blowApart = bs.readFlag();
|
|
100
|
+
result.damageDir = bs.readNormalVector(8);
|
|
101
|
+
}
|
|
102
|
+
// SoundMask (4 sound slots)
|
|
103
|
+
if (bs.readFlag()) {
|
|
104
|
+
const sounds = [];
|
|
105
|
+
for (let i = 0; i < 4; i++) {
|
|
106
|
+
if (bs.readFlag()) {
|
|
107
|
+
const playing = bs.readFlag();
|
|
108
|
+
const sound = {
|
|
109
|
+
index: i,
|
|
110
|
+
playing,
|
|
111
|
+
};
|
|
112
|
+
if (playing) {
|
|
113
|
+
sound.profileId = readObjectRef11(bs);
|
|
114
|
+
}
|
|
115
|
+
sounds.push(sound);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (sounds.length > 0) {
|
|
119
|
+
result.sounds = sounds;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ThreadMask (script animation threads, 4 slots)
|
|
123
|
+
if (bs.readFlag()) {
|
|
124
|
+
const threads = [];
|
|
125
|
+
for (let i = 0; i < 4; i++) {
|
|
126
|
+
if (bs.readFlag()) {
|
|
127
|
+
threads.push({
|
|
128
|
+
index: i,
|
|
129
|
+
sequence: bs.readInt(5),
|
|
130
|
+
state: bs.readInt(2),
|
|
131
|
+
forward: bs.readFlag(),
|
|
132
|
+
atEnd: bs.readFlag(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (threads.length > 0) {
|
|
137
|
+
result.threads = threads;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ImageMask (8 mounted image slots)
|
|
141
|
+
let imageSkinDirty = false;
|
|
142
|
+
if (bs.readFlag()) {
|
|
143
|
+
const images = [];
|
|
144
|
+
for (let i = 0; i < 8; i++) {
|
|
145
|
+
if (bs.readFlag()) {
|
|
146
|
+
const img = { index: i };
|
|
147
|
+
// Optional image datablock reference.
|
|
148
|
+
if (bs.readFlag()) {
|
|
149
|
+
img.dataBlockId = readObjectRef11(bs);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
img.dataBlockId = 0;
|
|
153
|
+
}
|
|
154
|
+
// FUN_00588870 payload: hasSkin -> (tagIndex|string).
|
|
155
|
+
if (bs.readFlag()) {
|
|
156
|
+
if (bs.readFlag()) {
|
|
157
|
+
img.skinTagIndex = bs.readInt(10);
|
|
158
|
+
imageSkinDirty = true;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
img.skinName = bs.readString();
|
|
162
|
+
imageSkinDirty = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Per-slot state flags + 3-bit fire count.
|
|
166
|
+
// The retail Tribes 2 binary (FUN_005ef0e0) packs these in a different
|
|
167
|
+
// order than the V12 SDK source code. Verified via Ghidra decompilation:
|
|
168
|
+
// the binary stores to offsets 0x3ac (triggerDown), 0x398 (loaded),
|
|
169
|
+
// 0x3ad (ammo), 0x3af (wet), 0x3ae (target), confirmed by matching the
|
|
170
|
+
// setImage(slot, db, skin, loaded, ammo, triggerDown) call signature.
|
|
171
|
+
img.triggerDown = bs.readFlag();
|
|
172
|
+
img.loaded = bs.readFlag();
|
|
173
|
+
img.ammo = bs.readFlag();
|
|
174
|
+
img.wet = bs.readFlag();
|
|
175
|
+
img.target = bs.readFlag();
|
|
176
|
+
img.fireCount = bs.readInt(3);
|
|
177
|
+
// Mounted-image firing bit (FUN_005ef0e0):
|
|
178
|
+
// Read when (this+0x18 & 8)==0, i.e., when the object is NOT
|
|
179
|
+
// "properly added" to the scene. Initial block creates and packet
|
|
180
|
+
// stream ghost creates are NOT properly added (isInitial=true),
|
|
181
|
+
// but packet stream ghost updates ARE properly added (isInitial=false).
|
|
182
|
+
if (isInitial) {
|
|
183
|
+
img.imageExtraFlag = bs.readFlag();
|
|
184
|
+
}
|
|
185
|
+
images.push(img);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (images.length > 0) {
|
|
189
|
+
result.images = images;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// CloakMask section (ShapeBase, binary-verified FUN_005ef0e0).
|
|
193
|
+
// Combined mask: ShieldMask | CloakMask | InvincibleMask (0x1c0).
|
|
194
|
+
if (bs.readFlag()) {
|
|
195
|
+
// CloakMask sub-section.
|
|
196
|
+
const hasCloakData = bs.readFlag();
|
|
197
|
+
if (hasCloakData) {
|
|
198
|
+
result.cloaked = bs.readFlag(); // mCloaked (stealth pack active)
|
|
199
|
+
result.isControlled = bs.readFlag(); // bool(getControllingClient())
|
|
200
|
+
const fading = bs.readFlag(); // mFading && mFadeElapsedTime >= mFadeDelay
|
|
201
|
+
result.fading = fading;
|
|
202
|
+
if (fading) {
|
|
203
|
+
result.fadeOut = bs.readFlag(); // mFadeOut (true=fading out, false=fading in)
|
|
204
|
+
result.fadeTime = bs.readF32(); // mFadeTime (duration in seconds)
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
result.fadeVal = bs.readFlag(); // mFadeVal == 1.0 (true=visible, false=hidden)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const hasStateB = bs.readFlag();
|
|
211
|
+
if (hasStateB) {
|
|
212
|
+
const useStateBFlag = bs.readFlag();
|
|
213
|
+
if (useStateBFlag) {
|
|
214
|
+
const stateBMode = bs.readFlag();
|
|
215
|
+
result.stateBMode = stateBMode;
|
|
216
|
+
if (stateBMode) {
|
|
217
|
+
result.energyPackOn = true;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
result.energyPackOn = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
result.shieldNormal = bs.readNormalVector(8);
|
|
225
|
+
result.energyPercent = bs.readFloat(5);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (bs.readFlag()) {
|
|
229
|
+
result.stateValue1 = bs.readU32();
|
|
230
|
+
result.stateValue2 = bs.readU32();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (imageSkinDirty) {
|
|
234
|
+
result.imageSkinDirty = true;
|
|
235
|
+
}
|
|
236
|
+
// MountedMask
|
|
237
|
+
if (bs.readFlag()) {
|
|
238
|
+
if (bs.readFlag()) {
|
|
239
|
+
// Mounting
|
|
240
|
+
result.mountObject = bs.readInt(10);
|
|
241
|
+
result.mountNode = bs.readInt(5); // NumMountPointBits=5
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
result.mountObject = -1; // Unmounting
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
// ============================================================
|
|
250
|
+
// Player ghost parser
|
|
251
|
+
// ============================================================
|
|
252
|
+
function playerUnpackUpdate(bs, isInitial, conn) {
|
|
253
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
254
|
+
// ImpactMask (only on non-initial updates)
|
|
255
|
+
if (bs.readFlag()) {
|
|
256
|
+
result.impactSound = bs.readInt(3); // ImpactBits=3
|
|
257
|
+
}
|
|
258
|
+
// ActionMask — action animation
|
|
259
|
+
if (bs.readFlag()) {
|
|
260
|
+
result.action = bs.readInt(8); // ActionAnimBits=8
|
|
261
|
+
result.actionHoldAtEnd = bs.readFlag();
|
|
262
|
+
result.actionAtEnd = bs.readFlag();
|
|
263
|
+
result.actionFirstPerson = bs.readFlag();
|
|
264
|
+
if (!result.actionAtEnd) {
|
|
265
|
+
if (bs.readFlag()) {
|
|
266
|
+
result.actionAnimPos = bs.readSignedFloat(6);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ActionMask — arm animation
|
|
271
|
+
if (bs.readFlag()) {
|
|
272
|
+
result.armAction = bs.readInt(8); // ActionAnimBits=8
|
|
273
|
+
}
|
|
274
|
+
// Control object shortcut — if controlled by this client, skip rest
|
|
275
|
+
if (bs.readFlag()) {
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
// MoveMask
|
|
279
|
+
if (bs.readFlag()) {
|
|
280
|
+
result.actionState = bs.readInt(3); // NumStateBits=3
|
|
281
|
+
if (bs.readFlag()) {
|
|
282
|
+
// RecoverState
|
|
283
|
+
result.recoverTicks = bs.readInt(7); // RecoverDelayBits=7
|
|
284
|
+
}
|
|
285
|
+
// FUN_005db2d0: two state flags immediately before compressed position.
|
|
286
|
+
result.moveFlag0 = bs.readFlag();
|
|
287
|
+
result.moveFlag1 = bs.readFlag();
|
|
288
|
+
result.position = bs.readCompressedPoint(conn.compressionPoint);
|
|
289
|
+
// Velocity
|
|
290
|
+
if (bs.readFlag()) {
|
|
291
|
+
// Binary (0x5db850): readInt(13) FIRST, then readNormalVector(10)
|
|
292
|
+
const velMag = bs.readInt(13) / 32.0;
|
|
293
|
+
const velDir = bs.readNormalVector(10);
|
|
294
|
+
result.velocity = {
|
|
295
|
+
x: velDir.x * velMag,
|
|
296
|
+
y: velDir.y * velMag,
|
|
297
|
+
z: velDir.z * velMag,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
result.velocity = { x: 0, y: 0, z: 0 };
|
|
302
|
+
}
|
|
303
|
+
result.headX = bs.readSignedFloat(6); // * maxLookAngle
|
|
304
|
+
result.headZ = bs.readSignedFloat(6); // * maxLookAngle
|
|
305
|
+
result.rotationZ = bs.readFloat(7) * 2 * Math.PI;
|
|
306
|
+
result.move = readMove(bs);
|
|
307
|
+
result.allowWarp = bs.readFlag();
|
|
308
|
+
}
|
|
309
|
+
// Energy (always present after MoveMask section)
|
|
310
|
+
result.energy = bs.readFloat(5); // EnergyLevelBits=5
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
function playerReadPacketData(bs, conn) {
|
|
314
|
+
const result = {};
|
|
315
|
+
// ShapeBase::readPacketData
|
|
316
|
+
result.energyLevel = bs.readF32();
|
|
317
|
+
result.rechargeRate = bs.readF32();
|
|
318
|
+
// Player::readPacketData
|
|
319
|
+
result.actionState = bs.readInt(3); // NumStateBits=3
|
|
320
|
+
if (bs.readFlag()) {
|
|
321
|
+
// RecoverState
|
|
322
|
+
result.recoverTicks = bs.readInt(7);
|
|
323
|
+
}
|
|
324
|
+
if (bs.readFlag()) {
|
|
325
|
+
result.jumpDelay = bs.readInt(7); // JumpDelayBits=7
|
|
326
|
+
}
|
|
327
|
+
if (bs.readFlag()) {
|
|
328
|
+
// Not mounted — read full position/velocity
|
|
329
|
+
const pos = {
|
|
330
|
+
x: bs.readF32(),
|
|
331
|
+
y: bs.readF32(),
|
|
332
|
+
z: bs.readF32(),
|
|
333
|
+
};
|
|
334
|
+
result.position = pos;
|
|
335
|
+
// Update compression point (this IS the compression point)
|
|
336
|
+
conn.compressionPoint = pos;
|
|
337
|
+
result.velocity = {
|
|
338
|
+
x: bs.readF32(),
|
|
339
|
+
y: bs.readF32(),
|
|
340
|
+
z: bs.readF32(),
|
|
341
|
+
};
|
|
342
|
+
result.jumpSurfaceLastContact = bs.readInt(4);
|
|
343
|
+
}
|
|
344
|
+
result.headX = bs.readF32();
|
|
345
|
+
result.headZ = bs.readF32();
|
|
346
|
+
result.rotationZ = bs.readF32();
|
|
347
|
+
if (bs.readFlag()) {
|
|
348
|
+
// Has control object (e.g., vehicle being piloted)
|
|
349
|
+
// Binary FUN_005dab20: readInt(10) + resolveGhost + obj->readPacketData(conn, stream)
|
|
350
|
+
const controlGhostIndex = bs.readInt(10);
|
|
351
|
+
result.controlObjectGhost = controlGhostIndex;
|
|
352
|
+
// Recursively call the control object's readPacketData.
|
|
353
|
+
// Look up the ghost class and find its readPacketData parser.
|
|
354
|
+
const controlGhost = conn.ghostTracker.getGhost(controlGhostIndex);
|
|
355
|
+
const controlParser = controlGhost
|
|
356
|
+
? conn.getGhostParser?.(controlGhost.classId)
|
|
357
|
+
: undefined;
|
|
358
|
+
if (controlParser?.readPacketData) {
|
|
359
|
+
const prevGhostIndex = conn.currentGhostIndex;
|
|
360
|
+
conn.currentGhostIndex = controlGhostIndex;
|
|
361
|
+
result.controlObjectData = controlParser.readPacketData(bs, conn);
|
|
362
|
+
conn.currentGhostIndex = prevGhostIndex;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
result.disableMove = bs.readFlag();
|
|
366
|
+
result.pilot = bs.readFlag();
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
// ============================================================
|
|
370
|
+
// Vehicle ghost parser
|
|
371
|
+
// ============================================================
|
|
372
|
+
function vehicleUnpackUpdate(bs, isInitial, conn) {
|
|
373
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
374
|
+
result.jetting = bs.readFlag();
|
|
375
|
+
// Control object shortcut — server writes true and returns early,
|
|
376
|
+
// meaning no further Vehicle data follows in the stream.
|
|
377
|
+
if (bs.readFlag()) {
|
|
378
|
+
result._controlledEarlyReturn = true;
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
// Steering
|
|
382
|
+
result.steeringYaw = bs.readFloat(9);
|
|
383
|
+
result.steeringPitch = bs.readFloat(9);
|
|
384
|
+
result.move = readMove(bs);
|
|
385
|
+
result.frozen = bs.readFlag();
|
|
386
|
+
// PositionMask
|
|
387
|
+
if (bs.readFlag()) {
|
|
388
|
+
result.position = bs.readCompressedPoint(conn.compressionPoint);
|
|
389
|
+
// Angular position as quaternion (4x F32)
|
|
390
|
+
result.angPosition = {
|
|
391
|
+
x: bs.readF32(),
|
|
392
|
+
y: bs.readF32(),
|
|
393
|
+
z: bs.readF32(),
|
|
394
|
+
w: bs.readF32(),
|
|
395
|
+
};
|
|
396
|
+
// Linear momentum (3x F32)
|
|
397
|
+
result.linMomentum = bs.readPoint3F();
|
|
398
|
+
// Angular momentum (3x F32)
|
|
399
|
+
result.angMomentum = bs.readPoint3F();
|
|
400
|
+
}
|
|
401
|
+
// EnergyMask
|
|
402
|
+
if (bs.readFlag()) {
|
|
403
|
+
result.energy = bs.readFloat(8);
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
function vehicleReadPacketData(bs, conn) {
|
|
408
|
+
// Vehicle::readPacketData (binary FUN_0060d740)
|
|
409
|
+
// ShapeBase::readPacketData → GameBase::readPacketData (empty) + energy + recharge
|
|
410
|
+
const result = {};
|
|
411
|
+
result.energyLevel = bs.readF32();
|
|
412
|
+
result.rechargeRate = bs.readF32();
|
|
413
|
+
// Vehicle-specific rigid body state
|
|
414
|
+
result.steering = { x: bs.readF32(), y: bs.readF32() };
|
|
415
|
+
const linPos = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
416
|
+
result.linPosition = linPos;
|
|
417
|
+
result.angPosition = {
|
|
418
|
+
x: bs.readF32(),
|
|
419
|
+
y: bs.readF32(),
|
|
420
|
+
z: bs.readF32(),
|
|
421
|
+
w: bs.readF32(),
|
|
422
|
+
};
|
|
423
|
+
result.linMomentum = bs.readPoint3F();
|
|
424
|
+
result.angMomentum = bs.readPoint3F();
|
|
425
|
+
result.disableMove = bs.readFlag();
|
|
426
|
+
result.frozen = bs.readFlag();
|
|
427
|
+
conn.compressionPoint = linPos;
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
function wheeledVehicleReadPacketData(bs, conn) {
|
|
431
|
+
// WheeledVehicle::readPacketData (binary FUN_00615840)
|
|
432
|
+
// Calls Parent::readPacketData (Vehicle) + braking flag + per-wheel data
|
|
433
|
+
const result = vehicleReadPacketData(bs, conn);
|
|
434
|
+
result.braking = bs.readFlag();
|
|
435
|
+
// wheelCount × 3 F32 (avel, Dy, Dx per wheel)
|
|
436
|
+
// Use ghost wheel count cache (populated during ghost creates)
|
|
437
|
+
let wheelCount = 4;
|
|
438
|
+
const ghostIdx = conn.currentGhostIndex;
|
|
439
|
+
if (ghostIdx !== undefined) {
|
|
440
|
+
const cached = ghostWheelCountCache.get(ghostIdx);
|
|
441
|
+
if (cached !== undefined) {
|
|
442
|
+
wheelCount = cached;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const wheels = [];
|
|
446
|
+
for (let i = 0; i < wheelCount; i++) {
|
|
447
|
+
wheels.push({
|
|
448
|
+
avel: bs.readF32(),
|
|
449
|
+
dy: bs.readF32(),
|
|
450
|
+
dx: bs.readF32(),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
result.wheels = wheels;
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
// ============================================================
|
|
457
|
+
// FlyingVehicle ghost parser
|
|
458
|
+
// ============================================================
|
|
459
|
+
function flyingVehicleUnpackUpdate(bs, isInitial, conn) {
|
|
460
|
+
const result = vehicleUnpackUpdate(bs, isInitial, conn);
|
|
461
|
+
// FlyingVehicle has its own control gate flag (separate from Vehicle's).
|
|
462
|
+
// Retail binary removed the V12 ThrustMask wrapper but kept the per-subclass
|
|
463
|
+
// control check: if true, no FlyingVehicle-specific data follows.
|
|
464
|
+
if (bs.readFlag()) {
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
result.createHeightOn = bs.readFlag();
|
|
468
|
+
result.thrustDirection = bs.readInt(3); // NumThrustBits=3
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
// ============================================================
|
|
472
|
+
// HoverVehicle ghost parser
|
|
473
|
+
// ============================================================
|
|
474
|
+
function hoverVehicleUnpackUpdate(bs, isInitial, conn) {
|
|
475
|
+
const result = vehicleUnpackUpdate(bs, isInitial, conn);
|
|
476
|
+
result.thrustDirection = bs.readInt(3); // NumThrustBits=3
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
// ============================================================
|
|
480
|
+
// Item ghost parser
|
|
481
|
+
// ============================================================
|
|
482
|
+
function itemUnpackUpdate(bs, isInitial, conn) {
|
|
483
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
484
|
+
// InitialUpdateMask
|
|
485
|
+
if (bs.readFlag()) {
|
|
486
|
+
result.rotate = bs.readFlag();
|
|
487
|
+
result.isStatic = bs.readFlag();
|
|
488
|
+
result.collideable = bs.readFlag();
|
|
489
|
+
if (bs.readFlag()) {
|
|
490
|
+
result.scale = bs.readPoint3F();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// ThrowSrcMask
|
|
494
|
+
if (bs.readFlag()) {
|
|
495
|
+
result.collisionObject = bs.readInt(10);
|
|
496
|
+
}
|
|
497
|
+
// RotationMask (only if !rotate)
|
|
498
|
+
if (bs.readFlag()) {
|
|
499
|
+
const zSign = bs.readFlag() ? -1 : 1;
|
|
500
|
+
const angle = bs.readF32();
|
|
501
|
+
result.rotation = { zSign, angle };
|
|
502
|
+
}
|
|
503
|
+
// PositionMask
|
|
504
|
+
if (bs.readFlag()) {
|
|
505
|
+
result.position = bs.readPoint3F();
|
|
506
|
+
const atRest = bs.readFlag();
|
|
507
|
+
result.atRest = atRest;
|
|
508
|
+
if (!atRest) {
|
|
509
|
+
result.velocity = bs.readPoint3F();
|
|
510
|
+
}
|
|
511
|
+
result.warp = bs.readFlag();
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
// ============================================================
|
|
516
|
+
// StaticShape ghost parser
|
|
517
|
+
// ============================================================
|
|
518
|
+
function staticShapeUnpackUpdate(bs, isInitial, conn) {
|
|
519
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
520
|
+
// PositionMask
|
|
521
|
+
if (bs.readFlag()) {
|
|
522
|
+
// FUN_00602da0 -> FUN_0043c4c0 reads compressed affine transform
|
|
523
|
+
// (position + quaternion xyz + sign bit), not a full MatrixF.
|
|
524
|
+
const transform = bs.readAffineTransform();
|
|
525
|
+
result.transform = transform;
|
|
526
|
+
result.position = transform.position;
|
|
527
|
+
result.scale = bs.readPoint3F();
|
|
528
|
+
}
|
|
529
|
+
// Power state (always present)
|
|
530
|
+
result.powered = bs.readFlag();
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
function scopeAlwaysShapeUnpackUpdate(bs, isInitial, conn) {
|
|
534
|
+
// ScopeAlwaysShape vtable +0x4c -> FUN_00602da0 (same as StaticShape).
|
|
535
|
+
return staticShapeUnpackUpdate(bs, isInitial, conn);
|
|
536
|
+
}
|
|
537
|
+
function markerUnpackUpdate(bs, _isInitial, _conn) {
|
|
538
|
+
// Marker::unpackUpdate (simPath.cc): Parent::unpackUpdate (SceneObject,
|
|
539
|
+
// no data) then reads just a Point3F position.
|
|
540
|
+
const position = bs.readPoint3F();
|
|
541
|
+
return { position };
|
|
542
|
+
}
|
|
543
|
+
function simpleNetObjectUnpackUpdate(bs, _isInitial, _conn) {
|
|
544
|
+
// SimpleNetObject vtable +0x4c -> FUN_005c5220 (single string).
|
|
545
|
+
return { message: bs.readString() };
|
|
546
|
+
}
|
|
547
|
+
function beaconObjectUnpackUpdate(bs, isInitial, conn) {
|
|
548
|
+
// BeaconObject vtable +0x4c -> FUN_006a3ae0:
|
|
549
|
+
// StaticShape payload + optional 2-bit beacon type.
|
|
550
|
+
const result = staticShapeUnpackUpdate(bs, isInitial, conn);
|
|
551
|
+
if (bs.readFlag()) {
|
|
552
|
+
result.beaconType = bs.readInt(2);
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
function missionMarkerUnpackUpdate(bs, isInitial, conn) {
|
|
557
|
+
// MissionMarker extends ShapeBase (confirmed by V12 source and empirical
|
|
558
|
+
// testing). The Ghidra decompilation misidentified the parent call — the
|
|
559
|
+
// real parent is ShapeBase::unpackUpdate, not an empty stub.
|
|
560
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
561
|
+
// MissionMarker's own field: PositionMask
|
|
562
|
+
if (bs.readFlag()) {
|
|
563
|
+
const transform = bs.readAffineTransform();
|
|
564
|
+
result.transform = transform;
|
|
565
|
+
result.position = transform.position;
|
|
566
|
+
result.scale = bs.readPoint3F();
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
function debrisUnpackUpdate(bs, isInitial, conn) {
|
|
571
|
+
// Debris vtable +0x4c -> FUN_006844d0.
|
|
572
|
+
// Confirmed in decompiled binary: calls parent GameBase::unpackUpdate first.
|
|
573
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
574
|
+
// 6 × F32
|
|
575
|
+
result.value0 = bs.readF32();
|
|
576
|
+
result.value1 = bs.readF32();
|
|
577
|
+
result.value2 = bs.readF32();
|
|
578
|
+
result.value3 = bs.readF32();
|
|
579
|
+
result.value4 = bs.readF32();
|
|
580
|
+
result.value5 = bs.readF32();
|
|
581
|
+
// 4 × bool (readBool = 8-bit)
|
|
582
|
+
result.bool0 = bs.readBool();
|
|
583
|
+
result.bool1 = bs.readBool();
|
|
584
|
+
result.bool2 = bs.readBool();
|
|
585
|
+
result.bool3 = bs.readBool();
|
|
586
|
+
// 6 × F32
|
|
587
|
+
result.value6 = bs.readF32();
|
|
588
|
+
result.value7 = bs.readF32();
|
|
589
|
+
result.value8 = bs.readF32();
|
|
590
|
+
result.value9 = bs.readF32();
|
|
591
|
+
result.value10 = bs.readF32();
|
|
592
|
+
result.value11 = bs.readF32();
|
|
593
|
+
// 2 × bool
|
|
594
|
+
result.bool4 = bs.readBool();
|
|
595
|
+
result.bool5 = bs.readBool();
|
|
596
|
+
// 3 × F32
|
|
597
|
+
result.value12 = bs.readF32();
|
|
598
|
+
result.value13 = bs.readF32();
|
|
599
|
+
result.value14 = bs.readF32();
|
|
600
|
+
// 1 × bool
|
|
601
|
+
result.bool6 = bs.readBool();
|
|
602
|
+
// 2 × string
|
|
603
|
+
result.string0 = bs.readString();
|
|
604
|
+
result.string1 = bs.readString();
|
|
605
|
+
// 3 conditional object references
|
|
606
|
+
const refs = [];
|
|
607
|
+
for (let i = 0; i < 2; i++) {
|
|
608
|
+
refs.push(bs.readFlag() ? readObjectRef11(bs) : -1);
|
|
609
|
+
}
|
|
610
|
+
result.objectRefs = refs;
|
|
611
|
+
result.objectRef2 = bs.readFlag() ? readObjectRef11(bs) : -1;
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
function projectileUnpackUpdate(bs, isInitial, conn) {
|
|
615
|
+
// Projectile vtable +0x4c -> FUN_005e3360 (GameBase only).
|
|
616
|
+
return readGameBaseUpdate(bs, isInitial, conn);
|
|
617
|
+
}
|
|
618
|
+
// ============================================================
|
|
619
|
+
// Projectile ghost parsers
|
|
620
|
+
// ============================================================
|
|
621
|
+
function bombProjectileUnpackUpdate(bs, _isInitial, _conn) {
|
|
622
|
+
// BombProjectile vtable +0x4c -> FUN_00636050.
|
|
623
|
+
const result = {};
|
|
624
|
+
// Parent GameBase::unpackUpdate
|
|
625
|
+
if (bs.readFlag()) {
|
|
626
|
+
result.dataBlockId = readObjectRef11(bs);
|
|
627
|
+
}
|
|
628
|
+
if (bs.readFlag()) {
|
|
629
|
+
const hasTargetId = bs.readFlag();
|
|
630
|
+
result.targetId = hasTargetId ? bs.readInt(9) : -1;
|
|
631
|
+
}
|
|
632
|
+
if (!bs.readFlag()) {
|
|
633
|
+
if (bs.readFlag()) {
|
|
634
|
+
result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
635
|
+
result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
636
|
+
}
|
|
637
|
+
if (!bs.readFlag()) {
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
result.endPoint = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
641
|
+
result.endNormal = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
645
|
+
result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
646
|
+
result.currTick = bs.readInt(12); // nextPow2(0x1000) -> 12 bits
|
|
647
|
+
if (bs.readFlag()) {
|
|
648
|
+
result.resetFlag = true; // FUN_00636ae0 side-effect-only
|
|
649
|
+
}
|
|
650
|
+
if (bs.readFlag()) {
|
|
651
|
+
result.explodePoint = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
652
|
+
result.explodeNormal = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
653
|
+
}
|
|
654
|
+
if (bs.readFlag()) {
|
|
655
|
+
result.sourceObject = bs.readInt(11); // nextPow2(0x401) -> 11 bits
|
|
656
|
+
result.sourceSlot = bs.readInt(3); // nextPow2(8) -> 3 bits
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
result.sourceObject = -1;
|
|
660
|
+
result.sourceSlot = -1;
|
|
661
|
+
}
|
|
662
|
+
if (bs.readFlag()) {
|
|
663
|
+
result.vehicleObject = bs.readInt(11); // nextPow2(0x401) -> 11 bits
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
result.vehicleObject = 0;
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
function grenadeUnpackUpdate(bs, isInitial, conn) {
|
|
671
|
+
// Parent: Projectile → GameBase (Projectile has no override)
|
|
672
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
673
|
+
if (bs.readFlag()) {
|
|
674
|
+
// InitialUpdateMask
|
|
675
|
+
result.position = bs.readPoint3F();
|
|
676
|
+
result.velocity = bs.readPoint3F();
|
|
677
|
+
result.currTick = bs.readRangedU32(0, 4095);
|
|
678
|
+
result.quickSplash = bs.readFlag();
|
|
679
|
+
if (bs.readFlag()) {
|
|
680
|
+
result.explodePoint = bs.readPoint3F();
|
|
681
|
+
result.explodeNormal = bs.readPoint3F();
|
|
682
|
+
}
|
|
683
|
+
if (bs.readFlag()) {
|
|
684
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
685
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
686
|
+
}
|
|
687
|
+
if (bs.readFlag()) {
|
|
688
|
+
result.vehicleObject = bs.readRangedU32(0, 1024);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
// Non-initial
|
|
693
|
+
if (bs.readFlag()) {
|
|
694
|
+
// BounceMask
|
|
695
|
+
result.position = bs.readPoint3F();
|
|
696
|
+
result.velocity = bs.readPoint3F();
|
|
697
|
+
}
|
|
698
|
+
if (bs.readFlag()) {
|
|
699
|
+
// ExplosionMask
|
|
700
|
+
result.explodePoint = bs.readPoint3F();
|
|
701
|
+
result.explodeNormal = bs.readPoint3F();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
function seekerUnpackUpdate(bs, isInitial, conn) {
|
|
707
|
+
// SeekerProjectile::unpackUpdate in build 25034 is FUN_0063c010
|
|
708
|
+
// (vtable PTR_FUN_007ae54c + 0x4c), with parent GameBase::unpackUpdate.
|
|
709
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
710
|
+
const fullStateMask = bs.readFlag();
|
|
711
|
+
if (!fullStateMask) {
|
|
712
|
+
// 0x0063c010 non-full path
|
|
713
|
+
const isExplosion = bs.readFlag();
|
|
714
|
+
if (isExplosion) {
|
|
715
|
+
// FUN_00639370: explode(position, normal) — SeekerProjectile detonation.
|
|
716
|
+
result.explodePosition = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
717
|
+
result.explodeNormal = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
721
|
+
result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
722
|
+
const hasTargetInfo = bs.readFlag();
|
|
723
|
+
if (hasTargetInfo) {
|
|
724
|
+
const hasTargetGhost = bs.readFlag();
|
|
725
|
+
if (!hasTargetGhost) {
|
|
726
|
+
result.targetDirection = {
|
|
727
|
+
x: bs.readF32(),
|
|
728
|
+
y: bs.readF32(),
|
|
729
|
+
z: bs.readF32(),
|
|
730
|
+
};
|
|
731
|
+
result.targetMode = 1;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
// getBitCount(getNextPow2(0x401)) -> 11 bits
|
|
735
|
+
result.targetGhost = bs.readInt(11);
|
|
736
|
+
result.targetMode = 0;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
result.targetMode = 2;
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
744
|
+
// 0x0063c010 full-state path
|
|
745
|
+
result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
746
|
+
result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
747
|
+
result.orientation = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
|
|
748
|
+
// Optional source object + slot
|
|
749
|
+
if (bs.readFlag()) {
|
|
750
|
+
result.sourceObject = bs.readInt(11); // getBitCount(getNextPow2(0x401))
|
|
751
|
+
result.sourceSlot = bs.readInt(3); // getBitCount(getNextPow2(8))
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
result.sourceObject = -1;
|
|
755
|
+
result.sourceSlot = -1;
|
|
756
|
+
}
|
|
757
|
+
// Optional target descriptor
|
|
758
|
+
if (bs.readFlag()) {
|
|
759
|
+
const hasTargetGhost = bs.readFlag();
|
|
760
|
+
if (!hasTargetGhost) {
|
|
761
|
+
result.targetDirection = {
|
|
762
|
+
x: bs.readF32(),
|
|
763
|
+
y: bs.readF32(),
|
|
764
|
+
z: bs.readF32(),
|
|
765
|
+
};
|
|
766
|
+
result.targetMode = 1;
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
result.targetGhost = bs.readInt(11); // getBitCount(getNextPow2(0x401))
|
|
770
|
+
result.targetMode = 0;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
result.targetMode = 2;
|
|
775
|
+
}
|
|
776
|
+
// Final timeout/alive flag branch in FUN_0063c010.
|
|
777
|
+
result.timeoutReset = bs.readFlag();
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
// ============================================================
|
|
781
|
+
// Turret ghost parser
|
|
782
|
+
// ============================================================
|
|
783
|
+
function turretUnpackUpdate(bs, isInitial, conn) {
|
|
784
|
+
// Turret extends StaticShape (confirmed in decompiled binary FUN_00655f60
|
|
785
|
+
// calls FUN_00602da0 = StaticShape::unpackUpdate as parent)
|
|
786
|
+
const result = staticShapeUnpackUpdate(bs, isInitial, conn);
|
|
787
|
+
// Capacitor energy (Tribes 2 specific — not in V12/TorqueSDK)
|
|
788
|
+
if (bs.readFlag()) {
|
|
789
|
+
result.capacitorEnergy = bs.readFloat(8);
|
|
790
|
+
}
|
|
791
|
+
// Control client shortcut
|
|
792
|
+
if (bs.readFlag()) {
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
// Barrel rotation state
|
|
796
|
+
if (bs.readFlag()) {
|
|
797
|
+
result.phi = bs.readFloat(10);
|
|
798
|
+
result.theta = bs.readFloat(10);
|
|
799
|
+
result.activationLevel = bs.readFloat(8);
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
// ============================================================
|
|
804
|
+
// InteriorInstance ghost parser (partial — can't parse light groups without resource)
|
|
805
|
+
// ============================================================
|
|
806
|
+
function interiorUnpackUpdate(bs, _isInitial, _conn) {
|
|
807
|
+
const result = {};
|
|
808
|
+
if (bs.readFlag()) {
|
|
809
|
+
// InitMask — full initial state
|
|
810
|
+
result.crc = bs.readU32();
|
|
811
|
+
result.interiorFile = bs.readString();
|
|
812
|
+
result.showTerrainInside = bs.readFlag();
|
|
813
|
+
result.transform = bs.readMatrixF();
|
|
814
|
+
result.scale = bs.readPoint3F();
|
|
815
|
+
result.alarmState = bs.readFlag();
|
|
816
|
+
result.skinBase = bs.readString();
|
|
817
|
+
if (bs.readFlag()) {
|
|
818
|
+
result.audioProfileId = readObjectRef11(bs);
|
|
819
|
+
}
|
|
820
|
+
if (bs.readFlag()) {
|
|
821
|
+
result.audioEnvironmentId = readObjectRef11(bs);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// Normal update
|
|
826
|
+
if (bs.readFlag()) {
|
|
827
|
+
// TransformMask
|
|
828
|
+
result.transform = bs.readMatrixF();
|
|
829
|
+
result.scale = bs.readPoint3F();
|
|
830
|
+
}
|
|
831
|
+
result.alarmState = bs.readFlag();
|
|
832
|
+
// LightUpdateGrouper — iterates over bits 3-10 (8 slots), each slot
|
|
833
|
+
// containing a dirty flag + N active flags (one per light in that group).
|
|
834
|
+
// The loop exits at the first bit position with 0 keys. We don't know
|
|
835
|
+
// the interior resource so we can't determine key counts. For interiors
|
|
836
|
+
// with 0 animated lights (the common case), no bits are written here.
|
|
837
|
+
// We assume 0 and continue to the remaining masks.
|
|
838
|
+
// SkinBaseMask
|
|
839
|
+
if (bs.readFlag()) {
|
|
840
|
+
result.skinBase = bs.readString();
|
|
841
|
+
}
|
|
842
|
+
// AudioMask
|
|
843
|
+
if (bs.readFlag()) {
|
|
844
|
+
if (bs.readFlag()) {
|
|
845
|
+
result.audioProfileId = readObjectRef11(bs);
|
|
846
|
+
}
|
|
847
|
+
if (bs.readFlag()) {
|
|
848
|
+
result.audioEnvironmentId = readObjectRef11(bs);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
// ============================================================
|
|
855
|
+
// Camera ghost parser (simple)
|
|
856
|
+
// ============================================================
|
|
857
|
+
function cameraUnpackUpdate(bs, isInitial, conn) {
|
|
858
|
+
// Camera::unpackUpdate (FUN_005cc8c0):
|
|
859
|
+
// 1) ShapeBase::unpackUpdate (FUN_005ef0e0)
|
|
860
|
+
// 2) if (control object shortcut flag) return
|
|
861
|
+
// 3) if (camera update flag) read 5 x F32
|
|
862
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
863
|
+
// Controlled by local connection: no additional payload in this update.
|
|
864
|
+
if (bs.readFlag()) {
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
// Camera-specific payload (5 floats in retail Tribes2.exe).
|
|
868
|
+
if (bs.readFlag()) {
|
|
869
|
+
result.posX = bs.readF32();
|
|
870
|
+
result.posY = bs.readF32();
|
|
871
|
+
result.posZ = bs.readF32();
|
|
872
|
+
result.fovOrDist = bs.readF32();
|
|
873
|
+
result.orbitParam = bs.readF32();
|
|
874
|
+
}
|
|
875
|
+
return result;
|
|
876
|
+
}
|
|
877
|
+
function cameraReadPacketData(bs, conn) {
|
|
878
|
+
const result = {};
|
|
879
|
+
// Camera::readPacketData (FUN_005cc530 / FUN_005cc650):
|
|
880
|
+
// Parent::readPacketData + absolute camera transform basis + mode-specific orbit payload.
|
|
881
|
+
result.energyLevel = bs.readF32();
|
|
882
|
+
result.rechargeRate = bs.readF32();
|
|
883
|
+
const pos = {
|
|
884
|
+
x: bs.readF32(),
|
|
885
|
+
y: bs.readF32(),
|
|
886
|
+
z: bs.readF32(),
|
|
887
|
+
};
|
|
888
|
+
result.position = pos;
|
|
889
|
+
result.rotX = bs.readF32();
|
|
890
|
+
result.rotZ = bs.readF32();
|
|
891
|
+
// writeRangedU32(mode, 0, 4) -> 3 bits.
|
|
892
|
+
const cameraMode = bs.readInt(3);
|
|
893
|
+
result.cameraMode = cameraMode;
|
|
894
|
+
// Modes 3/4 include orbit-distance payload.
|
|
895
|
+
if (cameraMode === 3 || cameraMode === 4) {
|
|
896
|
+
result.minOrbitDist = bs.readF32();
|
|
897
|
+
result.maxOrbitDist = bs.readF32();
|
|
898
|
+
result.curOrbitDist = bs.readF32();
|
|
899
|
+
// OrbitObjectMode: observing flag + always-read 10-bit target ghost index.
|
|
900
|
+
if (cameraMode === 3) {
|
|
901
|
+
result.observingClientObject = bs.readFlag();
|
|
902
|
+
result.orbitObjectGhostIndex = bs.readInt(10);
|
|
903
|
+
}
|
|
904
|
+
// OrbitPointMode: NetConnection::readCompressed using current compression point.
|
|
905
|
+
if (cameraMode === 4) {
|
|
906
|
+
result.orbitPoint = bs.readCompressedPoint(conn.compressionPoint, 0.01);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// The camera packet updates the connection compression point to absolute camera pos.
|
|
910
|
+
conn.compressionPoint = pos;
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
// ============================================================
|
|
914
|
+
// LinearProjectile ghost parser
|
|
915
|
+
// ============================================================
|
|
916
|
+
function linearProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
917
|
+
// Parent: LinearProjectile → Projectile → GameBase
|
|
918
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
919
|
+
if (bs.readFlag()) {
|
|
920
|
+
// InitialUpdateMask
|
|
921
|
+
if (bs.readFlag()) {
|
|
922
|
+
// Hidden/already exploded (mHidden=true): initial create of finished projectile
|
|
923
|
+
// Binary: sets mHidden=1, reads pos+direction, computes endpoint, reads decal flag
|
|
924
|
+
result.hidden = true;
|
|
925
|
+
result.explodePosition = bs.readCompressedPoint(conn.compressionPoint);
|
|
926
|
+
result.explodeNormal = bs.readNormalVector(14); // binary: 0xe
|
|
927
|
+
result.endedWithDecal = bs.readFlag();
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
// Live projectile initial update
|
|
931
|
+
result.position = bs.readCompressedPoint(conn.compressionPoint);
|
|
932
|
+
result.direction = bs.readNormalVector(14); // binary: 0xe
|
|
933
|
+
result.currTick = bs.readRangedU32(0, 511); // nextPow2(0x200)=9 bits
|
|
934
|
+
if (bs.readFlag()) {
|
|
935
|
+
result.sourceObject = bs.readInt(10); // nextPow2(0x400)=10 bits
|
|
936
|
+
result.sourceSlot = bs.readRangedU32(0, 7); // nextPow2(8)=3 bits
|
|
937
|
+
if (bs.readFlag()) {
|
|
938
|
+
result.excessVel = bs.readRangedU32(0, 255); // nextPow2(0x100)=8 bits
|
|
939
|
+
result.excessDir = bs.readNormalVector(7);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (bs.readFlag()) {
|
|
943
|
+
result.vehicleObject = bs.readInt(10); // nextPow2(0x400)=10 bits
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
// Non-initial: explosion notification
|
|
949
|
+
result.explodePosition = bs.readCompressedPoint(conn.compressionPoint);
|
|
950
|
+
result.explodeNormal = bs.readNormalVector(14); // binary: 0xe
|
|
951
|
+
result.endedWithDecal = bs.readFlag();
|
|
952
|
+
}
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
// ============================================================
|
|
956
|
+
// ELFProjectile ghost parser
|
|
957
|
+
// ============================================================
|
|
958
|
+
function elfProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
959
|
+
// Parent: ELFProjectile → GameBase
|
|
960
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
961
|
+
if (bs.readFlag()) {
|
|
962
|
+
if (bs.readFlag()) {
|
|
963
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
964
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
965
|
+
result.targetObject = bs.readRangedU32(0, 1024);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
// ============================================================
|
|
971
|
+
// RepairProjectile ghost parser
|
|
972
|
+
// ============================================================
|
|
973
|
+
function repairProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
974
|
+
// Parent: RepairProjectile → GameBase
|
|
975
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
976
|
+
if (bs.readFlag()) {
|
|
977
|
+
// InitialUpdateMask
|
|
978
|
+
if (bs.readFlag()) {
|
|
979
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
980
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
981
|
+
result.repairingObject = bs.readRangedU32(0, 1024);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Non-initial update: no data
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
// ============================================================
|
|
988
|
+
// TargetProjectile ghost parser
|
|
989
|
+
// ============================================================
|
|
990
|
+
function targetProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
991
|
+
// Parent: TargetProjectile → Projectile → GameBase
|
|
992
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
993
|
+
if (bs.readFlag()) {
|
|
994
|
+
// InitialUpdateMask
|
|
995
|
+
result.initialPosition = bs.readPoint3F();
|
|
996
|
+
result.endPos = bs.readPoint3F();
|
|
997
|
+
result.truncated = bs.readFlag();
|
|
998
|
+
if (bs.readFlag()) {
|
|
999
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
1000
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
1001
|
+
result.clientOwned = bs.readFlag();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
// Swing update
|
|
1006
|
+
if (bs.readFlag()) {
|
|
1007
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
1008
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
1009
|
+
result.clientOwned = bs.readFlag();
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
result.initialPosition = bs.readPoint3F();
|
|
1013
|
+
}
|
|
1014
|
+
result.endPos = bs.readPoint3F();
|
|
1015
|
+
result.truncated = bs.readFlag();
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
// ============================================================
|
|
1020
|
+
// WayPoint ghost parser
|
|
1021
|
+
// ============================================================
|
|
1022
|
+
function wayPointUnpackUpdate(bs, isInitial, conn) {
|
|
1023
|
+
// WayPoint extends MissionMarker (V12 source: missionMarker.h)
|
|
1024
|
+
const result = missionMarkerUnpackUpdate(bs, isInitial, conn);
|
|
1025
|
+
// WayPoint additions
|
|
1026
|
+
if (bs.readFlag()) {
|
|
1027
|
+
result.name = bs.readString();
|
|
1028
|
+
}
|
|
1029
|
+
if (bs.readFlag()) {
|
|
1030
|
+
result.teamId = bs.readS32();
|
|
1031
|
+
}
|
|
1032
|
+
if (bs.readFlag()) {
|
|
1033
|
+
result.hidden = bs.readFlag();
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
}
|
|
1037
|
+
// ============================================================
|
|
1038
|
+
// SpawnSphere ghost parser
|
|
1039
|
+
// ============================================================
|
|
1040
|
+
function spawnSphereUnpackUpdate(bs, isInitial, conn) {
|
|
1041
|
+
// SpawnSphere extends MissionMarker (V12 source: missionMarker.h)
|
|
1042
|
+
const result = missionMarkerUnpackUpdate(bs, isInitial, conn);
|
|
1043
|
+
// SpawnSphere additions
|
|
1044
|
+
if (bs.readFlag()) {
|
|
1045
|
+
result.radius = bs.readF32();
|
|
1046
|
+
result.sphereWeight = bs.readF32();
|
|
1047
|
+
result.indoorWeight = bs.readF32();
|
|
1048
|
+
result.outdoorWeight = bs.readF32();
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
// ============================================================
|
|
1053
|
+
// ForceFieldBare ghost parser
|
|
1054
|
+
// ============================================================
|
|
1055
|
+
function forceFieldBareUnpackUpdate(bs, _isInitial, _conn) {
|
|
1056
|
+
// ForceFieldBare::unpackUpdate (FUN_00676d30):
|
|
1057
|
+
// GameBase parent, then two-level transform flags, then StateChangeMask.
|
|
1058
|
+
const result = readGameBaseUpdate(bs, _isInitial, _conn);
|
|
1059
|
+
// InitialUpdateMask flag — if true, reads transform+scale (initial path)
|
|
1060
|
+
if (bs.readFlag()) {
|
|
1061
|
+
result.transform = bs.readAffineTransform();
|
|
1062
|
+
result.scale = bs.readPoint3F();
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
// Non-initial: TransformMask flag
|
|
1066
|
+
if (bs.readFlag()) {
|
|
1067
|
+
result.transform = bs.readAffineTransform();
|
|
1068
|
+
result.scale = bs.readPoint3F();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// StateChangeMask
|
|
1072
|
+
if (bs.readFlag()) {
|
|
1073
|
+
const state = bs.readInt(2); // 0=Open, 1=Opening, 2=Closing, 3=Closed
|
|
1074
|
+
result.state = state;
|
|
1075
|
+
if (state === 3) {
|
|
1076
|
+
// Closed: position = 0 (not read from stream)
|
|
1077
|
+
result.position = 0;
|
|
1078
|
+
}
|
|
1079
|
+
else if (state === 0) {
|
|
1080
|
+
// Open: position = datablock fadeMS (not read from stream)
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
// Opening (1) or Closing (2): read position from stream
|
|
1084
|
+
result.position = bs.readU32();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
// ============================================================
|
|
1090
|
+
// TSStatic ghost parser
|
|
1091
|
+
// ============================================================
|
|
1092
|
+
function tsStaticUnpackUpdate(bs, _isInitial, _conn) {
|
|
1093
|
+
const result = {};
|
|
1094
|
+
// TSStatic::unpackUpdate (FUN_00690580) has no update-mask flags:
|
|
1095
|
+
// it always serializes MatrixF transform, Point3F scale, and shapeName.
|
|
1096
|
+
result.transform = bs.readMatrixF();
|
|
1097
|
+
result.scale = bs.readPoint3F();
|
|
1098
|
+
result.shapeName = bs.readString();
|
|
1099
|
+
return result;
|
|
1100
|
+
}
|
|
1101
|
+
// ============================================================
|
|
1102
|
+
// TerrainBlock ghost parser
|
|
1103
|
+
// ============================================================
|
|
1104
|
+
function terrainBlockUnpackUpdate(bs, _isInitial, _conn) {
|
|
1105
|
+
// TerrainBlock extends SceneObject (NOT GameBase).
|
|
1106
|
+
// V12 source: terrain/terrData.cc lines 910-941
|
|
1107
|
+
// Format:
|
|
1108
|
+
// flag(InitMask) → U32 CRC + readString(terrFileName)
|
|
1109
|
+
// + readString(detailTextureName) + U32 squareSize
|
|
1110
|
+
// + U32 count + U32[count] emptySquareRuns
|
|
1111
|
+
// else → flag(EmptyMask) → U32 count + U32[count] emptySquareRuns
|
|
1112
|
+
const result = {};
|
|
1113
|
+
if (bs.readFlag()) {
|
|
1114
|
+
// InitMask
|
|
1115
|
+
result.crc = bs.readU32();
|
|
1116
|
+
result.terrFileName = bs.readString();
|
|
1117
|
+
result.detailTextureName = bs.readString();
|
|
1118
|
+
result.squareSize = bs.readU32();
|
|
1119
|
+
const size = bs.readU32();
|
|
1120
|
+
const emptySquareRuns = [];
|
|
1121
|
+
for (let i = 0; i < size; i++) {
|
|
1122
|
+
emptySquareRuns.push(bs.readU32());
|
|
1123
|
+
}
|
|
1124
|
+
result.emptySquareRuns = emptySquareRuns;
|
|
1125
|
+
result.emptySquareRunCount = size;
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// Normal update
|
|
1129
|
+
if (bs.readFlag()) {
|
|
1130
|
+
// EmptyMask
|
|
1131
|
+
const size = bs.readU32();
|
|
1132
|
+
const emptySquareRuns = [];
|
|
1133
|
+
for (let i = 0; i < size; i++) {
|
|
1134
|
+
emptySquareRuns.push(bs.readU32());
|
|
1135
|
+
}
|
|
1136
|
+
result.emptySquareRuns = emptySquareRuns;
|
|
1137
|
+
result.emptySquareRunCount = size;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
// ============================================================
|
|
1143
|
+
// Sun ghost parser
|
|
1144
|
+
// ============================================================
|
|
1145
|
+
function sunUnpackUpdate(bs, _isInitial, _conn) {
|
|
1146
|
+
// Binary FUN_005b1620: TWO separate readFlag blocks.
|
|
1147
|
+
// Block 1: 5 texture/environment map name strings
|
|
1148
|
+
// Block 2: 19 F32 values (direction + color + ambient + extra light properties)
|
|
1149
|
+
const result = {};
|
|
1150
|
+
// Block 1: texture names (5 × readSTString)
|
|
1151
|
+
if (bs.readFlag()) {
|
|
1152
|
+
const textures = [];
|
|
1153
|
+
for (let i = 0; i < 5; i++) {
|
|
1154
|
+
textures.push(bs.readString());
|
|
1155
|
+
}
|
|
1156
|
+
result.textures = textures;
|
|
1157
|
+
}
|
|
1158
|
+
// Block 2: light properties (19 × F32)
|
|
1159
|
+
if (bs.readFlag()) {
|
|
1160
|
+
const values = [];
|
|
1161
|
+
for (let i = 0; i < 19; i++) {
|
|
1162
|
+
values.push(bs.readF32());
|
|
1163
|
+
}
|
|
1164
|
+
result.direction = { x: values[0], y: values[1], z: values[2] };
|
|
1165
|
+
result.color = { r: values[3], g: values[4], b: values[5], a: values[6] };
|
|
1166
|
+
result.ambient = { r: values[7], g: values[8], b: values[9], a: values[10] };
|
|
1167
|
+
// Remaining 8 values are additional light properties in build 25034
|
|
1168
|
+
result.extraLightProps = values.slice(11);
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
// ============================================================
|
|
1173
|
+
// Sky ghost parser
|
|
1174
|
+
// ============================================================
|
|
1175
|
+
function skyUnpackUpdate(bs, _isInitial, _conn) {
|
|
1176
|
+
const result = {};
|
|
1177
|
+
// Decompiled from FUN_005ab580 (Sky::unpackUpdate) / FUN_005abfd0 (packUpdate).
|
|
1178
|
+
// This layout is significantly larger than Torque-era Sky examples and must
|
|
1179
|
+
// stay aligned with the retail Tribes 2 binary.
|
|
1180
|
+
// InitialUpdateMask
|
|
1181
|
+
if (bs.readFlag()) {
|
|
1182
|
+
result.materialList = bs.readString();
|
|
1183
|
+
result.fogColor = { r: bs.readF32(), g: bs.readF32(), b: bs.readF32() };
|
|
1184
|
+
const fogVolumeCount = bs.readU32();
|
|
1185
|
+
if (fogVolumeCount > 64) {
|
|
1186
|
+
throw new Error(`Invalid Sky fogVolumeCount: ${fogVolumeCount}`);
|
|
1187
|
+
}
|
|
1188
|
+
result.fogVolumeCount = fogVolumeCount;
|
|
1189
|
+
// bool fields are read via Stream::read(1) in the binary (byte-sized bool)
|
|
1190
|
+
result.useSkyTextures = bs.readBool();
|
|
1191
|
+
result.renderBottomTexture = bs.readBool();
|
|
1192
|
+
result.skySolidColor = { r: bs.readF32(), g: bs.readF32(), b: bs.readF32() };
|
|
1193
|
+
result.windEffectPrecipitation = bs.readBool();
|
|
1194
|
+
const fogVolumes = [];
|
|
1195
|
+
for (let i = 0; i < fogVolumeCount; i++) {
|
|
1196
|
+
// V12 sky.cc writes: visibleDistance, minHeight, maxHeight, color
|
|
1197
|
+
fogVolumes.push({
|
|
1198
|
+
visibleDistance: bs.readF32(),
|
|
1199
|
+
minHeight: bs.readF32(),
|
|
1200
|
+
maxHeight: bs.readF32(),
|
|
1201
|
+
color: { r: bs.readF32(), g: bs.readF32(), b: bs.readF32() },
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
result.fogVolumes = fogVolumes;
|
|
1205
|
+
const cloudLayers = [];
|
|
1206
|
+
for (let i = 0; i < 3; i++) {
|
|
1207
|
+
cloudLayers.push({
|
|
1208
|
+
texture: bs.readString(),
|
|
1209
|
+
heightPercent: bs.readF32(),
|
|
1210
|
+
speed: bs.readF32(),
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
result.cloudLayers = cloudLayers;
|
|
1214
|
+
// Initial wind velocity vector
|
|
1215
|
+
result.windVelocity = bs.readPoint3F();
|
|
1216
|
+
result.stormCurrent = bs.readF32();
|
|
1217
|
+
if (bs.readFlag()) {
|
|
1218
|
+
result.stormInit = {
|
|
1219
|
+
startPct: bs.readF32(),
|
|
1220
|
+
duration: bs.readF32(),
|
|
1221
|
+
indexOrMode: bs.readF32(),
|
|
1222
|
+
startTime: bs.readF32(),
|
|
1223
|
+
targetPct: bs.readF32(),
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// StormCloudsShow mask
|
|
1228
|
+
if (bs.readFlag()) {
|
|
1229
|
+
result.stormCloudsOn = bs.readBool();
|
|
1230
|
+
}
|
|
1231
|
+
// StormFogShow mask
|
|
1232
|
+
if (bs.readFlag()) {
|
|
1233
|
+
result.stormFogOn = bs.readBool();
|
|
1234
|
+
}
|
|
1235
|
+
// Distance mask (visible/fog distances)
|
|
1236
|
+
if (bs.readFlag()) {
|
|
1237
|
+
result.visibleDistance = bs.readF32();
|
|
1238
|
+
result.fogDistance = bs.readF32();
|
|
1239
|
+
}
|
|
1240
|
+
// Storm/falloff mask
|
|
1241
|
+
if (bs.readFlag()) {
|
|
1242
|
+
result.stormType = bs.readF32();
|
|
1243
|
+
result.stormMagnitude = bs.readF32();
|
|
1244
|
+
}
|
|
1245
|
+
// Storm timeline mask
|
|
1246
|
+
if (bs.readFlag()) {
|
|
1247
|
+
result.stormTimeline = {
|
|
1248
|
+
startPct: bs.readF32(),
|
|
1249
|
+
duration: bs.readF32(),
|
|
1250
|
+
indexOrMode: bs.readF32(),
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
// Storm cloud profile mask
|
|
1254
|
+
if (bs.readFlag()) {
|
|
1255
|
+
result.stormCloudProfile = {
|
|
1256
|
+
enabled: bs.readF32(),
|
|
1257
|
+
value0: bs.readF32(),
|
|
1258
|
+
value1: bs.readF32(),
|
|
1259
|
+
value2: bs.readF32(),
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
// Wind velocity update mask
|
|
1263
|
+
if (bs.readFlag()) {
|
|
1264
|
+
result.windVelocity = bs.readPoint3F();
|
|
1265
|
+
}
|
|
1266
|
+
return result;
|
|
1267
|
+
}
|
|
1268
|
+
// ============================================================
|
|
1269
|
+
// Lightning ghost parser (GameBase subclass)
|
|
1270
|
+
// ============================================================
|
|
1271
|
+
function lightningUnpackUpdate(bs, isInitial, conn) {
|
|
1272
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1273
|
+
if (bs.readFlag()) {
|
|
1274
|
+
// InitialUpdateMask — only sent on create
|
|
1275
|
+
result.position = bs.readPoint3F();
|
|
1276
|
+
result.scale = bs.readPoint3F();
|
|
1277
|
+
result.strikeWidth = bs.readF32();
|
|
1278
|
+
result.chanceToHitTarget = bs.readF32();
|
|
1279
|
+
result.strikeRadius = bs.readF32();
|
|
1280
|
+
result.boltStartRadius = bs.readF32();
|
|
1281
|
+
result.color = { r: bs.readF32(), g: bs.readF32(), b: bs.readF32() };
|
|
1282
|
+
result.fadeColor = { r: bs.readF32(), g: bs.readF32(), b: bs.readF32() };
|
|
1283
|
+
result.useFog = bs.readInt(8) !== 0; // bool read as U8
|
|
1284
|
+
result.strikesPerMinute = bs.readF32();
|
|
1285
|
+
}
|
|
1286
|
+
return result;
|
|
1287
|
+
}
|
|
1288
|
+
// ============================================================
|
|
1289
|
+
// WaterBlock ghost parser (SceneObject subclass, NO GameBase)
|
|
1290
|
+
// ============================================================
|
|
1291
|
+
const WC_NUM_SUBMERGE_TEX = 2;
|
|
1292
|
+
function waterBlockUnpackUpdate(bs, _isInitial, _conn) {
|
|
1293
|
+
const result = {};
|
|
1294
|
+
// No flag checks — always sends everything
|
|
1295
|
+
result.transform = bs.readAffineTransform();
|
|
1296
|
+
result.scale = bs.readPoint3F();
|
|
1297
|
+
result.surfaceName = bs.readString();
|
|
1298
|
+
result.envMapName = bs.readString();
|
|
1299
|
+
const submergeNames = [];
|
|
1300
|
+
for (let i = 0; i < WC_NUM_SUBMERGE_TEX; i++) {
|
|
1301
|
+
submergeNames.push(bs.readString());
|
|
1302
|
+
}
|
|
1303
|
+
result.submergeNames = submergeNames;
|
|
1304
|
+
result.liquidType = bs.readS32();
|
|
1305
|
+
result.density = bs.readF32();
|
|
1306
|
+
result.viscosity = bs.readF32();
|
|
1307
|
+
result.waveMagnitude = bs.readF32();
|
|
1308
|
+
result.surfaceOpacity = bs.readF32();
|
|
1309
|
+
result.envMapIntensity = bs.readF32();
|
|
1310
|
+
result.removeWetEdges = bs.readInt(8) !== 0; // bool as U8
|
|
1311
|
+
if (bs.readFlag()) {
|
|
1312
|
+
result.audioEnvironmentId = readObjectRef11(bs);
|
|
1313
|
+
}
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1316
|
+
// ============================================================
|
|
1317
|
+
// MissionArea ghost parser (NetObject subclass)
|
|
1318
|
+
// ============================================================
|
|
1319
|
+
function missionAreaUnpackUpdate(bs, _isInitial, _conn) {
|
|
1320
|
+
const result = {};
|
|
1321
|
+
if (bs.readFlag()) {
|
|
1322
|
+
// RectI: point.x, point.y, extent.x, extent.y
|
|
1323
|
+
result.area = {
|
|
1324
|
+
x: bs.readS32(),
|
|
1325
|
+
y: bs.readS32(),
|
|
1326
|
+
w: bs.readS32(),
|
|
1327
|
+
h: bs.readS32(),
|
|
1328
|
+
};
|
|
1329
|
+
result.flightCeiling = bs.readF32();
|
|
1330
|
+
result.flightCeilingRange = bs.readF32();
|
|
1331
|
+
}
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1334
|
+
// ============================================================
|
|
1335
|
+
// Splash ghost parser (GameBase subclass)
|
|
1336
|
+
// ============================================================
|
|
1337
|
+
function splashUnpackUpdate(bs, isInitial, conn) {
|
|
1338
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1339
|
+
if (bs.readFlag()) {
|
|
1340
|
+
result.position = bs.readPoint3F();
|
|
1341
|
+
}
|
|
1342
|
+
return result;
|
|
1343
|
+
}
|
|
1344
|
+
// ============================================================
|
|
1345
|
+
// Shockwave ghost parser (GameBase subclass)
|
|
1346
|
+
// ============================================================
|
|
1347
|
+
function shockwaveUnpackUpdate(bs, isInitial, conn) {
|
|
1348
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1349
|
+
if (bs.readFlag()) {
|
|
1350
|
+
result.position = bs.readPoint3F();
|
|
1351
|
+
result.normal = bs.readPoint3F();
|
|
1352
|
+
}
|
|
1353
|
+
return result;
|
|
1354
|
+
}
|
|
1355
|
+
// ============================================================
|
|
1356
|
+
// FireballAtmosphere ghost parser (GameBase subclass)
|
|
1357
|
+
// ============================================================
|
|
1358
|
+
function fireballAtmosphereUnpackUpdate(bs, isInitial, conn) {
|
|
1359
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1360
|
+
if (bs.readFlag()) {
|
|
1361
|
+
result.dropRadius = bs.readF32();
|
|
1362
|
+
result.dropsPerMinute = bs.readF32();
|
|
1363
|
+
result.maxDropAngle = bs.readF32();
|
|
1364
|
+
result.minDropAngle = bs.readF32();
|
|
1365
|
+
result.startVelocity = bs.readF32();
|
|
1366
|
+
result.dropHeight = bs.readF32();
|
|
1367
|
+
result.dropDir = bs.readPoint3F();
|
|
1368
|
+
}
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
// ============================================================
|
|
1372
|
+
// VehicleBlocker ghost parser (SceneObject subclass, NO GameBase)
|
|
1373
|
+
// ============================================================
|
|
1374
|
+
function vehicleBlockerUnpackUpdate(bs, _isInitial, _conn) {
|
|
1375
|
+
// VehicleBlocker instance vtable is 0x00796ca4. Slot +0x4c resolves to
|
|
1376
|
+
// FUN_005b9d40, which always reads:
|
|
1377
|
+
// 16 x U32 (MatrixF)
|
|
1378
|
+
// 6 x U32 (two Point3F corners)
|
|
1379
|
+
// No flags are present in this payload.
|
|
1380
|
+
const result = {};
|
|
1381
|
+
result.transform = bs.readMatrixF();
|
|
1382
|
+
result.boundsMin = bs.readPoint3F();
|
|
1383
|
+
result.boundsMax = bs.readPoint3F();
|
|
1384
|
+
return result;
|
|
1385
|
+
}
|
|
1386
|
+
// ============================================================
|
|
1387
|
+
// ParticleEmissionDummy ghost parser (GameBase subclass)
|
|
1388
|
+
// ============================================================
|
|
1389
|
+
function particleEmissionDummyUnpackUpdate(bs, isInitial, conn) {
|
|
1390
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1391
|
+
// Always writes transform + scale + optional datablock
|
|
1392
|
+
result.transform = bs.readMatrixF();
|
|
1393
|
+
result.scale = bs.readPoint3F();
|
|
1394
|
+
if (bs.readFlag()) {
|
|
1395
|
+
result.emitterDatablockId = readObjectRef11(bs);
|
|
1396
|
+
}
|
|
1397
|
+
return result;
|
|
1398
|
+
}
|
|
1399
|
+
// ============================================================
|
|
1400
|
+
// Precipitation ghost parser (GameBase subclass)
|
|
1401
|
+
// ============================================================
|
|
1402
|
+
function precipitationUnpackUpdate(bs, isInitial, conn) {
|
|
1403
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1404
|
+
// Mask order from decompiled binary FUN_00681260:
|
|
1405
|
+
// InitMask, StormShowMask, StormMask, PercentageMask
|
|
1406
|
+
// (Note: V12 source has different order; binary is authoritative)
|
|
1407
|
+
// InitMask
|
|
1408
|
+
if (bs.readFlag()) {
|
|
1409
|
+
result.percentage = bs.readF32();
|
|
1410
|
+
const colorCount = bs.readS32();
|
|
1411
|
+
result.colorCount = colorCount;
|
|
1412
|
+
if (colorCount < 0 || colorCount > 3) {
|
|
1413
|
+
throw new Error(`Invalid precipitation colorCount: ${colorCount}`);
|
|
1414
|
+
}
|
|
1415
|
+
// Colors use packed ColorF (FUN_0043f040): 4×U8 = 32 bits each, NOT 4×F32
|
|
1416
|
+
const colors = [];
|
|
1417
|
+
for (let i = 0; i < colorCount; i++) {
|
|
1418
|
+
colors.push(readPackedColorF(bs));
|
|
1419
|
+
}
|
|
1420
|
+
result.colors = colors;
|
|
1421
|
+
result.offsetSpeed = bs.readF32();
|
|
1422
|
+
result.minVelocity = bs.readF32();
|
|
1423
|
+
result.maxVelocity = bs.readF32();
|
|
1424
|
+
result.maxDrops = bs.readS32();
|
|
1425
|
+
result.maxRadius = bs.readF32();
|
|
1426
|
+
// Inner flag: storm initialization data (3 F32s)
|
|
1427
|
+
if (bs.readFlag()) {
|
|
1428
|
+
result.stormLastTime = bs.readF32();
|
|
1429
|
+
result.stormTime = bs.readF32();
|
|
1430
|
+
result.stormEndPercentage = bs.readF32();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// StormShowMask
|
|
1434
|
+
if (bs.readFlag()) {
|
|
1435
|
+
result.stormPrecipitationOn = bs.readBool();
|
|
1436
|
+
}
|
|
1437
|
+
// StormMask
|
|
1438
|
+
if (bs.readFlag()) {
|
|
1439
|
+
result.stormTime = bs.readF32();
|
|
1440
|
+
result.stormEndPercentage = bs.readF32();
|
|
1441
|
+
}
|
|
1442
|
+
// PercentageMask
|
|
1443
|
+
if (bs.readFlag()) {
|
|
1444
|
+
result.percentageUpdate = bs.readF32();
|
|
1445
|
+
}
|
|
1446
|
+
return result;
|
|
1447
|
+
}
|
|
1448
|
+
// ============================================================
|
|
1449
|
+
// SniperProjectile ghost parser
|
|
1450
|
+
// ============================================================
|
|
1451
|
+
function sniperProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
1452
|
+
// Parent: SniperProjectile → LinearProjectile → Projectile → GameBase
|
|
1453
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1454
|
+
if (bs.readFlag()) {
|
|
1455
|
+
// InitialUpdateMask
|
|
1456
|
+
result.energyPercentage = bs.readFloat(7);
|
|
1457
|
+
result.initialPosition = bs.readPoint3F();
|
|
1458
|
+
result.endPos = bs.readPoint3F();
|
|
1459
|
+
result.truncated = bs.readFlag();
|
|
1460
|
+
result.hitWater = bs.readFlag();
|
|
1461
|
+
if (bs.readFlag()) {
|
|
1462
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
1463
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
1464
|
+
result.clientOwned = bs.readFlag();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
// Swing update
|
|
1469
|
+
if (bs.readFlag()) {
|
|
1470
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
1471
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
1472
|
+
result.clientOwned = bs.readFlag();
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
result.initialPosition = bs.readPoint3F();
|
|
1476
|
+
}
|
|
1477
|
+
result.endPos = bs.readPoint3F();
|
|
1478
|
+
result.truncated = bs.readFlag();
|
|
1479
|
+
}
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1482
|
+
// ============================================================
|
|
1483
|
+
// ShockLanceProjectile ghost parser
|
|
1484
|
+
// ============================================================
|
|
1485
|
+
function shockLanceProjectileUnpackUpdate(bs, isInitial, conn) {
|
|
1486
|
+
// Parent: ShockLanceProjectile → Projectile → GameBase
|
|
1487
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1488
|
+
// Target (always written)
|
|
1489
|
+
if (bs.readFlag()) {
|
|
1490
|
+
result.targetObject = bs.readRangedU32(0, 1024);
|
|
1491
|
+
}
|
|
1492
|
+
// Initial update
|
|
1493
|
+
if (bs.readFlag()) {
|
|
1494
|
+
result.start = bs.readPoint3F();
|
|
1495
|
+
result.end = bs.readPoint3F();
|
|
1496
|
+
result.hitObject = bs.readFlag();
|
|
1497
|
+
if (bs.readFlag()) {
|
|
1498
|
+
result.sourceObject = bs.readRangedU32(0, 1024);
|
|
1499
|
+
result.sourceSlot = bs.readRangedU32(0, 7);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return result;
|
|
1503
|
+
}
|
|
1504
|
+
// ============================================================
|
|
1505
|
+
// WheeledVehicle ghost parser
|
|
1506
|
+
// ============================================================
|
|
1507
|
+
/**
|
|
1508
|
+
* Determine wheel count from a WheeledVehicleData shape name.
|
|
1509
|
+
* In standard Tribes 2, the only WheeledVehicle is the MPB/Jericho (6 wheels).
|
|
1510
|
+
* Wildcat and Beowulf are HoverVehicleData, NOT WheeledVehicleData.
|
|
1511
|
+
* wheelCount is computed at runtime from shape ground#/spring# node pairs.
|
|
1512
|
+
* Falls back to 6 (the only standard WheeledVehicle) if shape unknown.
|
|
1513
|
+
*/
|
|
1514
|
+
function getWheelCountFromShape(shapeName) {
|
|
1515
|
+
if (!shapeName)
|
|
1516
|
+
return 6;
|
|
1517
|
+
const lower = shapeName.toLowerCase();
|
|
1518
|
+
// MPB (vehicle_land_mpbase.dts) has 6 wheels: Ground0-5 + Spring0-5
|
|
1519
|
+
if (lower.includes("mpb") || lower.includes("mpbase"))
|
|
1520
|
+
return 6;
|
|
1521
|
+
// Default to 6 for unknown shapes (standard T2 only has the MPB)
|
|
1522
|
+
return 6;
|
|
1523
|
+
}
|
|
1524
|
+
/** Cache of dataBlockId → wheelCount for WheeledVehicle ghosts. */
|
|
1525
|
+
const wheelCountCache = new Map();
|
|
1526
|
+
/** Cache of ghostIndex → wheelCount for WheeledVehicle updates (where dbId is absent). */
|
|
1527
|
+
const ghostWheelCountCache = new Map();
|
|
1528
|
+
function wheeledVehicleUnpackUpdate(bs, isInitial, conn) {
|
|
1529
|
+
const result = vehicleUnpackUpdate(bs, isInitial, conn);
|
|
1530
|
+
// Always read braking/wheels. Binary (FUN_00615a70) checks
|
|
1531
|
+
// param_1[0x2097]==in_ECX (GameConnection.mControlObject == this vehicle)
|
|
1532
|
+
// but in demo recordings mControlObject is always the Player, never
|
|
1533
|
+
// a vehicle, so this condition is always false.
|
|
1534
|
+
{
|
|
1535
|
+
result.braking = bs.readFlag();
|
|
1536
|
+
if (bs.readFlag()) {
|
|
1537
|
+
// Determine wheel count from the DataBlock's shape name.
|
|
1538
|
+
// On creates, dataBlockId is available (DataBlockMask set).
|
|
1539
|
+
// On updates, use ghost-index cache populated during create.
|
|
1540
|
+
let wheelCount = 4;
|
|
1541
|
+
const dbId = result.dataBlockId;
|
|
1542
|
+
const ghostIdx = conn.currentGhostIndex;
|
|
1543
|
+
if (dbId !== undefined) {
|
|
1544
|
+
const cached = wheelCountCache.get(dbId);
|
|
1545
|
+
if (cached !== undefined) {
|
|
1546
|
+
wheelCount = cached;
|
|
1547
|
+
}
|
|
1548
|
+
else if (conn.getDataBlockData) {
|
|
1549
|
+
const dbData = conn.getDataBlockData(dbId);
|
|
1550
|
+
if (dbData) {
|
|
1551
|
+
const shapeName = dbData.shapeName;
|
|
1552
|
+
if (typeof shapeName === "string") {
|
|
1553
|
+
wheelCount = getWheelCountFromShape(shapeName);
|
|
1554
|
+
}
|
|
1555
|
+
wheelCountCache.set(dbId, wheelCount);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// Cache by ghost index for future updates
|
|
1559
|
+
if (ghostIdx !== undefined) {
|
|
1560
|
+
ghostWheelCountCache.set(ghostIdx, wheelCount);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
else if (ghostIdx !== undefined) {
|
|
1564
|
+
// UPDATE: look up by ghost index
|
|
1565
|
+
const cached = ghostWheelCountCache.get(ghostIdx);
|
|
1566
|
+
if (cached !== undefined) {
|
|
1567
|
+
wheelCount = cached;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const wheels = [];
|
|
1571
|
+
for (let i = 0; i < wheelCount; i++) {
|
|
1572
|
+
wheels.push({
|
|
1573
|
+
avel: bs.readF32(),
|
|
1574
|
+
dy: bs.readF32(),
|
|
1575
|
+
dx: bs.readF32(),
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
result.wheels = wheels;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return result;
|
|
1582
|
+
}
|
|
1583
|
+
// ============================================================
|
|
1584
|
+
// Trigger ghost parser
|
|
1585
|
+
// ============================================================
|
|
1586
|
+
function triggerUnpackUpdate(bs, _isInitial, _conn) {
|
|
1587
|
+
// Trigger::unpackUpdate (0x0061bab0):
|
|
1588
|
+
// Parent chain reads 0 bits (confirmed via r2ghidra — the parent call at
|
|
1589
|
+
// 0x005e2840 chains to 0x00436e00 which is empty).
|
|
1590
|
+
// Then reads 4 raw bytes (U32 tickPeriodMS, stored at this+0x44).
|
|
1591
|
+
// The V12 source has a full polyhedron (transform, points, planes, edges)
|
|
1592
|
+
// but the Tribes 2 build 25034 binary strips all of that.
|
|
1593
|
+
return { tickPeriodMS: bs.readU32() };
|
|
1594
|
+
}
|
|
1595
|
+
// ============================================================
|
|
1596
|
+
// PhysicalZone ghost parser (SceneObject subclass)
|
|
1597
|
+
// ============================================================
|
|
1598
|
+
function physicalZoneUnpackUpdate(bs, _isInitial, _conn) {
|
|
1599
|
+
const result = {};
|
|
1600
|
+
if (bs.readFlag()) {
|
|
1601
|
+
// Initial update — raw MatrixF (16×F32) + Point3F scale
|
|
1602
|
+
result.transform = bs.readMatrixF();
|
|
1603
|
+
result.scale = bs.readPoint3F();
|
|
1604
|
+
// Polyhedron points
|
|
1605
|
+
const numPoints = bs.readU32();
|
|
1606
|
+
if (numPoints > Math.floor(bs.getRemainingBits() / 96)) {
|
|
1607
|
+
throw new Error(`Invalid physicalZone point count: ${numPoints}`);
|
|
1608
|
+
}
|
|
1609
|
+
const points = [];
|
|
1610
|
+
for (let i = 0; i < numPoints; i++) {
|
|
1611
|
+
points.push(bs.readPoint3F());
|
|
1612
|
+
}
|
|
1613
|
+
result.points = points;
|
|
1614
|
+
// Polyhedron planes
|
|
1615
|
+
const numPlanes = bs.readU32();
|
|
1616
|
+
if (numPlanes > Math.floor(bs.getRemainingBits() / 128)) {
|
|
1617
|
+
throw new Error(`Invalid physicalZone plane count: ${numPlanes}`);
|
|
1618
|
+
}
|
|
1619
|
+
const planes = [];
|
|
1620
|
+
for (let i = 0; i < numPlanes; i++) {
|
|
1621
|
+
planes.push({
|
|
1622
|
+
x: bs.readF32(),
|
|
1623
|
+
y: bs.readF32(),
|
|
1624
|
+
z: bs.readF32(),
|
|
1625
|
+
d: bs.readF32(),
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
result.planes = planes;
|
|
1629
|
+
// Polyhedron edges
|
|
1630
|
+
const numEdges = bs.readU32();
|
|
1631
|
+
if (numEdges > Math.floor(bs.getRemainingBits() / 128)) {
|
|
1632
|
+
throw new Error(`Invalid physicalZone edge count: ${numEdges}`);
|
|
1633
|
+
}
|
|
1634
|
+
const edges = [];
|
|
1635
|
+
for (let i = 0; i < numEdges; i++) {
|
|
1636
|
+
edges.push({
|
|
1637
|
+
face0: bs.readU32(),
|
|
1638
|
+
face1: bs.readU32(),
|
|
1639
|
+
vertex0: bs.readU32(),
|
|
1640
|
+
vertex1: bs.readU32(),
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
result.edges = edges;
|
|
1644
|
+
result.velocityMod = bs.readF32();
|
|
1645
|
+
result.gravityMod = bs.readF32();
|
|
1646
|
+
result.appliedForce = bs.readPoint3F();
|
|
1647
|
+
result.active = bs.readFlag();
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
// Non-initial update: just active flag
|
|
1651
|
+
result.active = bs.readFlag();
|
|
1652
|
+
}
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
// ============================================================
|
|
1656
|
+
// AudioEmitter ghost parser (SceneObject subclass)
|
|
1657
|
+
// ============================================================
|
|
1658
|
+
function audioEmitterUnpackUpdate(bs, _isInitial, _conn) {
|
|
1659
|
+
const result = {};
|
|
1660
|
+
// Initial update flag
|
|
1661
|
+
result.initialUpdate = bs.readFlag();
|
|
1662
|
+
// Transform
|
|
1663
|
+
if (bs.readFlag()) {
|
|
1664
|
+
result.transform = bs.readAffineTransform();
|
|
1665
|
+
}
|
|
1666
|
+
// Profile
|
|
1667
|
+
if (bs.readFlag()) {
|
|
1668
|
+
if (bs.readFlag()) {
|
|
1669
|
+
result.audioProfileId = readObjectRef11(bs);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// Description
|
|
1673
|
+
if (bs.readFlag()) {
|
|
1674
|
+
if (bs.readFlag()) {
|
|
1675
|
+
result.audioDescriptionId = readObjectRef11(bs);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
// Filename
|
|
1679
|
+
if (bs.readFlag()) {
|
|
1680
|
+
result.filename = bs.readString();
|
|
1681
|
+
}
|
|
1682
|
+
// UseProfileDescription
|
|
1683
|
+
if (bs.readFlag()) {
|
|
1684
|
+
result.useProfileDescription = bs.readFlag();
|
|
1685
|
+
}
|
|
1686
|
+
// Volume
|
|
1687
|
+
if (bs.readFlag()) {
|
|
1688
|
+
result.volume = bs.readF32();
|
|
1689
|
+
}
|
|
1690
|
+
// IsLooping
|
|
1691
|
+
if (bs.readFlag()) {
|
|
1692
|
+
result.isLooping = bs.readFlag();
|
|
1693
|
+
}
|
|
1694
|
+
// Is3D
|
|
1695
|
+
if (bs.readFlag()) {
|
|
1696
|
+
result.is3D = bs.readFlag();
|
|
1697
|
+
}
|
|
1698
|
+
// MinDistance
|
|
1699
|
+
if (bs.readFlag()) {
|
|
1700
|
+
result.minDistance = bs.readF32();
|
|
1701
|
+
}
|
|
1702
|
+
// MaxDistance
|
|
1703
|
+
if (bs.readFlag()) {
|
|
1704
|
+
result.maxDistance = bs.readF32();
|
|
1705
|
+
}
|
|
1706
|
+
// ConeInsideAngle (S32 in engine — field type 1)
|
|
1707
|
+
if (bs.readFlag()) {
|
|
1708
|
+
result.coneInsideAngle = bs.readS32();
|
|
1709
|
+
}
|
|
1710
|
+
// ConeOutsideAngle (S32 in engine — field type 1)
|
|
1711
|
+
if (bs.readFlag()) {
|
|
1712
|
+
result.coneOutsideAngle = bs.readS32();
|
|
1713
|
+
}
|
|
1714
|
+
// ConeOutsideVolume
|
|
1715
|
+
if (bs.readFlag()) {
|
|
1716
|
+
result.coneOutsideVolume = bs.readF32();
|
|
1717
|
+
}
|
|
1718
|
+
// ConeVector
|
|
1719
|
+
if (bs.readFlag()) {
|
|
1720
|
+
result.coneVector = bs.readPoint3F();
|
|
1721
|
+
}
|
|
1722
|
+
// LoopCount (S32 in engine — field type 1)
|
|
1723
|
+
if (bs.readFlag()) {
|
|
1724
|
+
result.loopCount = bs.readS32();
|
|
1725
|
+
}
|
|
1726
|
+
// MinLoopGap (S32 in engine — field type 1, milliseconds)
|
|
1727
|
+
if (bs.readFlag()) {
|
|
1728
|
+
result.minLoopGap = bs.readS32();
|
|
1729
|
+
}
|
|
1730
|
+
// MaxLoopGap (S32 in engine — field type 1, milliseconds)
|
|
1731
|
+
if (bs.readFlag()) {
|
|
1732
|
+
result.maxLoopGap = bs.readS32();
|
|
1733
|
+
}
|
|
1734
|
+
// AudioType (S32/enum in engine — field type 9, written as 4 raw bytes)
|
|
1735
|
+
if (bs.readFlag()) {
|
|
1736
|
+
result.audioType = bs.readS32();
|
|
1737
|
+
}
|
|
1738
|
+
// OutsideAmbient
|
|
1739
|
+
if (bs.readFlag()) {
|
|
1740
|
+
result.outsideAmbient = bs.readFlag();
|
|
1741
|
+
}
|
|
1742
|
+
return result;
|
|
1743
|
+
}
|
|
1744
|
+
// ============================================================
|
|
1745
|
+
// StationFXPersonal ghost parser (GameBase subclass)
|
|
1746
|
+
// ============================================================
|
|
1747
|
+
function stationFXPersonalUnpackUpdate(bs, isInitial, conn) {
|
|
1748
|
+
const result = readGameBaseUpdate(bs, isInitial, conn);
|
|
1749
|
+
if (bs.readFlag()) {
|
|
1750
|
+
// InitialUpdateMask
|
|
1751
|
+
if (bs.readFlag()) {
|
|
1752
|
+
result.stationObject = bs.readRangedU32(0, 1024);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return result;
|
|
1756
|
+
}
|
|
1757
|
+
// ============================================================
|
|
1758
|
+
// AIObjective ghost parser (AIObjective::unpackUpdate @ FUN_0047dae0)
|
|
1759
|
+
// ============================================================
|
|
1760
|
+
function aiObjectiveUnpackUpdate(bs, isInitial, conn) {
|
|
1761
|
+
// AIObjective::unpackUpdate calls FUN_0066a8b0 (ShapeBase-derived),
|
|
1762
|
+
// then reads one additional flag.
|
|
1763
|
+
const result = readShapeBaseUpdate(bs, isInitial, conn);
|
|
1764
|
+
if (bs.readFlag()) {
|
|
1765
|
+
result.transform = bs.readAffineTransform();
|
|
1766
|
+
result.scale = bs.readPoint3F();
|
|
1767
|
+
}
|
|
1768
|
+
result.unknownFlag = bs.readFlag();
|
|
1769
|
+
return result;
|
|
1770
|
+
}
|
|
1771
|
+
// ============================================================
|
|
1772
|
+
// Register all ghost parsers into the registry
|
|
1773
|
+
// ============================================================
|
|
1774
|
+
export function registerGhostParsers(registry) {
|
|
1775
|
+
registry.catalogGhost({
|
|
1776
|
+
name: "AIObjective",
|
|
1777
|
+
unpackUpdate: aiObjectiveUnpackUpdate,
|
|
1778
|
+
});
|
|
1779
|
+
registry.catalogGhost({
|
|
1780
|
+
name: "BeaconObject",
|
|
1781
|
+
unpackUpdate: beaconObjectUnpackUpdate,
|
|
1782
|
+
});
|
|
1783
|
+
registry.catalogGhost({
|
|
1784
|
+
name: "BombProjectile",
|
|
1785
|
+
unpackUpdate: bombProjectileUnpackUpdate,
|
|
1786
|
+
});
|
|
1787
|
+
registry.catalogGhost({
|
|
1788
|
+
name: "Player",
|
|
1789
|
+
unpackUpdate: playerUnpackUpdate,
|
|
1790
|
+
readPacketData: playerReadPacketData,
|
|
1791
|
+
});
|
|
1792
|
+
registry.catalogGhost({
|
|
1793
|
+
name: "Debris",
|
|
1794
|
+
unpackUpdate: debrisUnpackUpdate,
|
|
1795
|
+
});
|
|
1796
|
+
registry.catalogGhost({
|
|
1797
|
+
name: "GameBase",
|
|
1798
|
+
unpackUpdate: readGameBaseUpdate,
|
|
1799
|
+
});
|
|
1800
|
+
registry.catalogGhost({
|
|
1801
|
+
name: "ShapeBase",
|
|
1802
|
+
unpackUpdate: readShapeBaseUpdate,
|
|
1803
|
+
readPacketData: vehicleReadPacketData,
|
|
1804
|
+
});
|
|
1805
|
+
registry.catalogGhost({
|
|
1806
|
+
name: "Vehicle",
|
|
1807
|
+
unpackUpdate: vehicleUnpackUpdate,
|
|
1808
|
+
readPacketData: vehicleReadPacketData,
|
|
1809
|
+
});
|
|
1810
|
+
registry.catalogGhost({
|
|
1811
|
+
name: "FlyingVehicle",
|
|
1812
|
+
unpackUpdate: flyingVehicleUnpackUpdate,
|
|
1813
|
+
readPacketData: vehicleReadPacketData,
|
|
1814
|
+
});
|
|
1815
|
+
registry.catalogGhost({
|
|
1816
|
+
name: "HoverVehicle",
|
|
1817
|
+
unpackUpdate: hoverVehicleUnpackUpdate,
|
|
1818
|
+
readPacketData: vehicleReadPacketData,
|
|
1819
|
+
});
|
|
1820
|
+
registry.catalogGhost({
|
|
1821
|
+
name: "Item",
|
|
1822
|
+
unpackUpdate: itemUnpackUpdate,
|
|
1823
|
+
});
|
|
1824
|
+
registry.catalogGhost({
|
|
1825
|
+
name: "Marker",
|
|
1826
|
+
unpackUpdate: markerUnpackUpdate,
|
|
1827
|
+
});
|
|
1828
|
+
registry.catalogGhost({
|
|
1829
|
+
name: "MissionMarker",
|
|
1830
|
+
unpackUpdate: missionMarkerUnpackUpdate,
|
|
1831
|
+
});
|
|
1832
|
+
registry.catalogGhost({
|
|
1833
|
+
name: "StaticShape",
|
|
1834
|
+
unpackUpdate: staticShapeUnpackUpdate,
|
|
1835
|
+
});
|
|
1836
|
+
registry.catalogGhost({
|
|
1837
|
+
name: "Projectile",
|
|
1838
|
+
unpackUpdate: projectileUnpackUpdate,
|
|
1839
|
+
});
|
|
1840
|
+
registry.catalogGhost({
|
|
1841
|
+
name: "ScopeAlwaysShape",
|
|
1842
|
+
unpackUpdate: scopeAlwaysShapeUnpackUpdate,
|
|
1843
|
+
});
|
|
1844
|
+
registry.catalogGhost({
|
|
1845
|
+
name: "GrenadeProjectile",
|
|
1846
|
+
unpackUpdate: grenadeUnpackUpdate,
|
|
1847
|
+
});
|
|
1848
|
+
registry.catalogGhost({
|
|
1849
|
+
name: "SimpleNetObject",
|
|
1850
|
+
unpackUpdate: simpleNetObjectUnpackUpdate,
|
|
1851
|
+
});
|
|
1852
|
+
registry.catalogGhost({
|
|
1853
|
+
name: "SeekerProjectile",
|
|
1854
|
+
unpackUpdate: seekerUnpackUpdate,
|
|
1855
|
+
});
|
|
1856
|
+
registry.catalogGhost({
|
|
1857
|
+
name: "Turret",
|
|
1858
|
+
unpackUpdate: turretUnpackUpdate,
|
|
1859
|
+
});
|
|
1860
|
+
registry.catalogGhost({
|
|
1861
|
+
name: "InteriorInstance",
|
|
1862
|
+
unpackUpdate: interiorUnpackUpdate,
|
|
1863
|
+
});
|
|
1864
|
+
registry.catalogGhost({
|
|
1865
|
+
name: "Camera",
|
|
1866
|
+
unpackUpdate: cameraUnpackUpdate,
|
|
1867
|
+
readPacketData: cameraReadPacketData,
|
|
1868
|
+
});
|
|
1869
|
+
registry.catalogGhost({
|
|
1870
|
+
name: "LinearProjectile",
|
|
1871
|
+
unpackUpdate: linearProjectileUnpackUpdate,
|
|
1872
|
+
});
|
|
1873
|
+
registry.catalogGhost({
|
|
1874
|
+
name: "ELFProjectile",
|
|
1875
|
+
unpackUpdate: elfProjectileUnpackUpdate,
|
|
1876
|
+
});
|
|
1877
|
+
registry.catalogGhost({
|
|
1878
|
+
name: "RepairProjectile",
|
|
1879
|
+
unpackUpdate: repairProjectileUnpackUpdate,
|
|
1880
|
+
});
|
|
1881
|
+
registry.catalogGhost({
|
|
1882
|
+
name: "TargetProjectile",
|
|
1883
|
+
unpackUpdate: targetProjectileUnpackUpdate,
|
|
1884
|
+
});
|
|
1885
|
+
// TracerProjectile resolves through LinearProjectile vtables in the retail binary.
|
|
1886
|
+
registry.catalogGhost({
|
|
1887
|
+
name: "TracerProjectile",
|
|
1888
|
+
unpackUpdate: linearProjectileUnpackUpdate,
|
|
1889
|
+
});
|
|
1890
|
+
registry.catalogGhost({
|
|
1891
|
+
name: "WayPoint",
|
|
1892
|
+
unpackUpdate: wayPointUnpackUpdate,
|
|
1893
|
+
});
|
|
1894
|
+
registry.catalogGhost({
|
|
1895
|
+
name: "SpawnSphere",
|
|
1896
|
+
unpackUpdate: spawnSphereUnpackUpdate,
|
|
1897
|
+
});
|
|
1898
|
+
registry.catalogGhost({
|
|
1899
|
+
name: "ForceFieldBare",
|
|
1900
|
+
unpackUpdate: forceFieldBareUnpackUpdate,
|
|
1901
|
+
});
|
|
1902
|
+
registry.catalogGhost({
|
|
1903
|
+
name: "TSStatic",
|
|
1904
|
+
unpackUpdate: tsStaticUnpackUpdate,
|
|
1905
|
+
});
|
|
1906
|
+
registry.catalogGhost({
|
|
1907
|
+
name: "TerrainBlock",
|
|
1908
|
+
unpackUpdate: terrainBlockUnpackUpdate,
|
|
1909
|
+
});
|
|
1910
|
+
registry.catalogGhost({
|
|
1911
|
+
name: "Sun",
|
|
1912
|
+
unpackUpdate: sunUnpackUpdate,
|
|
1913
|
+
});
|
|
1914
|
+
registry.catalogGhost({
|
|
1915
|
+
name: "Sky",
|
|
1916
|
+
unpackUpdate: skyUnpackUpdate,
|
|
1917
|
+
});
|
|
1918
|
+
registry.catalogGhost({
|
|
1919
|
+
name: "Lightning",
|
|
1920
|
+
unpackUpdate: lightningUnpackUpdate,
|
|
1921
|
+
});
|
|
1922
|
+
registry.catalogGhost({
|
|
1923
|
+
name: "WaterBlock",
|
|
1924
|
+
unpackUpdate: waterBlockUnpackUpdate,
|
|
1925
|
+
});
|
|
1926
|
+
registry.catalogGhost({
|
|
1927
|
+
name: "MissionArea",
|
|
1928
|
+
unpackUpdate: missionAreaUnpackUpdate,
|
|
1929
|
+
});
|
|
1930
|
+
registry.catalogGhost({
|
|
1931
|
+
name: "Splash",
|
|
1932
|
+
unpackUpdate: splashUnpackUpdate,
|
|
1933
|
+
});
|
|
1934
|
+
registry.catalogGhost({
|
|
1935
|
+
name: "Shockwave",
|
|
1936
|
+
unpackUpdate: shockwaveUnpackUpdate,
|
|
1937
|
+
});
|
|
1938
|
+
registry.catalogGhost({
|
|
1939
|
+
name: "FireballAtmosphere",
|
|
1940
|
+
unpackUpdate: fireballAtmosphereUnpackUpdate,
|
|
1941
|
+
});
|
|
1942
|
+
registry.catalogGhost({
|
|
1943
|
+
name: "VehicleBlocker",
|
|
1944
|
+
unpackUpdate: vehicleBlockerUnpackUpdate,
|
|
1945
|
+
});
|
|
1946
|
+
registry.catalogGhost({
|
|
1947
|
+
name: "ParticleEmissionDummy",
|
|
1948
|
+
unpackUpdate: particleEmissionDummyUnpackUpdate,
|
|
1949
|
+
});
|
|
1950
|
+
registry.catalogGhost({
|
|
1951
|
+
name: "Precipitation",
|
|
1952
|
+
unpackUpdate: precipitationUnpackUpdate,
|
|
1953
|
+
});
|
|
1954
|
+
// Projectile aliases — same wire format as GrenadeProjectile
|
|
1955
|
+
registry.catalogGhost({
|
|
1956
|
+
name: "EnergyProjectile",
|
|
1957
|
+
unpackUpdate: grenadeUnpackUpdate,
|
|
1958
|
+
});
|
|
1959
|
+
registry.catalogGhost({
|
|
1960
|
+
name: "FlareProjectile",
|
|
1961
|
+
unpackUpdate: grenadeUnpackUpdate,
|
|
1962
|
+
});
|
|
1963
|
+
registry.catalogGhost({
|
|
1964
|
+
name: "LinearFlareProjectile",
|
|
1965
|
+
unpackUpdate: linearProjectileUnpackUpdate,
|
|
1966
|
+
});
|
|
1967
|
+
registry.catalogGhost({
|
|
1968
|
+
name: "SniperProjectile",
|
|
1969
|
+
unpackUpdate: sniperProjectileUnpackUpdate,
|
|
1970
|
+
});
|
|
1971
|
+
registry.catalogGhost({
|
|
1972
|
+
name: "ShockLanceProjectile",
|
|
1973
|
+
unpackUpdate: shockLanceProjectileUnpackUpdate,
|
|
1974
|
+
});
|
|
1975
|
+
registry.catalogGhost({
|
|
1976
|
+
name: "WheeledVehicle",
|
|
1977
|
+
unpackUpdate: wheeledVehicleUnpackUpdate,
|
|
1978
|
+
readPacketData: wheeledVehicleReadPacketData,
|
|
1979
|
+
});
|
|
1980
|
+
registry.catalogGhost({
|
|
1981
|
+
name: "Trigger",
|
|
1982
|
+
unpackUpdate: triggerUnpackUpdate,
|
|
1983
|
+
});
|
|
1984
|
+
registry.catalogGhost({
|
|
1985
|
+
name: "PhysicalZone",
|
|
1986
|
+
unpackUpdate: physicalZoneUnpackUpdate,
|
|
1987
|
+
});
|
|
1988
|
+
registry.catalogGhost({
|
|
1989
|
+
name: "AudioEmitter",
|
|
1990
|
+
unpackUpdate: audioEmitterUnpackUpdate,
|
|
1991
|
+
});
|
|
1992
|
+
registry.catalogGhost({
|
|
1993
|
+
name: "StationFXPersonal",
|
|
1994
|
+
unpackUpdate: stationFXPersonalUnpackUpdate,
|
|
1995
|
+
});
|
|
1996
|
+
// StationFXVehicle has identical wire format to StationFXPersonal
|
|
1997
|
+
registry.catalogGhost({
|
|
1998
|
+
name: "StationFXVehicle",
|
|
1999
|
+
unpackUpdate: stationFXPersonalUnpackUpdate,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
//# sourceMappingURL=GhostManager.js.map
|