@gmvjs/dunia1 0.0.0-stage → 0.1.1
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 +72 -2
- package/dist/fat.d.ts +57 -0
- package/dist/fat.js +226 -0
- package/dist/fat.js.map +1 -0
- package/dist/fcb.d.ts +14 -0
- package/dist/fcb.js +86 -0
- package/dist/fcb.js.map +1 -0
- package/dist/games/farcry2.d.ts +7 -0
- package/dist/games/farcry2.js +11 -0
- package/dist/games/farcry2.js.map +1 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +12 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/level.d.ts +120 -0
- package/dist/level.js +751 -0
- package/dist/level.js.map +1 -0
- package/dist/mab.d.ts +40 -0
- package/dist/mab.js +228 -0
- package/dist/mab.js.map +1 -0
- package/dist/realtree.d.ts +27 -0
- package/dist/realtree.js +127 -0
- package/dist/realtree.js.map +1 -0
- package/dist/rml.d.ts +15 -0
- package/dist/rml.js +77 -0
- package/dist/rml.js.map +1 -0
- package/dist/xbg.d.ts +115 -0
- package/dist/xbg.js +524 -0
- package/dist/xbg.js.map +1 -0
- package/dist/xbt.d.ts +3 -0
- package/dist/xbt.js +14 -0
- package/dist/xbt.js.map +1 -0
- package/package.json +54 -4
package/dist/level.js
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { boundsOf } from "@gmvjs/core/geometry";
|
|
2
|
+
import { LevelBuilder, markerMesh } from "@gmvjs/core/levelbuilder";
|
|
3
|
+
import { compose } from "@gmvjs/core/math";
|
|
4
|
+
import { LEVEL_MARK, duniaApi } from "./fat.js";
|
|
5
|
+
import { fcbFind, fcbString, parseFcb, fcbHash, fcbVec3 } from "./fcb.js";
|
|
6
|
+
import { xmlElements } from "./rml.js";
|
|
7
|
+
import { isRealTree, parseRealTree, realTreeMeshes } from "./realtree.js";
|
|
8
|
+
import { parseXbg, parseXbm, xbgMeshes, bakeBindPose, kitHiddenParts } from "./xbg.js";
|
|
9
|
+
const SECTOR = 64, GRID = 65, TER_MAGIC = 3909095506;
|
|
10
|
+
const TRI_BUDGET = 4e6;
|
|
11
|
+
const SMALL_WORLD = 12e3;
|
|
12
|
+
const COLLECTION_LOD = 1, COLLECTION_BUDGET = 3e6;
|
|
13
|
+
function rngOf(seed) {
|
|
14
|
+
let a = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
a = a + 1831565813 >>> 0;
|
|
17
|
+
let t = a;
|
|
18
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
20
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const H = {
|
|
24
|
+
Entity: fcbHash("Entity"),
|
|
25
|
+
WorldSector: fcbHash("WorldSector"),
|
|
26
|
+
hidName: fcbHash("hidName"),
|
|
27
|
+
hidPos: fcbHash("hidPos"),
|
|
28
|
+
hidAngles: fcbHash("hidAngles"),
|
|
29
|
+
hidMeshName: fcbHash("hidMeshName"),
|
|
30
|
+
X: fcbHash("X"),
|
|
31
|
+
Y: fcbHash("Y"),
|
|
32
|
+
model: 3214621276,
|
|
33
|
+
archetype: 879405875,
|
|
34
|
+
layer: 1229720050,
|
|
35
|
+
layerName: 3312423428
|
|
36
|
+
};
|
|
37
|
+
function worldDescriptor(u) {
|
|
38
|
+
const layers = [];
|
|
39
|
+
let sx = 0, sy = 0, mx = 1, my = 1, inLayers = -1;
|
|
40
|
+
for (const el of xmlElements(u)) {
|
|
41
|
+
if (el.name === "GridMapSectors" && !sx) {
|
|
42
|
+
sx = +el.attrs.CountX || 0;
|
|
43
|
+
sy = +el.attrs.CountY || 0;
|
|
44
|
+
}
|
|
45
|
+
if (el.name === "GridWorldMaps") {
|
|
46
|
+
mx = +el.attrs.CountX || 1;
|
|
47
|
+
my = +el.attrs.CountY || 1;
|
|
48
|
+
}
|
|
49
|
+
if (el.name === "Layers") {
|
|
50
|
+
inLayers = el.depth;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (inLayers >= 0 && el.depth <= inLayers) inLayers = -1;
|
|
54
|
+
if (inLayers >= 0 && el.depth === inLayers + 1 && el.name === "Layer") {
|
|
55
|
+
layers.push({ name: (el.attrs.Name ?? "").trim(), texture: el.attrs.Texture ?? "", normal: el.attrs.NormalMap ?? "", tiling: +el.attrs.Tiling || 20 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { layers, countX: sx * mx, countY: sy * my };
|
|
59
|
+
}
|
|
60
|
+
function parseTerrainSector(u) {
|
|
61
|
+
const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);
|
|
62
|
+
if (u.length < 592 + GRID * GRID * 4 || dv.getUint32(0, true) !== TER_MAGIC) throw new Error("Не ландшафт сектора Far Cry 2");
|
|
63
|
+
const height = new Float32Array(GRID * GRID), mask = new Uint8Array(GRID * GRID);
|
|
64
|
+
for (let i = 0; i < GRID * GRID; i++) {
|
|
65
|
+
height[i] = dv.getUint16(592 + i * 4, true) / 128;
|
|
66
|
+
mask[i] = u[592 + i * 4 + 3];
|
|
67
|
+
}
|
|
68
|
+
const water = dv.getFloat32(60, true);
|
|
69
|
+
return { id: dv.getUint32(20, true), min: dv.getFloat32(28, true), max: dv.getFloat32(32, true), layers: [u[36], u[37], u[38], u[39]], height, mask, water: Number.isFinite(water) && water > 0 ? water : 0 };
|
|
70
|
+
}
|
|
71
|
+
function vertexLayer(mask, slots) {
|
|
72
|
+
for (let k = 3; k >= 0; k--) if (mask & 1 << k && slots[k] !== 255) return slots[k];
|
|
73
|
+
for (const s of slots) if (s !== 255) return s;
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
function anglesQuat(a) {
|
|
77
|
+
const h = (d) => d * Math.PI / 360;
|
|
78
|
+
const qx = [Math.cos(h(a[0])), Math.sin(h(a[0])), 0, 0], qy = [Math.cos(h(a[1])), 0, Math.sin(h(a[1])), 0], qz = [Math.cos(h(a[2])), 0, 0, Math.sin(h(a[2]))];
|
|
79
|
+
return qmul(qmul(qz, qy), qx);
|
|
80
|
+
}
|
|
81
|
+
const qmul = (a, b) => [
|
|
82
|
+
a[0] * b[0] - a[1] * b[1] - a[2] * b[2] - a[3] * b[3],
|
|
83
|
+
a[0] * b[1] + a[1] * b[0] + a[2] * b[3] - a[3] * b[2],
|
|
84
|
+
a[0] * b[2] - a[1] * b[3] + a[2] * b[0] + a[3] * b[1],
|
|
85
|
+
a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0]
|
|
86
|
+
];
|
|
87
|
+
const modelOf = (e) => {
|
|
88
|
+
const walk = (o, d) => {
|
|
89
|
+
const v = fcbString(o.values.get(H.model));
|
|
90
|
+
if (v) return v;
|
|
91
|
+
if (d > 6) return void 0;
|
|
92
|
+
for (const c of o.children) {
|
|
93
|
+
if (c.type === H.Entity) continue;
|
|
94
|
+
const r = walk(c, d + 1);
|
|
95
|
+
if (r) return r;
|
|
96
|
+
}
|
|
97
|
+
return void 0;
|
|
98
|
+
};
|
|
99
|
+
return walk(e, 0);
|
|
100
|
+
};
|
|
101
|
+
const meshNamesOf = (e) => {
|
|
102
|
+
for (const o of fcbFind(e, fcbHash("object"))) {
|
|
103
|
+
const s = fcbString(o.values.get(H.hidMeshName));
|
|
104
|
+
if (s && s.includes(";")) return s.split(";").filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
};
|
|
108
|
+
function entityLibrary(root) {
|
|
109
|
+
const out = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const e of fcbFind(root, H.Entity)) {
|
|
111
|
+
const n = fcbString(e.values.get(H.hidName)), m = modelOf(e);
|
|
112
|
+
if (n && m && !out.has(n)) out.set(n, { model: m, parts: meshNamesOf(e) });
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
const PARTICLES = fcbHash("CNewParticlesComponent"), LIGHT = fcbHash("CDynamicLightComponent"), PARTICLE_NAME = 423866672;
|
|
117
|
+
function sectorEntities(root, lib) {
|
|
118
|
+
const list = [], unknownArchetype = /* @__PURE__ */ new Set(), markers = [];
|
|
119
|
+
let noModel = 0;
|
|
120
|
+
const visit = (o, layer) => {
|
|
121
|
+
if (o.type === H.layer) layer = fcbString(o.values.get(H.layerName)) ?? layer;
|
|
122
|
+
if (o.type === H.Entity) {
|
|
123
|
+
const pos = fcbVec3(o.values.get(H.hidPos));
|
|
124
|
+
const arch = fcbString(o.values.get(H.archetype));
|
|
125
|
+
let model = modelOf(o), parts = meshNamesOf(o);
|
|
126
|
+
if (!model && arch) {
|
|
127
|
+
const L = lib.get(arch);
|
|
128
|
+
if (L) {
|
|
129
|
+
model = L.model;
|
|
130
|
+
if (!parts.length) parts = L.parts;
|
|
131
|
+
} else unknownArchetype.add(arch);
|
|
132
|
+
}
|
|
133
|
+
if (pos && model) list.push({ name: fcbString(o.values.get(H.hidName)) ?? arch ?? "entity", model, parts, pos, angles: fcbVec3(o.values.get(H.hidAngles)) ?? [0, 0, 0], layer, ...arch ? { archetype: arch } : {} });
|
|
134
|
+
else if (pos) {
|
|
135
|
+
noModel++;
|
|
136
|
+
const name = fcbString(o.values.get(H.hidName)) ?? "entity";
|
|
137
|
+
const pc = fcbFind(o, PARTICLES)[0];
|
|
138
|
+
if (pc) markers.push({ kind: "particle", name: fcbString(pc.values.get(PARTICLE_NAME)) ?? name, pos });
|
|
139
|
+
else if (fcbFind(o, LIGHT).length) markers.push({ kind: "light", name, pos });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const c of o.children) visit(c, layer);
|
|
143
|
+
};
|
|
144
|
+
visit(root, "main");
|
|
145
|
+
return { list, noModel, unknownArchetype, markers };
|
|
146
|
+
}
|
|
147
|
+
function sectorCollections(root) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const set of fcbFind(root, 290258663)) {
|
|
150
|
+
const d = set.children.find((c) => c.type === 2710340740);
|
|
151
|
+
if (!d) continue;
|
|
152
|
+
const arr = (k) => {
|
|
153
|
+
const v = d.values.get(k);
|
|
154
|
+
if (!v || v.length < 4) return null;
|
|
155
|
+
const dv = new DataView(v.buffer, v.byteOffset, v.byteLength);
|
|
156
|
+
const n = dv.getUint32(0, true);
|
|
157
|
+
return n * 4 + 4 <= v.length ? { dv, n } : null;
|
|
158
|
+
};
|
|
159
|
+
const models = arr(2311635093), counts = arr(1487750114), groups = arr(2039231445), xy = arr(3531669696), zs = arr(2384941770), rot = arr(2563143869);
|
|
160
|
+
if (!models || !counts || !groups || !xy || !zs || counts.n !== models.n) continue;
|
|
161
|
+
let g = 0;
|
|
162
|
+
for (let m = 0; m < models.n; m++) {
|
|
163
|
+
const hash = models.dv.getUint32(4 + m * 4, true), ng = counts.dv.getUint32(4 + m * 4, true);
|
|
164
|
+
for (let k = 0; k < ng && g < groups.n; k++, g++) {
|
|
165
|
+
const gv = groups.dv.getUint32(4 + g * 4, true), cnt = gv & 255, first = gv >>> 8;
|
|
166
|
+
for (let i = first; i < first + cnt && i < xy.n && i < zs.n; i++) {
|
|
167
|
+
const x = xy.dv.getUint16(4 + i * 4, true) / 10, y = xy.dv.getUint16(6 + i * 4, true) / 10, z = zs.dv.getFloat32(4 + i * 4, true);
|
|
168
|
+
const yaw = rot && i < rot.n ? (rot.dv.getUint32(4 + i * 4, true) & 65535) / 65536 * 360 : 0;
|
|
169
|
+
out.push({ hash, pos: [x, y, z], yaw });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
async function mapLimit(items, n, f) {
|
|
177
|
+
const out = new Array(items.length);
|
|
178
|
+
let k = 0;
|
|
179
|
+
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
180
|
+
while (k < items.length) {
|
|
181
|
+
const i = k++;
|
|
182
|
+
out[i] = await f(items[i]);
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
const terrainStep = (w, h, budget = 52e4) => {
|
|
188
|
+
let s = 1;
|
|
189
|
+
while ((Math.floor(w * SECTOR / s) + 1) * (Math.floor(h * SECTOR / s) + 1) > budget && s < SECTOR) s *= 2;
|
|
190
|
+
return s;
|
|
191
|
+
};
|
|
192
|
+
function terrainMeshes(spec, sectors, widthInSectors, layers, step) {
|
|
193
|
+
const NX = spec.w * SECTOR / step + 1, NY = spec.h * SECTOR / step + 1, n = NX * NY;
|
|
194
|
+
const hgt = new Float32Array(n).fill(NaN), base = new Int16Array(n).fill(-1), over = [1, 2, 3].map(() => new Int16Array(n).fill(-1));
|
|
195
|
+
for (let gy = 0; gy < NY; gy++) {
|
|
196
|
+
for (let gx = 0; gx < NX; gx++) {
|
|
197
|
+
const mx = gx * step, my = gy * step;
|
|
198
|
+
let sx = Math.floor(mx / SECTOR), sy = Math.floor(my / SECTOR);
|
|
199
|
+
if (sx >= spec.w) sx = spec.w - 1;
|
|
200
|
+
if (sy >= spec.h) sy = spec.h - 1;
|
|
201
|
+
const s = sectors.get((spec.y0 + sy) * widthInSectors + spec.x0 + sx);
|
|
202
|
+
if (!s) continue;
|
|
203
|
+
const lx = mx - sx * SECTOR, ly = my - sy * SECTOR, k = ly * GRID + lx, g = gy * NX + gx, m = s.mask[k];
|
|
204
|
+
hgt[g] = s.height[k];
|
|
205
|
+
const b0 = s.layers.findIndex((l) => l !== 255);
|
|
206
|
+
base[g] = b0 >= 0 ? s.layers[b0] : -1;
|
|
207
|
+
for (let j = 1; j <= 3; j++) if (j > b0 && m & 1 << j && s.layers[j] !== 255) over[j - 1][g] = s.layers[j];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const nor = new Float32Array(n * 3);
|
|
211
|
+
const at = (x, y, d) => {
|
|
212
|
+
const v = hgt[Math.min(NY - 1, Math.max(0, y)) * NX + Math.min(NX - 1, Math.max(0, x))];
|
|
213
|
+
return Number.isNaN(v) ? d : v;
|
|
214
|
+
};
|
|
215
|
+
for (let y = 0; y < NY; y++) for (let x = 0; x < NX; x++) {
|
|
216
|
+
const c = hgt[y * NX + x];
|
|
217
|
+
if (Number.isNaN(c)) continue;
|
|
218
|
+
const dx = (at(x + 1, y, c) - at(x - 1, y, c)) / (2 * step), dy = (at(x, y + 1, c) - at(x, y - 1, c)) / (2 * step), l = Math.hypot(dx, dy, 1);
|
|
219
|
+
nor[(y * NX + x) * 3] = -dx / l;
|
|
220
|
+
nor[(y * NX + x) * 3 + 1] = -dy / l;
|
|
221
|
+
nor[(y * NX + x) * 3 + 2] = 1 / l;
|
|
222
|
+
}
|
|
223
|
+
const meshes = [], local = new Int32Array(n);
|
|
224
|
+
const ox = spec.x0 * SECTOR, oy = spec.y0 * SECTOR;
|
|
225
|
+
const valid = (g) => !Number.isNaN(hgt[g]) && !Number.isNaN(hgt[g + 1]) && !Number.isNaN(hgt[g + NX]) && !Number.isNaN(hgt[g + NX + 1]);
|
|
226
|
+
const emit = (L, name, take, alpha) => {
|
|
227
|
+
local.fill(-1);
|
|
228
|
+
const P = [], U = [], N = [], A = [], I = [];
|
|
229
|
+
const layer = layers[L], rep = (layer?.tiling ?? 20) / SECTOR;
|
|
230
|
+
const vi = (x, y) => {
|
|
231
|
+
const g = y * NX + x;
|
|
232
|
+
let v = local[g];
|
|
233
|
+
if (v < 0) {
|
|
234
|
+
v = local[g] = P.length / 3;
|
|
235
|
+
const wx = ox + x * step, wy = oy + y * step;
|
|
236
|
+
P.push(wx, wy, hgt[g]);
|
|
237
|
+
N.push(nor[g * 3], nor[g * 3 + 1], nor[g * 3 + 2]);
|
|
238
|
+
U.push(wx * rep, wy * rep);
|
|
239
|
+
if (alpha) A.push(alpha(g));
|
|
240
|
+
}
|
|
241
|
+
return v;
|
|
242
|
+
};
|
|
243
|
+
for (let y = 0; y + 1 < NY; y++) for (let x = 0; x + 1 < NX; x++) {
|
|
244
|
+
const g = y * NX + x;
|
|
245
|
+
if (!take(g) || !valid(g)) continue;
|
|
246
|
+
const a = vi(x, y), b = vi(x + 1, y), c = vi(x, y + 1), d = vi(x + 1, y + 1);
|
|
247
|
+
I.push(a, b, d, a, d, c);
|
|
248
|
+
}
|
|
249
|
+
if (!I.length) return;
|
|
250
|
+
const nv = P.length / 3;
|
|
251
|
+
meshes.push({
|
|
252
|
+
name,
|
|
253
|
+
texture: layer?.texture ?? "",
|
|
254
|
+
normalMap: layer?.normal || void 0,
|
|
255
|
+
vertexCount: nv,
|
|
256
|
+
pos: Float32Array.from(P),
|
|
257
|
+
nor: Float32Array.from(N),
|
|
258
|
+
uv: Float32Array.from(U),
|
|
259
|
+
bone: new Float32Array(0),
|
|
260
|
+
weight: new Float32Array(0),
|
|
261
|
+
indices: nv > 65535 ? Uint32Array.from(I) : Uint16Array.from(I),
|
|
262
|
+
skinned: false,
|
|
263
|
+
...layer ? {} : { color: [0.45, 0.42, 0.36] },
|
|
264
|
+
...alpha ? { vertexAlpha: Float32Array.from(A) } : {}
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const label = (L) => layers[L]?.name || (L < 0 ? "без слоя" : `слой ${L}`);
|
|
268
|
+
for (const L of [...new Set(base)].sort((a, b) => a - b)) emit(L, `Ландшафт · ${label(L)}`, (g) => base[g] === L);
|
|
269
|
+
over.forEach((o, j) => {
|
|
270
|
+
for (const L of [...new Set(o)].filter((l) => l >= 0).sort((a, b) => a - b)) {
|
|
271
|
+
emit(L, `Ландшафт · ${label(L)} (наложение ${j + 1})`, (g) => o[g] === L || o[g + 1] === L || o[g + NX] === L || o[g + NX + 1] === L, (g) => o[g] === L ? 1 : 0);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return meshes;
|
|
275
|
+
}
|
|
276
|
+
function waterMesh(spec, sectors, widthInSectors, step) {
|
|
277
|
+
const P = [], I = [];
|
|
278
|
+
const cells = SECTOR / step;
|
|
279
|
+
for (let sy = 0; sy < spec.h; sy++) for (let sx = 0; sx < spec.w; sx++) {
|
|
280
|
+
const s = sectors.get((spec.y0 + sy) * widthInSectors + spec.x0 + sx);
|
|
281
|
+
if (!s || !s.water || s.min >= s.water) continue;
|
|
282
|
+
const ox = (spec.x0 + sx) * SECTOR, oy = (spec.y0 + sy) * SECTOR, L = s.water;
|
|
283
|
+
for (let cy = 0; cy < cells; cy++) for (let cx = 0; cx < cells; cx++) {
|
|
284
|
+
let lo = Infinity;
|
|
285
|
+
for (const [dx, dy] of [[0, 0], [1, 0], [0, 1], [1, 1]]) lo = Math.min(lo, s.height[(cy + dy) * step * GRID + (cx + dx) * step]);
|
|
286
|
+
if (lo >= L) continue;
|
|
287
|
+
const v = P.length / 3, x = ox + cx * step, y = oy + cy * step;
|
|
288
|
+
P.push(x, y, L, x + step, y, L, x, y + step, L, x + step, y + step, L);
|
|
289
|
+
I.push(v, v + 1, v + 3, v, v + 3, v + 2);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!I.length) return null;
|
|
293
|
+
const nv = P.length / 3, nor = new Float32Array(nv * 3), uv = new Float32Array(nv * 2);
|
|
294
|
+
for (let i = 0; i < nv; i++) {
|
|
295
|
+
nor[i * 3 + 2] = 1;
|
|
296
|
+
uv[i * 2] = P[i * 3] / 8;
|
|
297
|
+
uv[i * 2 + 1] = P[i * 3 + 1] / 8;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
name: "Вода",
|
|
301
|
+
texture: "",
|
|
302
|
+
color: [0.16, 0.3, 0.36],
|
|
303
|
+
vertexCount: nv,
|
|
304
|
+
pos: Float32Array.from(P),
|
|
305
|
+
nor,
|
|
306
|
+
uv,
|
|
307
|
+
bone: new Float32Array(0),
|
|
308
|
+
weight: new Float32Array(0),
|
|
309
|
+
indices: nv > 65535 ? Uint32Array.from(I) : Uint16Array.from(I),
|
|
310
|
+
skinned: false,
|
|
311
|
+
vertexAlpha: new Float32Array(nv).fill(0.7)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const isTerrainSize = (s) => s >= 23536 && s <= 26e3 && (s - 23536) % 12 === 0;
|
|
315
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
316
|
+
const groupCache = /* @__PURE__ */ new WeakMap();
|
|
317
|
+
async function terrainOf(api, files) {
|
|
318
|
+
const refs = files.filter((r) => isTerrainSize(r.fe.size));
|
|
319
|
+
const heads = await mapLimit(refs, 16, async (r) => {
|
|
320
|
+
try {
|
|
321
|
+
return await api.readRef(r, 24);
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const out = [];
|
|
327
|
+
heads.forEach((h, i) => {
|
|
328
|
+
if (!h || h.length < 24) return;
|
|
329
|
+
const dv = new DataView(h.buffer, h.byteOffset, h.byteLength);
|
|
330
|
+
if (dv.getUint32(0, true) === TER_MAGIC && dv.getUint32(4, true) === 7) out.push({ ref: refs[i], id: dv.getUint32(20, true) });
|
|
331
|
+
});
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function terrainGroups(api, dat) {
|
|
335
|
+
let m = groupCache.get(api);
|
|
336
|
+
if (!m) {
|
|
337
|
+
m = /* @__PURE__ */ new Map();
|
|
338
|
+
groupCache.set(api, m);
|
|
339
|
+
}
|
|
340
|
+
let p = m.get(dat);
|
|
341
|
+
if (!p) {
|
|
342
|
+
p = (async () => {
|
|
343
|
+
const groups = [];
|
|
344
|
+
let cur = null;
|
|
345
|
+
for (const t of await terrainOf(api, api.datRefs(dat))) {
|
|
346
|
+
if (!cur || cur.terrain.has(t.id)) {
|
|
347
|
+
cur = { start: t.ref.fe.off, terrain: /* @__PURE__ */ new Map(), end: 0 };
|
|
348
|
+
groups.push(cur);
|
|
349
|
+
}
|
|
350
|
+
cur.terrain.set(t.id, t.ref);
|
|
351
|
+
cur.end = t.ref.fe.off;
|
|
352
|
+
}
|
|
353
|
+
return groups;
|
|
354
|
+
})();
|
|
355
|
+
m.set(dat, p);
|
|
356
|
+
}
|
|
357
|
+
return p;
|
|
358
|
+
}
|
|
359
|
+
function worldIndex(api, world) {
|
|
360
|
+
let m = indexCache.get(api);
|
|
361
|
+
if (!m) {
|
|
362
|
+
m = /* @__PURE__ */ new Map();
|
|
363
|
+
indexCache.set(api, m);
|
|
364
|
+
}
|
|
365
|
+
let p = m.get(world.name);
|
|
366
|
+
if (!p) {
|
|
367
|
+
p = (async () => {
|
|
368
|
+
let files = world.files();
|
|
369
|
+
const own = await terrainOf(api, files);
|
|
370
|
+
let terrain = new Map(own.map((t) => [t.id, t.ref]));
|
|
371
|
+
if (!terrain.size || terrain.size < own.length) {
|
|
372
|
+
const groups = await terrainGroups(api, world.dat);
|
|
373
|
+
const worlds = api.duniaWorlds().filter((w) => w.dat === world.dat).sort((x, y) => x.end - y.end);
|
|
374
|
+
const k = worlds.findIndex((w) => w.name === world.name);
|
|
375
|
+
if (groups.length === worlds.length && k >= 0) {
|
|
376
|
+
const g = groups[k], next = groups[k + 1]?.start ?? Infinity;
|
|
377
|
+
terrain = g.terrain;
|
|
378
|
+
files = api.datRefs(world.dat).filter((r) => r.fe.off >= g.start && r.fe.off < next);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
let terrainEnd = 0;
|
|
382
|
+
for (const r of terrain.values()) terrainEnd = Math.max(terrainEnd, r.fe.off);
|
|
383
|
+
return { files, terrain, terrainEnd };
|
|
384
|
+
})();
|
|
385
|
+
m.set(world.name, p);
|
|
386
|
+
}
|
|
387
|
+
return p;
|
|
388
|
+
}
|
|
389
|
+
function worldObjects(api, world, idx) {
|
|
390
|
+
idx.objects ??= (async () => {
|
|
391
|
+
const out = /* @__PURE__ */ new Map();
|
|
392
|
+
const scan = async (cand) => {
|
|
393
|
+
const CHUNK = 2048;
|
|
394
|
+
let empty = 0, found = 0;
|
|
395
|
+
for (let i = 0; i < cand.length && empty < 2; i += CHUNK) {
|
|
396
|
+
const part = cand.slice(i, i + CHUNK);
|
|
397
|
+
const heads = await mapLimit(part, 32, async (r) => {
|
|
398
|
+
try {
|
|
399
|
+
return await api.readRef(r, 64);
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
let here = 0;
|
|
405
|
+
heads.forEach((h, k) => {
|
|
406
|
+
const xy = h && worldSectorXY(h);
|
|
407
|
+
if (!xy) return;
|
|
408
|
+
const key = xy[1] * 65536 + xy[0];
|
|
409
|
+
let l = out.get(key);
|
|
410
|
+
if (!l) {
|
|
411
|
+
l = [];
|
|
412
|
+
out.set(key, l);
|
|
413
|
+
}
|
|
414
|
+
l.push(part[k]);
|
|
415
|
+
here++;
|
|
416
|
+
});
|
|
417
|
+
found += here;
|
|
418
|
+
empty = here || !found ? 0 : empty + 1;
|
|
419
|
+
}
|
|
420
|
+
return found;
|
|
421
|
+
};
|
|
422
|
+
const files = idx.files.filter((r) => r.fe.size >= 64);
|
|
423
|
+
const before = files.filter((r) => r.fe.off <= idx.terrainEnd && !isTerrainSize(r.fe.size)), after = files.filter((r) => r.fe.off > idx.terrainEnd);
|
|
424
|
+
if (before.length < SMALL_WORLD) {
|
|
425
|
+
await scan([...before, ...after]);
|
|
426
|
+
idx.full = true;
|
|
427
|
+
} else if (!await scan(after)) {
|
|
428
|
+
await scan(before);
|
|
429
|
+
idx.full = true;
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
})();
|
|
433
|
+
return idx.objects;
|
|
434
|
+
}
|
|
435
|
+
function worldSectorXY(h) {
|
|
436
|
+
if (h.length < 24) return null;
|
|
437
|
+
const dv = new DataView(h.buffer, h.byteOffset, h.byteLength);
|
|
438
|
+
if (dv.getUint32(0, true) !== 1178821230) return null;
|
|
439
|
+
let p = 16;
|
|
440
|
+
if (h[p] >= 254) p += 4;
|
|
441
|
+
p++;
|
|
442
|
+
if (dv.getUint32(p, true) !== H.WorldSector) return null;
|
|
443
|
+
p += 4;
|
|
444
|
+
let n = h[p++];
|
|
445
|
+
if (n >= 254) return null;
|
|
446
|
+
let x = -1, y = -1;
|
|
447
|
+
for (; n > 0 && p + 5 <= h.length; n--) {
|
|
448
|
+
const k = dv.getUint32(p, true), sz = h[p + 4];
|
|
449
|
+
p += 5;
|
|
450
|
+
if (sz >= 254 || p + sz > h.length) break;
|
|
451
|
+
if (sz === 4 && k === H.X) x = dv.getUint32(p, true);
|
|
452
|
+
if (sz === 4 && k === H.Y) y = dv.getUint32(p, true);
|
|
453
|
+
p += sz;
|
|
454
|
+
}
|
|
455
|
+
return x >= 0 && y >= 0 ? [x, y] : null;
|
|
456
|
+
}
|
|
457
|
+
async function loadDuniaLevel(text, main, ctx) {
|
|
458
|
+
const spec = JSON.parse(text.slice(LEVEL_MARK.length));
|
|
459
|
+
const arc = main.archive ?? ctx.archives.find((a) => duniaApi(a)) ?? null;
|
|
460
|
+
const api = duniaApi(arc);
|
|
461
|
+
const world = api?.duniaWorld(spec.world);
|
|
462
|
+
if (!arc || !api || !world) throw new Error("Уровень Far Cry 2 открывается из архивов игры (папка Data_Win32)");
|
|
463
|
+
const desc = worldDescriptor(await api.readRef(world.gameXml));
|
|
464
|
+
const width = desc.countX || spec.x0 + spec.w;
|
|
465
|
+
const idx = await worldIndex(api, world);
|
|
466
|
+
const sectors = /* @__PURE__ */ new Map();
|
|
467
|
+
const want = [];
|
|
468
|
+
for (let y = spec.y0; y < spec.y0 + spec.h; y++) for (let x = spec.x0; x < spec.x0 + spec.w; x++) {
|
|
469
|
+
const r = idx.terrain.get(y * width + x);
|
|
470
|
+
if (r) want.push(r);
|
|
471
|
+
}
|
|
472
|
+
await mapLimit(want, 8, async (r) => {
|
|
473
|
+
try {
|
|
474
|
+
const s = parseTerrainSector(await api.readRef(r));
|
|
475
|
+
sectors.set(s.id, s);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
const step = terrainStep(spec.w, spec.h);
|
|
480
|
+
const terrain = terrainMeshes(spec, sectors, width, desc.layers, step);
|
|
481
|
+
const water = waterMesh(spec, sectors, width, step);
|
|
482
|
+
if (water) terrain.push(water);
|
|
483
|
+
const B = new LevelBuilder();
|
|
484
|
+
const info = [
|
|
485
|
+
`Far Cry 2 (Dunia): ${spec.world} · ${spec.name} — сектора ${spec.x0}…${spec.x0 + spec.w - 1} × ${spec.y0}…${spec.y0 + spec.h - 1} (${spec.w * 64}×${spec.h * 64} м)`,
|
|
486
|
+
`Ландшафт: секторов ${sectors.size} из ${spec.w * spec.h}, сетка ${step} м, слоёв ${terrain.length}`
|
|
487
|
+
];
|
|
488
|
+
let shown = 0;
|
|
489
|
+
if (!spec.terrainOnly) {
|
|
490
|
+
const objs = await worldObjects(api, world, idx);
|
|
491
|
+
const refs = [];
|
|
492
|
+
const inReg = (x, y) => x >= spec.x0 && x < spec.x0 + spec.w && y >= spec.y0 && y < spec.y0 + spec.h;
|
|
493
|
+
for (let y = spec.y0; y < spec.y0 + spec.h; y++) for (let x = spec.x0; x < spec.x0 + spec.w; x++) refs.push(...objs.get(y * 65536 + x) ?? []);
|
|
494
|
+
if (!idx.full && want.length) {
|
|
495
|
+
const lo = Math.min(...want.map((r) => r.fe.off)), hi = Math.max(...want.map((r) => r.fe.off));
|
|
496
|
+
const cand = idx.files.filter((r) => r.fe.off > lo - 65536 && r.fe.off < hi + 65536 && r.fe.size >= 64 && !isTerrainSize(r.fe.size));
|
|
497
|
+
const heads = await mapLimit(cand, 32, async (r) => {
|
|
498
|
+
try {
|
|
499
|
+
return await api.readRef(r, 64);
|
|
500
|
+
} catch {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
heads.forEach((h, i) => {
|
|
505
|
+
const xy = h && worldSectorXY(h);
|
|
506
|
+
if (xy && inReg(xy[0], xy[1]) && !refs.includes(cand[i])) refs.push(cand[i]);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
shown = await placeEntities(arc, api, world.name, refs, B, (s) => info.push(s));
|
|
510
|
+
} else info.push("Весь мир — только ландшафт; объекты — на картах worlds/…/maps/*.fc2level");
|
|
511
|
+
const meshes = [...B.build(), ...terrain];
|
|
512
|
+
if (!meshes.length) throw new Error("В области уровня нет секторов");
|
|
513
|
+
if (!shown && !spec.terrainOnly) info.push("Объекты не найдены");
|
|
514
|
+
info.push(water ? `Вода: по уровню сектора (${[...new Set([...sectors.values()].map((x) => x.water).filter(Boolean))].slice(0, 5).join(", ")} м), полупрозрачная плоскость там, где земля ниже` : "Воды в области нет");
|
|
515
|
+
info.push("Не показаны: небо; деревья RealTree — приближённо");
|
|
516
|
+
return { meshes, instances: B.scene, skeleton: null, motions: [], bounds: boundsOf(meshes), info, defaultZUp: true };
|
|
517
|
+
}
|
|
518
|
+
async function placeEntities(arc, api, worldName, sectorRefs, B, note) {
|
|
519
|
+
let lib = /* @__PURE__ */ new Map();
|
|
520
|
+
const le = arc.lookup(`worlds/${worldName}/generated/entitylibrary.fcb`);
|
|
521
|
+
if (le) {
|
|
522
|
+
try {
|
|
523
|
+
lib = entityLibrary(parseFcb(await arc.read(le)));
|
|
524
|
+
} catch {
|
|
525
|
+
note("entitylibrary.fcb не прочитана");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const ents = [], unknown = /* @__PURE__ */ new Set();
|
|
529
|
+
let noModel = 0, sectorsWithObjects = 0, collected = 0;
|
|
530
|
+
const markers = [];
|
|
531
|
+
await mapLimit(sectorRefs, 8, async (r) => {
|
|
532
|
+
let root;
|
|
533
|
+
try {
|
|
534
|
+
root = parseFcb(await api.readRef(r));
|
|
535
|
+
} catch {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const s = sectorEntities(root, lib);
|
|
539
|
+
if (s.list.length) sectorsWithObjects++;
|
|
540
|
+
ents.push(...s.list);
|
|
541
|
+
noModel += s.noModel;
|
|
542
|
+
markers.push(...s.markers);
|
|
543
|
+
for (const c of sectorCollections(root)) {
|
|
544
|
+
const name = api.nameOfHash(c.hash);
|
|
545
|
+
ents.push({ name: name?.split("\\").pop()?.replace(/\.xbg$/i, "") ?? `#${c.hash.toString(16)}`, model: `#${c.hash.toString(16).padStart(8, "0")}`, parts: [], pos: c.pos, angles: [0, 0, c.yaw], layer: "collections" });
|
|
546
|
+
collected++;
|
|
547
|
+
}
|
|
548
|
+
for (const a of s.unknownArchetype) unknown.add(a);
|
|
549
|
+
});
|
|
550
|
+
ents.sort((a, b) => a.name.localeCompare(b.name) || a.pos[0] - b.pos[0] || a.pos[1] - b.pos[1] || a.pos[2] - b.pos[2]);
|
|
551
|
+
const counts = /* @__PURE__ */ new Map(), colCounts = /* @__PURE__ */ new Map();
|
|
552
|
+
for (const e of ents) {
|
|
553
|
+
const m = e.layer === "collections" ? colCounts : counts;
|
|
554
|
+
m.set(e.model.toLowerCase(), (m.get(e.model.toLowerCase()) ?? 0) + 1);
|
|
555
|
+
}
|
|
556
|
+
const parsedModels = /* @__PURE__ */ new Map();
|
|
557
|
+
const realtrees = /* @__PURE__ */ new Set(), rtMeshes = /* @__PURE__ */ new Map();
|
|
558
|
+
const realTreeOf = async (t) => {
|
|
559
|
+
const mats = await Promise.all(t.materials.map(async (m) => {
|
|
560
|
+
const e = arc.lookup(m.replace(/\.mlm$/i, ".xbm"));
|
|
561
|
+
try {
|
|
562
|
+
return e ? { path: m, x: parseXbm(await arc.read(e)) } : null;
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}));
|
|
567
|
+
const slot = (x, ...keys) => {
|
|
568
|
+
for (const k of keys) if (x?.textures[k]) return x.textures[k];
|
|
569
|
+
return void 0;
|
|
570
|
+
};
|
|
571
|
+
const pick2 = (trunk) => {
|
|
572
|
+
const m = mats.find((q) => q && /trunk|bark/i.test(q.x.shader + q.x.name) === trunk) ?? mats[trunk ? 0 : 1] ?? null;
|
|
573
|
+
return { material: m?.path, texture: slot(m?.x, "DiffuseTexture1", "DiffuseTexture") ?? "", normal: slot(m?.x, "NormalTexture1", "NormalTexture") };
|
|
574
|
+
};
|
|
575
|
+
return realTreeMeshes(t, pick2(true), pick2(false));
|
|
576
|
+
};
|
|
577
|
+
const displayName = (k) => k.startsWith("#") ? api.nameOfHash(parseInt(k.slice(1), 16)) ?? k : k;
|
|
578
|
+
const readModel = async (k) => {
|
|
579
|
+
if (k.startsWith("#")) {
|
|
580
|
+
const r = api.refByHash(parseInt(k.slice(1), 16));
|
|
581
|
+
return r ? api.readRef(r) : null;
|
|
582
|
+
}
|
|
583
|
+
const e = arc.lookup(k);
|
|
584
|
+
return e ? arc.read(e) : null;
|
|
585
|
+
};
|
|
586
|
+
await mapLimit([.../* @__PURE__ */ new Set([...counts.keys(), ...colCounts.keys()])], 8, async (k) => {
|
|
587
|
+
try {
|
|
588
|
+
const u = await readModel(k);
|
|
589
|
+
if (!u) {
|
|
590
|
+
parsedModels.set(k, null);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (String.fromCharCode(u[0], u[1], u[2], u[3]) !== "HSEM") {
|
|
594
|
+
parsedModels.set(k, null);
|
|
595
|
+
if (isRealTree(u)) {
|
|
596
|
+
realtrees.add(k);
|
|
597
|
+
try {
|
|
598
|
+
rtMeshes.set(k, await realTreeOf(parseRealTree(u)));
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
parsedModels.set(k, { u, x: parseXbg(u) });
|
|
605
|
+
} catch {
|
|
606
|
+
parsedModels.set(k, null);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const lodOf = /* @__PURE__ */ new Map();
|
|
610
|
+
const trisOf = (k, l) => {
|
|
611
|
+
const rt = rtMeshes.get(k);
|
|
612
|
+
if (rt) return rt.reduce((n2, m) => n2 + m.indices.length / 3, 0);
|
|
613
|
+
const p = parsedModels.get(k), L = p?.x.lods[Math.min(l, (p?.x.lods.length ?? 1) - 1)];
|
|
614
|
+
if (!p || !L) return 0;
|
|
615
|
+
let n = 0;
|
|
616
|
+
L.prims.forEach((pr, i) => {
|
|
617
|
+
const name = p.x.parts[pr.part]?.name ?? "";
|
|
618
|
+
if (!/STATE0*[2-9]$|_N_/i.test(name.replace(/_LOD\d+$/i, ""))) n += ((L.prims[i + 1]?.firstIndex ?? L.icount) - pr.firstIndex) / 3;
|
|
619
|
+
});
|
|
620
|
+
return n;
|
|
621
|
+
};
|
|
622
|
+
let total = 0;
|
|
623
|
+
for (const [k, n] of counts) total += n * trisOf(k, 0);
|
|
624
|
+
const lodNote = total;
|
|
625
|
+
if (total > TRI_BUDGET) {
|
|
626
|
+
const order = [...counts].sort((a, b) => b[1] * trisOf(b[0], 0) - a[1] * trisOf(a[0], 0));
|
|
627
|
+
for (const [k, n] of order) {
|
|
628
|
+
if (total <= TRI_BUDGET) break;
|
|
629
|
+
const p = parsedModels.get(k);
|
|
630
|
+
if (!p || p.x.lods.length < 2) continue;
|
|
631
|
+
const far = Math.min(2, p.x.lods.length - 1);
|
|
632
|
+
total -= n * (trisOf(k, 0) - trisOf(k, far));
|
|
633
|
+
lodOf.set(k, far);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const colLod = (k) => Math.min(COLLECTION_LOD, (parsedModels.get(k)?.x.lods.length ?? 1) - 1);
|
|
637
|
+
const colKeepOf = /* @__PURE__ */ new Map();
|
|
638
|
+
let colTotal = 0;
|
|
639
|
+
{
|
|
640
|
+
const rows = [...colCounts].map(([k, n]) => ({ k, t: n * trisOf(k, colLod(k)) })).filter((r) => r.t > 0).sort((a, b) => a.t - b.t);
|
|
641
|
+
let left = COLLECTION_BUDGET;
|
|
642
|
+
rows.forEach((r, i) => {
|
|
643
|
+
colTotal += r.t;
|
|
644
|
+
const keep = Math.min(1, left / (rows.length - i) / r.t);
|
|
645
|
+
colKeepOf.set(r.k, keep);
|
|
646
|
+
left -= keep * r.t;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const matCache = /* @__PURE__ */ new Map();
|
|
650
|
+
const protos = /* @__PURE__ */ new Map();
|
|
651
|
+
const missing = /* @__PURE__ */ new Set();
|
|
652
|
+
let skinned = 0;
|
|
653
|
+
const protoOf = (model, parts, lod) => {
|
|
654
|
+
const mk = model.toLowerCase(), key = `${mk}|${lod}|${parts.join(";")}`;
|
|
655
|
+
let pr = protos.get(key);
|
|
656
|
+
if (!pr) {
|
|
657
|
+
pr = (async () => {
|
|
658
|
+
const rt = rtMeshes.get(mk);
|
|
659
|
+
if (rt?.length) return { pi: B.proto(displayName(model), rt, "static", (m) => m.name), parts: rt };
|
|
660
|
+
const p = parsedModels.get(mk);
|
|
661
|
+
if (!p) {
|
|
662
|
+
if (!realtrees.has(mk)) missing.add(model);
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const r = await xbgMeshes(p.u, arc, matCache, lod, p.x);
|
|
667
|
+
const all = r.skinned ? bakeBindPose(r.x, r.meshes) : r.meshes;
|
|
668
|
+
if (r.skinned) skinned++;
|
|
669
|
+
const want = new Set(parts.map((q) => q.toUpperCase()));
|
|
670
|
+
const kit = want.size ? /* @__PURE__ */ new Set() : kitHiddenParts(all.map((m) => m.name.split(" · ")[0]));
|
|
671
|
+
const list = all.filter((m) => {
|
|
672
|
+
const part = m.name.split(" · ")[0];
|
|
673
|
+
return !m.defaultHidden && !kit.has(part) && (!want.size || want.has(part.toUpperCase()));
|
|
674
|
+
});
|
|
675
|
+
if (!list.length) return null;
|
|
676
|
+
const label = (m) => m.material?.split("\\").pop() || m.texture.split("\\").pop() || "?";
|
|
677
|
+
return { pi: B.proto(displayName(model), list, "static", label), parts: list };
|
|
678
|
+
} catch {
|
|
679
|
+
missing.add(model);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
})();
|
|
683
|
+
protos.set(key, pr);
|
|
684
|
+
}
|
|
685
|
+
return pr;
|
|
686
|
+
};
|
|
687
|
+
let placed = 0, placedCol = 0, treeInst = 0;
|
|
688
|
+
const treeMarks = [];
|
|
689
|
+
const pick = rngOf(24301);
|
|
690
|
+
let thinned = 0;
|
|
691
|
+
for (const e of ents) {
|
|
692
|
+
const col = e.layer === "collections", mk = e.model.toLowerCase();
|
|
693
|
+
if (col && (colKeepOf.get(mk) ?? 1) < 1 && pick() >= colKeepOf.get(mk)) {
|
|
694
|
+
thinned++;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const pr = await protoOf(e.model, e.parts, col ? colLod(mk) : lodOf.get(mk) ?? 0);
|
|
698
|
+
if (!pr) {
|
|
699
|
+
if (realtrees.has(e.model.toLowerCase())) {
|
|
700
|
+
treeInst++;
|
|
701
|
+
treeMarks.push({ name: e.name, pos: e.pos });
|
|
702
|
+
}
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const M = new Float64Array(16);
|
|
706
|
+
compose(e.pos, anglesQuat(e.angles), [1, 1, 1], M);
|
|
707
|
+
const path = displayName(e.model);
|
|
708
|
+
const folder = path.startsWith("#") ? "collections" : path.replace(/\\/g, "/").split("/").slice(0, -1).join("/") || "graphics";
|
|
709
|
+
B.place(pr.pi, pr.parts, M, e.name, e.layer === "collections" ? `collections/${folder}` : folder);
|
|
710
|
+
placed++;
|
|
711
|
+
if (e.layer === "collections") placedCol++;
|
|
712
|
+
}
|
|
713
|
+
const mark = (list, label, color, size, layer) => {
|
|
714
|
+
if (!list.length) return;
|
|
715
|
+
const m = markerMesh(label, size, color), pi = B.proto(label, [m], "marker", () => label, true);
|
|
716
|
+
for (const x of list) {
|
|
717
|
+
const M = new Float64Array(16);
|
|
718
|
+
compose(x.pos, [1, 0, 0, 0], [1, 1, 1], M);
|
|
719
|
+
B.place(pi, [m], M, x.name, layer);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
mark(markers.filter((x) => x.kind === "particle"), "Метки · частицы", [0.3, 0.8, 1], 0.3, "particle");
|
|
723
|
+
mark(markers.filter((x) => x.kind === "light"), "Метки · свет", [1, 0.85, 0.3], 0.3, "light");
|
|
724
|
+
mark(treeMarks, "Метки · деревья RealTree (не разобраны)", [0.25, 0.75, 0.2], 1.5, "realtree");
|
|
725
|
+
note(`Объекты: ${placed} из ${ents.length} с моделью (секторов с объектами ${sectorsWithObjects}, файлов секторов ${sectorRefs.length}; моделей ${[...protos.keys()].length}, треугольников ${B.tris})`);
|
|
726
|
+
if (lodOf.size) note(`Дальний LOD у ${lodOf.size} моделей (ближние LOD всех объектов — ${Math.round(lodNote / 1e6 * 10) / 10} млн треугольников, бюджет ${TRI_BUDGET / 1e6} млн)`);
|
|
727
|
+
if (collected) note(`Коллекции (трава, кусты, камни, мусор — кисть редактора), LOD${COLLECTION_LOD}: ${placedCol} из ${collected}${thinned ? ` (массовые модели прорежены: всего ${Math.round(colTotal / 1e5) / 10} млн треугольников, бюджет ${COLLECTION_BUDGET / 1e6} млн)` : ""}${realtrees.size ? `; деревья и кусты RealTree (.rtr): моделей ${realtrees.size}, приближённо — ветви трубками по узлам, листва карточками у концов ветвей (сетку игра строит сама)${treeInst ? `, не разобрано ${treeInst} экземпляров` : ""}` : ""}`);
|
|
728
|
+
if (noModel) note(`Без модели (логика, точки, звук, частицы, свет): ${noModel}`);
|
|
729
|
+
if (markers.length || treeMarks.length) note(`Метки (скрыты, включаются в списке мешей): частицы ${markers.filter((x) => x.kind === "particle").length}, свет ${markers.filter((x) => x.kind === "light").length}, деревья RealTree ${treeMarks.length}`);
|
|
730
|
+
if (skinned) note(`Модели со скелетом (люди, животные) — в bind-позе: ${skinned}`);
|
|
731
|
+
if (missing.size) note(`Модели не найдены: ${missing.size} (${[...missing].slice(0, 3).join(", ")}${missing.size > 3 ? "…" : ""})`);
|
|
732
|
+
if (unknown.size) note(`Архетипы не найдены в entitylibrary.fcb: ${unknown.size} (${[...unknown].slice(0, 2).join(", ")})`);
|
|
733
|
+
return placed;
|
|
734
|
+
}
|
|
735
|
+
export {
|
|
736
|
+
anglesQuat,
|
|
737
|
+
entityLibrary,
|
|
738
|
+
loadDuniaLevel,
|
|
739
|
+
parseTerrainSector,
|
|
740
|
+
sectorCollections,
|
|
741
|
+
sectorEntities,
|
|
742
|
+
terrainMeshes,
|
|
743
|
+
terrainStep,
|
|
744
|
+
vertexLayer,
|
|
745
|
+
waterMesh,
|
|
746
|
+
worldDescriptor,
|
|
747
|
+
worldIndex,
|
|
748
|
+
worldObjects,
|
|
749
|
+
worldSectorXY
|
|
750
|
+
};
|
|
751
|
+
//# sourceMappingURL=level.js.map
|