@that-sky-project/that-sky-level 1.0.0 → 1.0.2
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 +636 -479
- package/package.json +24 -3
- package/docs/img/blender_edit_obj_model.png +0 -0
- package/docs/materials.md +0 -37
- package/level_example/HTNH_Test/HTNH_Test.Objects.level.json +0 -258
- package/level_example/HTNH_Test/HTNH_Test.obj +0 -46
- package/level_example/HTNH_Test/Makefile +0 -11
- package/src/formats/triangleMesh.js +0 -82
- package/src/formats/wavefront.js +0 -148
- package/src/level/adjacency.js +0 -466
- package/src/level/enums/kFieldType.js +0 -8
- package/src/level/enums/kMaterial.js +0 -39
- package/src/level/meshes/levelGeo.js +0 -283
- package/src/level/meshes/levelLod.js +0 -15
- package/src/level/meshes/levelMeshes.js +0 -141
- package/src/level/meshes/levelToc.js +0 -69
- package/src/level/objects/levelObjects.js +0 -683
- package/src/level/objects/levelObjectsJson.js +0 -256
- package/src/meshopt/LICENSE +0 -21
- package/src/meshopt/meshopt_decoder.js +0 -199
- package/src/meshopt/meshopt_encoder.js +0 -221
- package/src/utils/binaryStream.js +0 -806
- package/src/utils/helperClasses.js +0 -110
- package/src/utils/logger.js +0 -14
- package/src/utils/normVec.js +0 -67
- package/src/utils/vector.js +0 -232
- package/webpack.config.js +0 -31
|
@@ -1,683 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// ─── Variable type enum ───────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
const VAR_TYPE = Object.freeze({
|
|
6
|
-
NORMAL: 0, // Numeric scalar / vector / matrix; width determined by `size`
|
|
7
|
-
STRING: 1, // Inline null-terminated UTF-8 string
|
|
8
|
-
OBJECT_PTR: 2, // int32LE index into the objects array; -1 = null
|
|
9
|
-
ARRAY: 3 // uint32LE count, then elements (refs or inline sub-objects)
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
/** Read a null-terminated UTF-8 string from `buf` starting at `pos`. */
|
|
15
|
-
function readCString(buf, pos) {
|
|
16
|
-
let end = pos;
|
|
17
|
-
while (end < buf.length && buf[end] !== 0) end++;
|
|
18
|
-
return { value: buf.slice(pos, end).toString('utf8'), bytesRead: end - pos + 1 };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Write a null-terminated UTF-8 string into a new Buffer. */
|
|
22
|
-
function writeCString(str) {
|
|
23
|
-
const encoded = Buffer.from(str == null ? '' : String(str), 'utf8');
|
|
24
|
-
const out = Buffer.allocUnsafe(encoded.length + 1);
|
|
25
|
-
encoded.copy(out);
|
|
26
|
-
out[encoded.length] = 0;
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Return true when a field name conventionally indicates a boolean. */
|
|
31
|
-
function isBoolName(name) {
|
|
32
|
-
return (
|
|
33
|
-
name.startsWith('is') ||
|
|
34
|
-
name.startsWith('has') ||
|
|
35
|
-
name === 'enabled' ||
|
|
36
|
-
name === 'autoStart' ||
|
|
37
|
-
name.includes('Enable') ||
|
|
38
|
-
name.includes('Visible')
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Return true when a 4-byte NORMAL field should be treated as UInt32. */
|
|
43
|
-
function isUInt32Name(name) {
|
|
44
|
-
return name === 'bstGuid' || name.endsWith('Id') || name.endsWith('Index');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Decide whether a numeric JS value was originally stored as Float32 or Int32.
|
|
49
|
-
*
|
|
50
|
-
* The original parser stored a value as Float32 when the Float32 interpretation
|
|
51
|
-
* was "reasonable" (abs in (1e-10, 1e10) or exactly 0), and as Int32 otherwise.
|
|
52
|
-
* On write-back we must produce the same raw bytes.
|
|
53
|
-
*
|
|
54
|
-
* Strategy: write as Float32; check whether reading that Float32 back would
|
|
55
|
-
* reproduce the same JS number. If yes → write float. If no (the stored JS
|
|
56
|
-
* number was an integer that survived only as Int32), write Int32.
|
|
57
|
-
*/
|
|
58
|
-
function encode4ByteNormal(buf, offset, value) {
|
|
59
|
-
const n = typeof value === 'number' ? value : 0;
|
|
60
|
-
|
|
61
|
-
// Write tentatively as Float32 and read back.
|
|
62
|
-
buf.writeFloatLE(n, offset);
|
|
63
|
-
const roundTripped = buf.readFloatLE(offset);
|
|
64
|
-
|
|
65
|
-
// If the float round-trip is exact (within float32 precision), we're done.
|
|
66
|
-
if (roundTripped === n) return;
|
|
67
|
-
|
|
68
|
-
// The value is an integer that cannot be exactly represented as float32 (or
|
|
69
|
-
// is outside the "reasonable float" range). Write as Int32 instead so that
|
|
70
|
-
// the parser's fallback branch picks it up correctly on re-read.
|
|
71
|
-
buf.writeInt32LE(n | 0, offset);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Tgcl ─────────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Reader/writer for the TGCL binary level format.
|
|
78
|
-
*
|
|
79
|
-
* Data model after parsing:
|
|
80
|
-
*
|
|
81
|
-
* tgcl.header – raw header fields
|
|
82
|
-
* tgcl.types[] – type descriptors { index, name, firstMemVar, numMemVars }
|
|
83
|
-
* tgcl.memVars[] – field descriptors { index, varType, name, size, extra }
|
|
84
|
-
* tgcl.objects[] – object instances { _index, _type, _typeIndex, _name,
|
|
85
|
-
* _startPos, _nextPos, ...fields }
|
|
86
|
-
*
|
|
87
|
-
* All field values are plain JS primitives / objects:
|
|
88
|
-
* NORMAL 1B → number | boolean
|
|
89
|
-
* NORMAL 4B → number (UInt32 for *Id/*Index/bstGuid, else Float32/Int32)
|
|
90
|
-
* NORMAL 8B → number (Float64)
|
|
91
|
-
* NORMAL 12B → { x, y, z }
|
|
92
|
-
* NORMAL 16B → { x, y, z, w }
|
|
93
|
-
* NORMAL 64B → { matrix: number[4][4], pos: { x, y, z } }
|
|
94
|
-
* NORMAL else → hex string
|
|
95
|
-
* STRING → string
|
|
96
|
-
* OBJECT_PTR → "@object_N" | null
|
|
97
|
-
* ARRAY (refs)→ Array<"@object_N" | null>
|
|
98
|
-
* ARRAY (typed)→ Array<plain object>
|
|
99
|
-
*/
|
|
100
|
-
class Tgcl {
|
|
101
|
-
constructor() {
|
|
102
|
-
this._reset();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Clear the current state and populate it from a TGCL binary buffer.
|
|
109
|
-
* @param {Buffer} buffer
|
|
110
|
-
* @returns {this}
|
|
111
|
-
*/
|
|
112
|
-
fromFileBuffer(buffer) {
|
|
113
|
-
this._reset();
|
|
114
|
-
this._buf = buffer;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
this._parseHeader();
|
|
118
|
-
this._parseTypes();
|
|
119
|
-
this._parseMemVars();
|
|
120
|
-
this._parseObjects();
|
|
121
|
-
} finally {
|
|
122
|
-
this._buf = null; // release reference
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Serialise the current state to a TGCL binary buffer.
|
|
130
|
-
*
|
|
131
|
-
* Known limitation: ARRAY fields whose element-type index is both unknown
|
|
132
|
-
* (≥ numTypes) and not 0xFFFFFFFF were not decoded during reading; their
|
|
133
|
-
* element data is lost and the written array will have count = 0.
|
|
134
|
-
*
|
|
135
|
-
* @returns {Buffer}
|
|
136
|
-
*/
|
|
137
|
-
toFileBuffer() {
|
|
138
|
-
// ── 1. Data section (interned strings for type/field names) ──────────────
|
|
139
|
-
const { dataBuf, offsetOf } = this._buildDataSection();
|
|
140
|
-
|
|
141
|
-
// ── 2. Compute absolute file offsets ─────────────────────────────────────
|
|
142
|
-
const HEADER_SIZE = 44;
|
|
143
|
-
const typesSize = this.types.length * 12;
|
|
144
|
-
const memVarsSize = this.memVars.length * 16;
|
|
145
|
-
|
|
146
|
-
const typesOffset = HEADER_SIZE;
|
|
147
|
-
const memVarsOffset = typesOffset + typesSize;
|
|
148
|
-
const dataOffset = memVarsOffset + memVarsSize;
|
|
149
|
-
const stringsOffset = dataOffset + dataBuf.length; // objects start here
|
|
150
|
-
|
|
151
|
-
// ── 3. Types table ────────────────────────────────────────────────────────
|
|
152
|
-
const typesBuf = Buffer.allocUnsafe(typesSize);
|
|
153
|
-
for (let i = 0; i < this.types.length; i++) {
|
|
154
|
-
const t = this.types[i];
|
|
155
|
-
const off = i * 12;
|
|
156
|
-
typesBuf.writeUInt32LE(offsetOf(t.name), off);
|
|
157
|
-
typesBuf.writeUInt32LE(t.firstMemVar, off + 4);
|
|
158
|
-
typesBuf.writeUInt32LE(t.numMemVars, off + 8);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── 4. MemVars table ──────────────────────────────────────────────────────
|
|
162
|
-
const memVarsBuf = Buffer.allocUnsafe(memVarsSize);
|
|
163
|
-
for (let i = 0; i < this.memVars.length; i++) {
|
|
164
|
-
const mv = this.memVars[i];
|
|
165
|
-
const off = i * 16;
|
|
166
|
-
memVarsBuf.writeUInt32LE(mv.varType, off);
|
|
167
|
-
memVarsBuf.writeUInt32LE(offsetOf(mv.name), off + 4);
|
|
168
|
-
memVarsBuf.writeUInt32LE(mv.size, off + 8);
|
|
169
|
-
memVarsBuf.writeUInt32LE(mv.extra, off + 12);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── 5. Objects section ────────────────────────────────────────────────────
|
|
173
|
-
const objectParts = this.objects.map(obj => this._serializeObject(obj));
|
|
174
|
-
const objectsBuf = Buffer.concat(objectParts);
|
|
175
|
-
|
|
176
|
-
// ── 6. Header ─────────────────────────────────────────────────────────────
|
|
177
|
-
const fileSize = stringsOffset + objectsBuf.length;
|
|
178
|
-
const headerBuf = Buffer.allocUnsafe(HEADER_SIZE);
|
|
179
|
-
|
|
180
|
-
headerBuf.write('TGCL', 0, 'ascii');
|
|
181
|
-
headerBuf.writeUInt32LE(this.header?.version ?? 1, 4);
|
|
182
|
-
headerBuf.writeUInt32LE(this.types.length, 8);
|
|
183
|
-
headerBuf.writeUInt32LE(this.memVars.length, 12);
|
|
184
|
-
headerBuf.writeUInt32LE(this.objects.length, 16);
|
|
185
|
-
headerBuf.writeUInt32LE(this.header?.numRefs ?? 0, 20);
|
|
186
|
-
headerBuf.writeUInt32LE(typesOffset, 24);
|
|
187
|
-
headerBuf.writeUInt32LE(memVarsOffset, 28);
|
|
188
|
-
headerBuf.writeUInt32LE(dataOffset, 32);
|
|
189
|
-
headerBuf.writeUInt32LE(stringsOffset, 36);
|
|
190
|
-
headerBuf.writeUInt32LE(fileSize, 40);
|
|
191
|
-
|
|
192
|
-
return Buffer.concat([headerBuf, typesBuf, memVarsBuf, dataBuf, objectsBuf]);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Private: reset ─────────────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
_reset() {
|
|
198
|
-
this.header = null;
|
|
199
|
-
this.types = [];
|
|
200
|
-
this.memVars = [];
|
|
201
|
-
this.objects = [];
|
|
202
|
-
this._buf = null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── Private: read helpers ──────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
/** Read a null-terminated string from the data section. */
|
|
208
|
-
_readDataString(relativeOffset) {
|
|
209
|
-
const abs = this.header.dataOffset + relativeOffset;
|
|
210
|
-
const { value } = readCString(this._buf, abs);
|
|
211
|
-
return value;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
_parseHeader() {
|
|
215
|
-
const buf = this._buf;
|
|
216
|
-
const magic = buf.slice(0, 4).toString('ascii');
|
|
217
|
-
if (magic !== 'TGCL') throw new Error(`[Tgcl] Invalid magic: "${magic}", expected "TGCL"`);
|
|
218
|
-
|
|
219
|
-
this.header = {
|
|
220
|
-
magic,
|
|
221
|
-
version: buf.readUInt32LE(4),
|
|
222
|
-
numTypes: buf.readUInt32LE(8),
|
|
223
|
-
numMemVars: buf.readUInt32LE(12),
|
|
224
|
-
numObjects: buf.readUInt32LE(16),
|
|
225
|
-
numRefs: buf.readUInt32LE(20),
|
|
226
|
-
typesOffset: buf.readUInt32LE(24),
|
|
227
|
-
memVarsOffset: buf.readUInt32LE(28),
|
|
228
|
-
dataOffset: buf.readUInt32LE(32),
|
|
229
|
-
stringsOffset: buf.readUInt32LE(36),
|
|
230
|
-
fileSize: buf.readUInt32LE(40)
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
if (this.header.fileSize !== buf.length) {
|
|
234
|
-
console.warn(
|
|
235
|
-
`[Tgcl] File size mismatch: header says ${this.header.fileSize}, ` +
|
|
236
|
-
`buffer is ${buf.length} bytes`
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
_parseTypes() {
|
|
242
|
-
const { typesOffset, numTypes } = this.header;
|
|
243
|
-
const buf = this._buf;
|
|
244
|
-
|
|
245
|
-
for (let i = 0; i < numTypes; i++) {
|
|
246
|
-
const off = typesOffset + i * 12;
|
|
247
|
-
this.types.push({
|
|
248
|
-
index: i,
|
|
249
|
-
name: this._readDataString(buf.readUInt32LE(off)),
|
|
250
|
-
firstMemVar: buf.readUInt32LE(off + 4),
|
|
251
|
-
numMemVars: buf.readUInt32LE(off + 8)
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
_parseMemVars() {
|
|
257
|
-
const { memVarsOffset, numMemVars } = this.header;
|
|
258
|
-
const buf = this._buf;
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < numMemVars; i++) {
|
|
261
|
-
const off = memVarsOffset + i * 16;
|
|
262
|
-
this.memVars.push({
|
|
263
|
-
index: i,
|
|
264
|
-
varType: buf.readUInt32LE(off),
|
|
265
|
-
name: this._readDataString(buf.readUInt32LE(off + 4)),
|
|
266
|
-
size: buf.readUInt32LE(off + 8),
|
|
267
|
-
extra: buf.readUInt32LE(off + 12)
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Return the contiguous MemVar slice that belongs to `typeIndex`. */
|
|
273
|
-
_getTypeMemVars(typeIndex) {
|
|
274
|
-
const type = this.types[typeIndex];
|
|
275
|
-
if (!type) return [];
|
|
276
|
-
const end = type.firstMemVar + type.numMemVars;
|
|
277
|
-
return this.memVars.slice(type.firstMemVar, end);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
_parseObjects() {
|
|
281
|
-
let pos = this.header.stringsOffset;
|
|
282
|
-
const endPos = this.header.fileSize;
|
|
283
|
-
|
|
284
|
-
for (let i = 0; i < this.header.numObjects && pos < endPos; i++) {
|
|
285
|
-
try {
|
|
286
|
-
const obj = this._parseObject(pos, i);
|
|
287
|
-
this.objects.push(obj);
|
|
288
|
-
pos = obj._nextPos;
|
|
289
|
-
} catch (e) {
|
|
290
|
-
console.error(`[Tgcl] Error parsing object ${i} at offset ${pos}:`, e.message);
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
_parseObject(pos, index) {
|
|
297
|
-
const buf = this._buf;
|
|
298
|
-
const startPos = pos;
|
|
299
|
-
|
|
300
|
-
// Type index
|
|
301
|
-
const typeIndex = buf.readUInt32LE(pos); pos += 4;
|
|
302
|
-
|
|
303
|
-
// Object name (inline null-terminated string)
|
|
304
|
-
const { value: name, bytesRead: nameBytes } = readCString(buf, pos);
|
|
305
|
-
pos += nameBytes;
|
|
306
|
-
|
|
307
|
-
const type = this.types[typeIndex];
|
|
308
|
-
const obj = {
|
|
309
|
-
_index: index,
|
|
310
|
-
_type: type ? type.name : `unknown_type_${typeIndex}`,
|
|
311
|
-
_typeIndex: typeIndex,
|
|
312
|
-
_name: name,
|
|
313
|
-
_startPos: startPos
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
if (type) {
|
|
317
|
-
for (const mv of this._getTypeMemVars(typeIndex)) {
|
|
318
|
-
try {
|
|
319
|
-
const { value, bytesRead } = this._readValue(pos, mv);
|
|
320
|
-
obj[mv.name] = value;
|
|
321
|
-
pos += bytesRead;
|
|
322
|
-
} catch (e) {
|
|
323
|
-
console.warn(
|
|
324
|
-
`[Tgcl] Error reading field "${type.name}.${mv.name}" at offset ${pos}:`,
|
|
325
|
-
e.message
|
|
326
|
-
);
|
|
327
|
-
pos += mv.size || 4; // best-effort skip
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
obj._nextPos = pos;
|
|
333
|
-
return obj;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ── Private: value readers ─────────────────────────────────────────────────
|
|
337
|
-
|
|
338
|
-
_readValue(pos, memVar) {
|
|
339
|
-
const { varType } = memVar;
|
|
340
|
-
|
|
341
|
-
switch (varType) {
|
|
342
|
-
case VAR_TYPE.STRING:
|
|
343
|
-
return this._readStringValue(pos);
|
|
344
|
-
|
|
345
|
-
case VAR_TYPE.OBJECT_PTR:
|
|
346
|
-
return this._readObjectPtrValue(pos);
|
|
347
|
-
|
|
348
|
-
case VAR_TYPE.ARRAY:
|
|
349
|
-
return this._readArrayValue(pos, memVar);
|
|
350
|
-
|
|
351
|
-
case VAR_TYPE.NORMAL:
|
|
352
|
-
default:
|
|
353
|
-
return this._readNormalValue(pos, memVar.size, memVar.name);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
_readStringValue(pos) {
|
|
358
|
-
const { value, bytesRead } = readCString(this._buf, pos);
|
|
359
|
-
return { value, bytesRead };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
_readObjectPtrValue(pos) {
|
|
363
|
-
const idx = this._buf.readInt32LE(pos);
|
|
364
|
-
return { value: idx === -1 ? null : `@object_${idx}`, bytesRead: 4 };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
_readArrayValue(pos, memVar) {
|
|
368
|
-
const buf = this._buf;
|
|
369
|
-
const count = buf.readUInt32LE(pos);
|
|
370
|
-
const elementTypeIdx = memVar.extra;
|
|
371
|
-
|
|
372
|
-
// ── Ref array (0xFFFFFFFF) ───────────────────────────────────────────────
|
|
373
|
-
if (elementTypeIdx === 0xFFFFFFFF) {
|
|
374
|
-
const refs = [];
|
|
375
|
-
for (let i = 0; i < count; i++) {
|
|
376
|
-
const idx = buf.readInt32LE(pos + 4 + i * 4);
|
|
377
|
-
refs.push(idx === -1 ? null : `@object_${idx}`);
|
|
378
|
-
}
|
|
379
|
-
return { value: refs, bytesRead: 4 + count * 4 };
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// ── Inline typed sub-objects ─────────────────────────────────────────────
|
|
383
|
-
if (elementTypeIdx < this.types.length) {
|
|
384
|
-
const elemMemVars = this._getTypeMemVars(elementTypeIdx);
|
|
385
|
-
const elements = [];
|
|
386
|
-
let arrayPos = pos + 4;
|
|
387
|
-
|
|
388
|
-
for (let i = 0; i < count; i++) {
|
|
389
|
-
const elem = {};
|
|
390
|
-
for (const emv of elemMemVars) {
|
|
391
|
-
const { value, bytesRead } = this._readValue(arrayPos, emv);
|
|
392
|
-
elem[emv.name] = value;
|
|
393
|
-
arrayPos += bytesRead;
|
|
394
|
-
}
|
|
395
|
-
elements.push(elem);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return { value: elements, bytesRead: arrayPos - pos };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ── Unknown element type – store sentinel, skip raw bytes ────────────────
|
|
402
|
-
// Element size is stored in `extra` when it's not a type index.
|
|
403
|
-
// Fall back to 4 if zero to avoid infinite loops.
|
|
404
|
-
const elemSize = elementTypeIdx || 4;
|
|
405
|
-
console.warn(
|
|
406
|
-
`[Tgcl] Unknown ARRAY element type index ${elementTypeIdx}; ` +
|
|
407
|
-
`skipping ${count} * ${elemSize} bytes. Data is NOT preserved.`
|
|
408
|
-
);
|
|
409
|
-
return {
|
|
410
|
-
value: { _unknownArray: true, count, elementTypeIdx },
|
|
411
|
-
bytesRead: 4 + count * elemSize
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
_readNormalValue(pos, size, name) {
|
|
416
|
-
const buf = this._buf;
|
|
417
|
-
let value;
|
|
418
|
-
|
|
419
|
-
switch (size) {
|
|
420
|
-
case 1:
|
|
421
|
-
value = buf.readUInt8(pos);
|
|
422
|
-
if (isBoolName(name)) value = value !== 0;
|
|
423
|
-
break;
|
|
424
|
-
|
|
425
|
-
case 4:
|
|
426
|
-
if (isUInt32Name(name)) {
|
|
427
|
-
value = buf.readUInt32LE(pos);
|
|
428
|
-
} else {
|
|
429
|
-
const intVal = buf.readInt32LE(pos);
|
|
430
|
-
const floatVal = buf.readFloatLE(pos);
|
|
431
|
-
// Prefer float when the float interpretation is "reasonable".
|
|
432
|
-
value =
|
|
433
|
-
(Math.abs(floatVal) < 1e10 && Math.abs(floatVal) > 1e-10) ||
|
|
434
|
-
floatVal === 0
|
|
435
|
-
? floatVal
|
|
436
|
-
: intVal;
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
439
|
-
|
|
440
|
-
case 8:
|
|
441
|
-
value = buf.readDoubleLE(pos);
|
|
442
|
-
break;
|
|
443
|
-
|
|
444
|
-
case 12:
|
|
445
|
-
value = {
|
|
446
|
-
x: buf.readFloatLE(pos),
|
|
447
|
-
y: buf.readFloatLE(pos + 4),
|
|
448
|
-
z: buf.readFloatLE(pos + 8)
|
|
449
|
-
};
|
|
450
|
-
break;
|
|
451
|
-
|
|
452
|
-
case 16:
|
|
453
|
-
value = {
|
|
454
|
-
x: buf.readFloatLE(pos),
|
|
455
|
-
y: buf.readFloatLE(pos + 4),
|
|
456
|
-
z: buf.readFloatLE(pos + 8),
|
|
457
|
-
w: buf.readFloatLE(pos + 12)
|
|
458
|
-
};
|
|
459
|
-
break;
|
|
460
|
-
|
|
461
|
-
case 64:
|
|
462
|
-
value = {
|
|
463
|
-
matrix: [
|
|
464
|
-
[buf.readFloatLE(pos), buf.readFloatLE(pos + 4), buf.readFloatLE(pos + 8), buf.readFloatLE(pos + 12)],
|
|
465
|
-
[buf.readFloatLE(pos + 16), buf.readFloatLE(pos + 20), buf.readFloatLE(pos + 24), buf.readFloatLE(pos + 28)],
|
|
466
|
-
[buf.readFloatLE(pos + 32), buf.readFloatLE(pos + 36), buf.readFloatLE(pos + 40), buf.readFloatLE(pos + 44)],
|
|
467
|
-
[buf.readFloatLE(pos + 48), buf.readFloatLE(pos + 52), buf.readFloatLE(pos + 56), buf.readFloatLE(pos + 60)]
|
|
468
|
-
],
|
|
469
|
-
pos: {
|
|
470
|
-
x: buf.readFloatLE(pos + 48),
|
|
471
|
-
y: buf.readFloatLE(pos + 52),
|
|
472
|
-
z: buf.readFloatLE(pos + 56)
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
break;
|
|
476
|
-
|
|
477
|
-
default:
|
|
478
|
-
value = buf.slice(pos, pos + size).toString('hex');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return { value, bytesRead: size };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ── Private: write helpers ─────────────────────────────────────────────────
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Intern all type/field name strings into a contiguous Data Section buffer.
|
|
488
|
-
* Returns the buffer and a lookup function `offsetOf(name) → number`.
|
|
489
|
-
*/
|
|
490
|
-
_buildDataSection() {
|
|
491
|
-
const map = new Map(); // string → byte offset within data section
|
|
492
|
-
const parts = [];
|
|
493
|
-
let cursor = 0;
|
|
494
|
-
|
|
495
|
-
const intern = (str) => {
|
|
496
|
-
if (map.has(str)) return;
|
|
497
|
-
const chunk = writeCString(str);
|
|
498
|
-
map.set(str, cursor);
|
|
499
|
-
parts.push(chunk);
|
|
500
|
-
cursor += chunk.length;
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
for (const t of this.types) intern(t.name);
|
|
504
|
-
for (const mv of this.memVars) intern(mv.name);
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
dataBuf: Buffer.concat(parts),
|
|
508
|
-
offsetOf: (str) => {
|
|
509
|
-
const off = map.get(str);
|
|
510
|
-
if (off === undefined)
|
|
511
|
-
throw new Error(`[Tgcl] String "${str}" not found in data section`);
|
|
512
|
-
return off;
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
_serializeObject(obj) {
|
|
518
|
-
const parts = [];
|
|
519
|
-
const typeIndex = obj._typeIndex;
|
|
520
|
-
|
|
521
|
-
// typeIndex
|
|
522
|
-
const typeBuf = Buffer.allocUnsafe(4);
|
|
523
|
-
typeBuf.writeUInt32LE(typeIndex >>> 0);
|
|
524
|
-
parts.push(typeBuf);
|
|
525
|
-
|
|
526
|
-
// name
|
|
527
|
-
parts.push(writeCString(obj._name ?? ''));
|
|
528
|
-
|
|
529
|
-
// field values, in schema order
|
|
530
|
-
for (const mv of this._getTypeMemVars(typeIndex)) {
|
|
531
|
-
parts.push(this._writeValue(obj[mv.name], mv));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return Buffer.concat(parts);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ── Private: value writers ─────────────────────────────────────────────────
|
|
538
|
-
|
|
539
|
-
_writeValue(value, memVar) {
|
|
540
|
-
switch (memVar.varType) {
|
|
541
|
-
case VAR_TYPE.STRING:
|
|
542
|
-
return writeCString(value);
|
|
543
|
-
|
|
544
|
-
case VAR_TYPE.OBJECT_PTR:
|
|
545
|
-
return this._writeObjectPtrValue(value);
|
|
546
|
-
|
|
547
|
-
case VAR_TYPE.ARRAY:
|
|
548
|
-
return this._writeArrayValue(value, memVar);
|
|
549
|
-
|
|
550
|
-
case VAR_TYPE.NORMAL:
|
|
551
|
-
default:
|
|
552
|
-
return this._writeNormalValue(value, memVar.size, memVar.name);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
_writeObjectPtrValue(value) {
|
|
557
|
-
const buf = Buffer.allocUnsafe(4);
|
|
558
|
-
if (value == null) {
|
|
559
|
-
buf.writeInt32LE(-1);
|
|
560
|
-
} else {
|
|
561
|
-
const idx = parseInt(String(value).replace('@object_', ''), 10);
|
|
562
|
-
buf.writeInt32LE(isNaN(idx) ? -1 : idx);
|
|
563
|
-
}
|
|
564
|
-
return buf;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
_writeArrayValue(value, memVar) {
|
|
568
|
-
const elementTypeIdx = memVar.extra;
|
|
569
|
-
|
|
570
|
-
// ── Unknown array (not decoded at read time) ─────────────────────────────
|
|
571
|
-
if (value && value._unknownArray) {
|
|
572
|
-
// We cannot recover the original bytes. Write an empty array.
|
|
573
|
-
const buf = Buffer.allocUnsafe(4);
|
|
574
|
-
buf.writeUInt32LE(0);
|
|
575
|
-
return buf;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const items = Array.isArray(value) ? value : [];
|
|
579
|
-
|
|
580
|
-
// ── Ref array ────────────────────────────────────────────────────────────
|
|
581
|
-
if (elementTypeIdx === 0xFFFFFFFF) {
|
|
582
|
-
const buf = Buffer.allocUnsafe(4 + items.length * 4);
|
|
583
|
-
buf.writeUInt32LE(items.length, 0);
|
|
584
|
-
for (let i = 0; i < items.length; i++) {
|
|
585
|
-
const ref = items[i];
|
|
586
|
-
const idx = ref == null ? -1 : parseInt(String(ref).replace('@object_', ''), 10);
|
|
587
|
-
buf.writeInt32LE(isNaN(idx) ? -1 : idx, 4 + i * 4);
|
|
588
|
-
}
|
|
589
|
-
return buf;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// ── Inline typed sub-objects ─────────────────────────────────────────────
|
|
593
|
-
if (elementTypeIdx < this.types.length) {
|
|
594
|
-
const elemMemVars = this._getTypeMemVars(elementTypeIdx);
|
|
595
|
-
const countBuf = Buffer.allocUnsafe(4);
|
|
596
|
-
countBuf.writeUInt32LE(items.length);
|
|
597
|
-
const parts = [countBuf];
|
|
598
|
-
|
|
599
|
-
for (const elem of items) {
|
|
600
|
-
for (const emv of elemMemVars) {
|
|
601
|
-
parts.push(this._writeValue(elem != null ? elem[emv.name] : undefined, emv));
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return Buffer.concat(parts);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// ── Fallback: unknown element type, write empty ──────────────────────────
|
|
609
|
-
const fallback = Buffer.allocUnsafe(4);
|
|
610
|
-
fallback.writeUInt32LE(0);
|
|
611
|
-
return fallback;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
_writeNormalValue(value, size, name) {
|
|
615
|
-
const buf = Buffer.allocUnsafe(size);
|
|
616
|
-
const n = typeof value === 'number' ? value : 0;
|
|
617
|
-
|
|
618
|
-
switch (size) {
|
|
619
|
-
case 1:
|
|
620
|
-
if (isBoolName(name)) {
|
|
621
|
-
buf.writeUInt8(value ? 1 : 0);
|
|
622
|
-
} else {
|
|
623
|
-
buf.writeUInt8(n & 0xFF);
|
|
624
|
-
}
|
|
625
|
-
break;
|
|
626
|
-
|
|
627
|
-
case 4:
|
|
628
|
-
if (isUInt32Name(name)) {
|
|
629
|
-
buf.writeUInt32LE(n >>> 0); // treat as unsigned
|
|
630
|
-
} else {
|
|
631
|
-
// Reproduce the float-vs-int decision made at read time.
|
|
632
|
-
encode4ByteNormal(buf, 0, n);
|
|
633
|
-
}
|
|
634
|
-
break;
|
|
635
|
-
|
|
636
|
-
case 8:
|
|
637
|
-
buf.writeDoubleLE(n);
|
|
638
|
-
break;
|
|
639
|
-
|
|
640
|
-
case 12:
|
|
641
|
-
buf.writeFloatLE(value?.x ?? 0, 0);
|
|
642
|
-
buf.writeFloatLE(value?.y ?? 0, 4);
|
|
643
|
-
buf.writeFloatLE(value?.z ?? 0, 8);
|
|
644
|
-
break;
|
|
645
|
-
|
|
646
|
-
case 16:
|
|
647
|
-
buf.writeFloatLE(value?.x ?? 0, 0);
|
|
648
|
-
buf.writeFloatLE(value?.y ?? 0, 4);
|
|
649
|
-
buf.writeFloatLE(value?.z ?? 0, 8);
|
|
650
|
-
buf.writeFloatLE(value?.w ?? 0, 12);
|
|
651
|
-
break;
|
|
652
|
-
|
|
653
|
-
case 64: {
|
|
654
|
-
const m = value?.matrix;
|
|
655
|
-
if (Array.isArray(m)) {
|
|
656
|
-
for (let r = 0; r < 4; r++)
|
|
657
|
-
for (let c = 0; c < 4; c++)
|
|
658
|
-
buf.writeFloatLE(m[r]?.[c] ?? 0, (r * 4 + c) * 4);
|
|
659
|
-
} else {
|
|
660
|
-
buf.fill(0);
|
|
661
|
-
}
|
|
662
|
-
break;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
default:
|
|
666
|
-
// Stored as a hex string; restore raw bytes.
|
|
667
|
-
if (typeof value === 'string') {
|
|
668
|
-
const raw = Buffer.from(value, 'hex');
|
|
669
|
-
raw.copy(buf, 0, 0, Math.min(raw.length, size));
|
|
670
|
-
if (raw.length < size) buf.fill(0, raw.length); // zero-pad tail
|
|
671
|
-
} else {
|
|
672
|
-
buf.fill(0);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return buf;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
681
|
-
|
|
682
|
-
Tgcl.VAR_TYPE = VAR_TYPE;
|
|
683
|
-
module.exports = { Tgcl, LevelObjects: Tgcl };
|