@gmvjs/xray-v1 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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/db.d.ts +12 -0
  4. package/dist/db.js +134 -0
  5. package/dist/db.js.map +1 -0
  6. package/dist/details.d.ts +42 -0
  7. package/dist/details.js +210 -0
  8. package/dist/details.js.map +1 -0
  9. package/dist/export.d.ts +38 -0
  10. package/dist/export.js +241 -0
  11. package/dist/export.js.map +1 -0
  12. package/dist/games/cop.d.ts +7 -0
  13. package/dist/games/cop.js +11 -0
  14. package/dist/games/cop.js.map +1 -0
  15. package/dist/games/cs.d.ts +7 -0
  16. package/dist/games/cs.js +11 -0
  17. package/dist/games/cs.js.map +1 -0
  18. package/dist/games/sdk.d.ts +7 -0
  19. package/dist/games/sdk.js +10 -0
  20. package/dist/games/sdk.js.map +1 -0
  21. package/dist/games/soc.d.ts +9 -0
  22. package/dist/games/soc.js +13 -0
  23. package/dist/games/soc.js.map +1 -0
  24. package/dist/i18n.d.ts +2 -0
  25. package/dist/i18n.js +48 -0
  26. package/dist/i18n.js.map +1 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.js +131 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/level.d.ts +39 -0
  31. package/dist/level.js +234 -0
  32. package/dist/level.js.map +1 -0
  33. package/dist/load.d.ts +5 -0
  34. package/dist/load.js +126 -0
  35. package/dist/load.js.map +1 -0
  36. package/dist/lzhuf.d.ts +2 -0
  37. package/dist/lzhuf.js +136 -0
  38. package/dist/lzhuf.js.map +1 -0
  39. package/dist/motion.d.ts +28 -0
  40. package/dist/motion.js +20 -0
  41. package/dist/motion.js.map +1 -0
  42. package/dist/object.d.ts +138 -0
  43. package/dist/object.js +361 -0
  44. package/dist/object.js.map +1 -0
  45. package/dist/ogf.d.ts +93 -0
  46. package/dist/ogf.js +418 -0
  47. package/dist/ogf.js.map +1 -0
  48. package/dist/scene.d.ts +28 -0
  49. package/dist/scene.js +141 -0
  50. package/dist/scene.js.map +1 -0
  51. package/dist/skeleton.d.ts +34 -0
  52. package/dist/skeleton.js +252 -0
  53. package/dist/skeleton.js.map +1 -0
  54. package/dist/smotion.d.ts +44 -0
  55. package/dist/smotion.js +283 -0
  56. package/dist/smotion.js.map +1 -0
  57. package/dist/soc-key.d.ts +18 -0
  58. package/dist/soc-key.js +125 -0
  59. package/dist/soc-key.js.map +1 -0
  60. package/dist/thm.d.ts +26 -0
  61. package/dist/thm.js +69 -0
  62. package/dist/thm.js.map +1 -0
  63. package/dist/write-object.d.ts +31 -0
  64. package/dist/write-object.js +228 -0
  65. package/dist/write-object.js.map +1 -0
  66. package/dist/write-omf.d.ts +47 -0
  67. package/dist/write-scene.d.ts +26 -0
  68. package/dist/write-scene.js +114 -0
  69. package/dist/write-scene.js.map +1 -0
  70. package/package.json +70 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Shibisty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @gmvjs/xray-v1 — X-Ray Engine 1.x
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
4
+
5
+ X-Ray Engine 1.x (S.T.A.L.K.E.R.: 1.0 Shadow of Chernobyl, 1.5 Clear Sky, 1.6 Call of Pripyat; the parser tells the versions apart itself): OGF v4 models with skeletons and animations (embedded and external `.omf`, body parts), `.db`/`.xdb` archives (including Shadow of Chernobyl's encrypted `gamedata.db*`), compiled levels with grass from `level.details`, `.dm` details, SDK sources (`.object`, `.skl/.skls`, `.level` scenes) and export to X-Ray SDK 0.7.
6
+
7
+ An engine plugin for [`@gmvjs/core`](../core/README.md): formats, archives and game presets. Heavy parsers are loaded on demand (`import()` inside `load`), so adding the package costs almost nothing until a file of this engine is opened.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm i @gmvjs/core @gmvjs/xray-v1 @gmvjs/codecs
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ The whole engine (all games):
18
+
19
+ ```ts
20
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
21
+ import { xrayV1Engine } from '@gmvjs/xray-v1';
22
+
23
+ const session = new ViewerSession(createRegistry([xrayV1Engine]));
24
+ ```
25
+
26
+ One game: a package subpath (the game preset and an engine limited to that game):
27
+
28
+ ```ts
29
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
30
+ import soc from '@gmvjs/xray-v1/soc';
31
+
32
+ const session = new ViewerSession(createRegistry([soc.engine]), soc.preset.id);
33
+ ```
34
+
35
+ ## Games (subpaths)
36
+
37
+ | Import | Preset | Game | Status | Only for this game |
38
+ |---|---|---|---|---|
39
+ | `@gmvjs/xray-v1/soc` | `stalker-soc` | S.T.A.L.K.E.R.: Shadow of Chernobyl | 🟡 beta | `soc-key.ts`; the `gamedata.db*` file table key is looked up in the user's `bin/XR_3DA.exe`; the package contains no keys |
40
+ | `@gmvjs/xray-v1/cs` | `stalker-cs` | S.T.A.L.K.E.R.: Clear Sky | 🟡 beta | — |
41
+ | `@gmvjs/xray-v1/cop` | `stalker-cop` | S.T.A.L.K.E.R.: Call of Pripyat | 🟡 beta | — |
42
+ | `@gmvjs/xray-v1/sdk` | — | export to X-Ray SDK 0.7 (`ExporterPlugin` `xray-sdk`): `.object`, `.level` + `scene_object.part`, DDS + `.thm`; also used for Metro maps | — | `export.ts`, `write-object.ts`, `write-scene.ts` |
43
+
44
+ ## Formats
45
+
46
+ | id | What | Files | Status |
47
+ |---|---|---|---|
48
+ | `xray-ogf` | X-Ray OGF | `.ogf` + `.omf`, `.skl`, `.skls` animations | 🟡 beta |
49
+ | `xray-object` | X-Ray SDK .object | `.object` + `.omf`, `.skl`, `.skls` animations | 🟡 beta |
50
+ | `xray-level` | X-Ray: compiled level (level + level.geom) | by path `/(^|\/)levels\/[^/]+\/level$/` | 🟡 beta |
51
+ | `xray-dm` | X-Ray: detail (.dm: grass, bushes) | `.dm` | 🟡 beta |
52
+ | `xray-scene` | X-Ray SDK: level (.level + scene_object.part) | `.level` | 🟡 beta |
53
+
54
+ ## Archives
55
+
56
+ - `xray-db` — X-Ray .db / .xdb (Clear Sky, Call of Pripyat) (`.db`, `.xdb`, `.db0`, `.xdb0`, `.db1`, `.xdb1`, `.db2`, `.xdb2`…)
57
+
58
+ ## Export
59
+
60
+ - `xray-sdk` — S.T.A.L.K.E.R.: X-Ray SDK 0.7 sources (subpath `@gmvjs/xray-v1/sdk`)
61
+
62
+ ## Dependencies
63
+
64
+ - `@gmvjs/core` — plugin API, types, ZIP/folder archives, zlib, DDS/BC, geometry
65
+ - `@gmvjs/codecs` — LZO
66
+
67
+ ## Not done yet
68
+
69
+ - OGF older than v4 and binary `.level` files of old SDKs (no samples)
70
+ - SDK: checking in LevelEditor, mapping materials to shaders/gamemtl, writing OGF/OMF
71
+
72
+ ## License
73
+
74
+ MIT
75
+
76
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
77
+
78
+ If this project helps you, consider supporting its development on Patreon ❤️
package/dist/db.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Archive, ArchiveEntry, BlobFile } from '@gmvjs/core/types';
2
+ interface DbEntry extends ArchiveEntry {
3
+ off: number;
4
+ real: number;
5
+ packed: number;
6
+ }
7
+ /** Parse the (unpacked) file table. */
8
+ export declare function parseDbTable(t: Uint8Array): DbEntry[];
9
+ /** Forget found keys, including stored ones (tests, "reset"). */
10
+ export declare const resetSocKeys: () => void;
11
+ export declare function openXrayDb(file: BlobFile, siblings?: readonly BlobFile[]): Promise<Archive>;
12
+ export {};
package/dist/db.js ADDED
@@ -0,0 +1,134 @@
1
+ import { normPath } from "@gmvjs/core/binary";
2
+ import { readRange, createArchive } from "@gmvjs/core/archives/common";
3
+ import { lzhufDecompress } from "./lzhuf.js";
4
+ import { lzo1xDecompress } from "@gmvjs/codecs";
5
+ const CHUNK_HEADER = 1, COMPRESSED = 2147483648;
6
+ async function findTable(file) {
7
+ let p = 0;
8
+ for (let n = 0; n < 64 && p + 8 <= file.size; n++) {
9
+ const h = await readRange(file, p, p + 8), dv = new DataView(h.buffer, h.byteOffset, 8);
10
+ const id = dv.getUint32(0, true), size = dv.getUint32(4, true);
11
+ if (p + 8 + size > file.size) return null;
12
+ if ((id & ~COMPRESSED) >>> 0 === CHUNK_HEADER) return { id, off: p + 8, size };
13
+ p += 8 + size;
14
+ }
15
+ return null;
16
+ }
17
+ function parseDbTable(t) {
18
+ const dv = new DataView(t.buffer, t.byteOffset, t.byteLength), out = [];
19
+ let q = 0;
20
+ while (q + 2 <= t.length) {
21
+ const bs = dv.getUint16(q, true);
22
+ if (bs < 16 || q + 2 + bs > t.length) throw new Error(`таблица файлов повреждена (смещение ${q})`);
23
+ const real = dv.getUint32(q + 2, true), packed = dv.getUint32(q + 6, true);
24
+ let name = "";
25
+ for (let k = q + 14; k < q + 2 + bs - 4; k++) name += String.fromCharCode(t[k]);
26
+ const off = dv.getUint32(q + 2 + bs - 4, true);
27
+ q += 2 + bs;
28
+ if (!real && !packed) continue;
29
+ out.push({ path: normPath(name), size: real, off, real, packed });
30
+ }
31
+ return out;
32
+ }
33
+ const socKeys = /* @__PURE__ */ new Map();
34
+ const STORE = "gmv.xray.socKeys";
35
+ function loadStored() {
36
+ try {
37
+ const list = JSON.parse(globalThis.localStorage?.getItem(STORE) ?? "[]");
38
+ for (const [id, k] of list) if (k && Number.isFinite(k.tableSeed) && Number.isFinite(k.encryptSeed) && Number.isFinite(k.iterations)) socKeys.set(id, k);
39
+ } catch {
40
+ }
41
+ }
42
+ function saveStored() {
43
+ try {
44
+ globalThis.localStorage?.setItem(STORE, JSON.stringify([...socKeys]));
45
+ } catch {
46
+ }
47
+ }
48
+ let storedLoaded = false;
49
+ const resetSocKeys = () => {
50
+ socKeys.clear();
51
+ storedLoaded = true;
52
+ try {
53
+ globalThis.localStorage?.removeItem(STORE);
54
+ } catch {
55
+ }
56
+ };
57
+ function tableEntries(table, fileSize) {
58
+ try {
59
+ const e = parseDbTable(table);
60
+ return e.length && !e.some((x) => x.off + x.packed > fileSize) ? e : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function openXrayDb(file, siblings = []) {
66
+ const tab = await findTable(file);
67
+ if (!tab) throw new Error(`${file.name}: не архив X-Ray (нет таблицы файлов)`);
68
+ const raw = await readRange(file, tab.off, tab.off + tab.size);
69
+ let entries = null;
70
+ try {
71
+ entries = tableEntries(tab.id & COMPRESSED ? lzhufDecompress(raw) : raw, file.size);
72
+ } catch {
73
+ entries = null;
74
+ }
75
+ if (!entries && tab.id & COMPRESSED) {
76
+ const { findSocKey, socDecode } = await import("./soc-key.js");
77
+ const check = (t) => !!tableEntries(t, file.size);
78
+ if (!storedLoaded) {
79
+ storedLoaded = true;
80
+ loadStored();
81
+ }
82
+ for (const key of socKeys.values()) {
83
+ try {
84
+ entries = tableEntries(lzhufDecompress(socDecode(key, raw)), file.size);
85
+ } catch {
86
+ entries = null;
87
+ }
88
+ if (entries) break;
89
+ }
90
+ const exes = siblings.filter((f) => /\.exe$/i.test(f.name)).sort((a, b) => +!/xr_3da/i.test(a.name) - +!/xr_3da/i.test(b.name));
91
+ const tried = [];
92
+ for (const exe of entries ? [] : exes) {
93
+ const id = `${exe.name}:${exe.size}`;
94
+ if (socKeys.has(id)) continue;
95
+ let key = null;
96
+ try {
97
+ key = findSocKey(new Uint8Array(await exe.arrayBuffer()), raw, check);
98
+ } catch (e) {
99
+ tried.push(`${exe.webkitRelativePath || exe.name}: ${e.message}`);
100
+ continue;
101
+ }
102
+ if (!key) {
103
+ tried.push(`${exe.webkitRelativePath || exe.name} (${exe.size} Б) — ключ не найден`);
104
+ continue;
105
+ }
106
+ socKeys.set(id, key);
107
+ saveStored();
108
+ entries = tableEntries(lzhufDecompress(socDecode(key, raw)), file.size);
109
+ if (entries) break;
110
+ }
111
+ if (!entries)
112
+ throw new Error(`${file.name}: таблица файлов зашифрована (архивы «Тени Чернобыля»). Откройте папку игры целиком («Папка игры…») или выберите кнопкой «Открыть .db…» сначала bin\\XR_3DA.exe, затем архивы — ключ берётся из него. ${tried.length ? `Проверены: ${tried.join("; ")}.` : `Исполняемых файлов игры среди выбранных нет (выбрано файлов: ${siblings.length}).`}`);
113
+ }
114
+ if (!entries) throw new Error(`${file.name}: таблица файлов повреждена.`);
115
+ return createArchive({
116
+ name: file.name,
117
+ kind: "xray-db",
118
+ version: 0,
119
+ entries,
120
+ async read(e) {
121
+ const u = await readRange(file, e.off, e.off + e.packed);
122
+ if (e.packed === e.real) return u;
123
+ const out = new Uint8Array(e.real);
124
+ lzo1xDecompress(u, 0, out);
125
+ return out;
126
+ }
127
+ });
128
+ }
129
+ export {
130
+ openXrayDb,
131
+ parseDbTable,
132
+ resetSocKeys
133
+ };
134
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sources":["../src/db.ts"],"sourcesContent":["// X-Ray archives (.db, .db0…, .xdb): Clear Sky, Call of Pripyat and mods. A file is a chain of chunks [id u32, size u32]:\n// 0x29A = the text [header] (CS/CoP), 0 = data, 1 = the file table (flag 0x80000000 = LZHUF-compressed).\n// Table record: u16 length, then size, compressed size, CRC, name (no zero), offset in the archive.\n// Compressed files (size ≠ compressed size) use LZO1X. Shadow of Chernobyl encrypts the file table; such archives do not open without the key.\nimport { normPath } from '@gmvjs/core/binary';\nimport { createArchive, readRange } from '@gmvjs/core/archives/common';\nimport { lzhufDecompress } from './lzhuf';\nimport { lzo1xDecompress } from '@gmvjs/codecs';\nimport type { Archive, ArchiveEntry, BlobFile } from '@gmvjs/core/types';\nimport type { SocKey } from './soc-key';\n\ninterface DbEntry extends ArchiveEntry { off: number; real: number; packed: number }\n\nconst CHUNK_HEADER = 1, COMPRESSED = 0x80000000;\n\n/** Find the file table chunk by walking the chunk headers. */\nasync function findTable(file: BlobFile): Promise<{ id: number; off: number; size: number } | null> {\n let p = 0;\n for (let n = 0; n < 64 && p + 8 <= file.size; n++) {\n const h = await readRange(file, p, p + 8), dv = new DataView(h.buffer, h.byteOffset, 8);\n const id = dv.getUint32(0, true), size = dv.getUint32(4, true);\n if (p + 8 + size > file.size) return null;\n if ((id & ~COMPRESSED) >>> 0 === CHUNK_HEADER) return { id, off: p + 8, size };\n p += 8 + size;\n }\n return null;\n}\n\n/** Parse the (unpacked) file table. */\nexport function parseDbTable(t: Uint8Array): DbEntry[] {\n const dv = new DataView(t.buffer, t.byteOffset, t.byteLength), out: DbEntry[] = [];\n let q = 0;\n while (q + 2 <= t.length) {\n const bs = dv.getUint16(q, true);\n if (bs < 16 || q + 2 + bs > t.length) throw new Error(`таблица файлов повреждена (смещение ${q})`);\n const real = dv.getUint32(q + 2, true), packed = dv.getUint32(q + 6, true);\n let name = '';\n for (let k = q + 14; k < q + 2 + bs - 4; k++) name += String.fromCharCode(t[k]!);\n const off = dv.getUint32(q + 2 + bs - 4, true);\n q += 2 + bs;\n if (!real && !packed) continue; // folder\n out.push({ path: normPath(name), size: real, off, real, packed });\n }\n return out;\n}\n\n/** Shadow of Chernobyl keys found in the user's executables (by file name and size), kept while the page is open. */\nconst socKeys = new Map<string, SocKey>();\n/**\n * Found keys are also stored in the page's localStorage (only on the user's computer): once a key has been\n * found in their XR_3DA.exe, SoC archives open without the exe. Keys are never included in the extension build.\n */\nconst STORE = 'gmv.xray.socKeys';\nfunction loadStored(): void {\n try {\n const list = JSON.parse(globalThis.localStorage?.getItem(STORE) ?? '[]') as Array<[string, SocKey]>;\n for (const [id, k] of list) if (k && Number.isFinite(k.tableSeed) && Number.isFinite(k.encryptSeed) && Number.isFinite(k.iterations)) socKeys.set(id, k);\n } catch { /* storage unavailable: keys live only while the page is open */ }\n}\nfunction saveStored(): void {\n try { globalThis.localStorage?.setItem(STORE, JSON.stringify([...socKeys])); } catch { /* unavailable */ }\n}\nlet storedLoaded = false;\n/** Forget found keys, including stored ones (tests, \"reset\"). */\nexport const resetSocKeys = (): void => { socKeys.clear(); storedLoaded = true; try { globalThis.localStorage?.removeItem(STORE); } catch { /* unavailable */ } };\n\n/** Parse the table: records exist and lie inside the archive. */\nfunction tableEntries(table: Uint8Array, fileSize: number): DbEntry[] | null {\n try { const e = parseDbTable(table); return e.length && !e.some((x) => x.off + x.packed > fileSize) ? e : null; } catch { return null; }\n}\n\nexport async function openXrayDb(file: BlobFile, siblings: readonly BlobFile[] = []): Promise<Archive> {\n const tab = await findTable(file);\n if (!tab) throw new Error(`${file.name}: не архив X-Ray (нет таблицы файлов)`);\n const raw = await readRange(file, tab.off, tab.off + tab.size);\n let entries: DbEntry[] | null = null;\n try { entries = tableEntries(tab.id & COMPRESSED ? lzhufDecompress(raw) : raw, file.size); } catch { entries = null; }\n if (!entries && tab.id & COMPRESSED) {\n // Shadow of Chernobyl: the table is encrypted; the key is searched for in the XR_3DA.exe selected together with the archives (or in the game folder)\n const { findSocKey, socDecode } = await import('./soc-key');\n const check = (t: Uint8Array) => !!tableEntries(t, file.size);\n if (!storedLoaded) { storedLoaded = true; loadStored(); }\n for (const key of socKeys.values()) {\n try { entries = tableEntries(lzhufDecompress(socDecode(key, raw)), file.size); } catch { entries = null; }\n if (entries) break;\n }\n const exes = siblings.filter((f) => /\\.exe$/i.test(f.name)).sort((a, b) => +!/xr_3da/i.test(a.name) - +!/xr_3da/i.test(b.name));\n const tried: string[] = [];\n for (const exe of entries ? [] : exes) {\n const id = `${exe.name}:${exe.size}`;\n if (socKeys.has(id)) continue;\n let key: SocKey | null = null;\n try { key = findSocKey(new Uint8Array(await exe.arrayBuffer()), raw, check); }\n catch (e) { tried.push(`${exe.webkitRelativePath || exe.name}: ${(e as Error).message}`); continue; }\n if (!key) { tried.push(`${exe.webkitRelativePath || exe.name} (${exe.size} Б) — ключ не найден`); continue; }\n socKeys.set(id, key);\n saveStored();\n entries = tableEntries(lzhufDecompress(socDecode(key, raw)), file.size);\n if (entries) break;\n }\n if (!entries)\n throw new Error(`${file.name}: таблица файлов зашифрована (архивы «Тени Чернобыля»). Откройте папку игры целиком («Папка игры…») или выберите кнопкой «Открыть .db…» сначала bin\\\\XR_3DA.exe, затем архивы — ключ берётся из него. ${tried.length ? `Проверены: ${tried.join('; ')}.` : `Исполняемых файлов игры среди выбранных нет (выбрано файлов: ${siblings.length}).`}`);\n }\n if (!entries) throw new Error(`${file.name}: таблица файлов повреждена.`);\n return createArchive<DbEntry>({\n name: file.name,\n kind: 'xray-db',\n version: 0,\n entries,\n async read(e) {\n const u = await readRange(file, e.off, e.off + e.packed);\n if (e.packed === e.real) return u;\n const out = new Uint8Array(e.real);\n lzo1xDecompress(u, 0, out);\n return out;\n },\n });\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { Mesh } from '@gmvjs/core/types';
2
+ export interface DetailModel {
3
+ shader: string;
4
+ texture: string;
5
+ flags: number;
6
+ minScale: number;
7
+ maxScale: number;
8
+ pos: Float32Array;
9
+ uv: Float32Array;
10
+ indices: Uint16Array;
11
+ }
12
+ export declare function parseDM(u: Uint8Array): DetailModel;
13
+ /** .dm model mesh (normals up, as grass is lit). */
14
+ export declare function dmMesh(m: DetailModel, name: string): Mesh;
15
+ export interface DetailsMap {
16
+ version: number;
17
+ offsX: number;
18
+ offsZ: number;
19
+ sizeX: number;
20
+ sizeZ: number;
21
+ models: DetailModel[];
22
+ slots: DataView;
23
+ }
24
+ export declare function parseDetails(u: Uint8Array): DetailsMap;
25
+ /** Level collision (level.cform): u32 version 4, vertices, triangles, box (6×f32); vertices f32×3; triangles {u32×3, u32 material}. */
26
+ export interface CForm {
27
+ verts: Float32Array;
28
+ tris: Uint32Array;
29
+ }
30
+ export declare function parseCForm(u: Uint8Array): CForm;
31
+ export declare const DETAIL_TRI_BUDGET = 1500000;
32
+ export interface DetailsResult {
33
+ meshes: Mesh[];
34
+ items: number;
35
+ candidates: number;
36
+ keep: number;
37
+ }
38
+ /**
39
+ * Detail placement from the map and collision; budget is the triangle limit (the excess is thinned uniformly).
40
+ * Coordinates are as for the level geometry (left-handed, Y up).
41
+ */
42
+ export declare function buildDetails(map: DetailsMap, cf: CForm, budget?: number): DetailsResult;
@@ -0,0 +1,210 @@
1
+ const dvOf = (u) => new DataView(u.buffer, u.byteOffset, u.byteLength);
2
+ function chunks(u) {
3
+ const d = dvOf(u), out = /* @__PURE__ */ new Map();
4
+ for (let p = 0; p + 8 <= u.length; ) {
5
+ const id = d.getUint32(p, true) & 2147483647, size = d.getUint32(p + 4, true);
6
+ if (p + 8 + size > u.length) break;
7
+ out.set(id, u.subarray(p + 8, p + 8 + size));
8
+ p += 8 + size;
9
+ }
10
+ return out;
11
+ }
12
+ function parseDM(u) {
13
+ const d = dvOf(u);
14
+ let p = 0;
15
+ const str = () => {
16
+ const e = u.indexOf(0, p);
17
+ if (e < 0) throw new Error(".dm: строка не закрыта");
18
+ const s = new TextDecoder("latin1").decode(u.subarray(p, e));
19
+ p = e + 1;
20
+ return s;
21
+ };
22
+ const shader = str(), texture = str();
23
+ if (p + 20 > u.length) throw new Error(".dm обрезан");
24
+ const flags = d.getUint32(p, true), minScale = d.getFloat32(p + 4, true), maxScale = d.getFloat32(p + 8, true), nv = d.getUint32(p + 12, true), ni = d.getUint32(p + 16, true);
25
+ p += 20;
26
+ if (nv > 65536 || ni % 3 || p + nv * 20 + ni * 2 > u.length) throw new Error(".dm: неверное число вершин или индексов");
27
+ const pos = new Float32Array(nv * 3), uv = new Float32Array(nv * 2), indices = new Uint16Array(ni);
28
+ for (let i = 0; i < nv; i++, p += 20) {
29
+ pos[i * 3] = d.getFloat32(p, true);
30
+ pos[i * 3 + 1] = d.getFloat32(p + 4, true);
31
+ pos[i * 3 + 2] = d.getFloat32(p + 8, true);
32
+ uv[i * 2] = d.getFloat32(p + 12, true);
33
+ uv[i * 2 + 1] = d.getFloat32(p + 16, true);
34
+ }
35
+ for (let i = 0; i < ni; i++, p += 2) indices[i] = d.getUint16(p, true);
36
+ return { shader, texture, flags, minScale, maxScale, pos, uv, indices };
37
+ }
38
+ function dmMesh(m, name) {
39
+ const n = m.pos.length / 3, nor = new Float32Array(n * 3);
40
+ for (let i = 0; i < n; i++) nor[i * 3 + 1] = 1;
41
+ return { name, texture: m.texture, shader: m.shader, vertexCount: n, pos: m.pos, nor, uv: m.uv, bone: new Float32Array(n * 4), weight: new Float32Array(n * 4).map((_, i) => i % 4 === 0 ? 1 : 0), indices: m.indices, skinned: false, alphaTest: true };
42
+ }
43
+ function parseDetails(u) {
44
+ const c = chunks(u), h = c.get(0), objs = c.get(1), sl = c.get(2);
45
+ if (!h || h.length < 24 || !objs || !sl) throw new Error("level.details: нет заголовка, моделей или ячеек");
46
+ const hd = dvOf(h), [version, , offsX, offsZ, sizeX, sizeZ] = [0, 4, 8, 12, 16, 20].map((o) => hd.getUint32(o, true));
47
+ if (version !== 3) throw new Error(`level.details версии ${version} не поддерживается`);
48
+ if (sizeX * sizeZ * 16 > sl.length) throw new Error("level.details: ячейки обрезаны");
49
+ const models = [...chunks(objs)].sort((a, b) => a[0] - b[0]).map(([, b]) => parseDM(b));
50
+ return { version, offsX, offsZ, sizeX, sizeZ, models, slots: dvOf(sl) };
51
+ }
52
+ function parseCForm(u) {
53
+ const d = dvOf(u), ver = d.getUint32(0, true), nv = d.getUint32(4, true), nt = d.getUint32(8, true);
54
+ if (ver !== 4 || 36 + nv * 12 + nt * 16 > u.length) throw new Error("level.cform: неизвестный формат");
55
+ const verts = new Float32Array(nv * 3), tris = new Uint32Array(nt * 3);
56
+ for (let i = 0, p = 36; i < nv * 3; i++, p += 4) verts[i] = d.getFloat32(p, true);
57
+ for (let i = 0, p = 36 + nv * 12; i < nt; i++, p += 16) {
58
+ tris[i * 3] = d.getUint32(p, true);
59
+ tris[i * 3 + 1] = d.getUint32(p + 4, true);
60
+ tris[i * 3 + 2] = d.getUint32(p + 8, true);
61
+ }
62
+ return { verts, tris };
63
+ }
64
+ class CRandom {
65
+ constructor(s) {
66
+ this.s = s;
67
+ }
68
+ s;
69
+ next() {
70
+ this.s = Math.imul(this.s, 214013) + 2531011 >>> 0;
71
+ return this.s >>> 16 & 32767;
72
+ }
73
+ randI(n) {
74
+ return this.next() % n;
75
+ }
76
+ randF(a, b) {
77
+ return a + (b - a) * this.next() / 32767;
78
+ }
79
+ randFs(r) {
80
+ return this.randF(-r, r);
81
+ }
82
+ }
83
+ const DITHER = (() => {
84
+ let m = [[0]];
85
+ for (let n = 1; n < 16; n *= 2) m = Array.from({ length: n * 2 }, (_, y) => Array.from({ length: n * 2 }, (_2, x) => 4 * m[y % n][x % n] + [[0, 2], [3, 1]][+(y >= n)][+(x >= n)]));
86
+ return Uint8Array.from(m.flat());
87
+ })();
88
+ const DETAIL_TRI_BUDGET = 15e5;
89
+ const DENSITY = 0.3, SLOT = 2;
90
+ function buildDetails(map, cf, budget = DETAIL_TRI_BUDGET) {
91
+ const { sizeX, sizeZ, offsX, offsZ, slots, models } = map, nObj = models.length;
92
+ const dSize = Math.ceil(SLOT / DENSITY), jitter = DENSITY / 1.7;
93
+ const slotOk = new Uint8Array(sizeX * sizeZ), yLo = new Float32Array(sizeX * sizeZ), yHi = new Float32Array(sizeX * sizeZ);
94
+ for (let i = 0; i < sizeX * sizeZ; i++) {
95
+ const a = slots.getUint32(i * 16, true), b = slots.getUint32(i * 16 + 4, true);
96
+ const ids2 = [a >>> 20 & 63, a >>> 26 & 63, b & 63, b >>> 6 & 63];
97
+ let any = false;
98
+ for (let k = 0; k < 4; k++) if (ids2[k] < nObj && slots.getUint16(i * 16 + 8 + k * 2, true)) any = true;
99
+ if (!any) continue;
100
+ slotOk[i] = 1;
101
+ yLo[i] = (a & 4095) * 0.2 - 200;
102
+ yHi[i] = yLo[i] + (a >>> 12 & 255) * 0.1;
103
+ }
104
+ const { verts: V, tris: T } = cf, nt = T.length / 3;
105
+ const cnt = new Uint32Array(sizeX * sizeZ + 1);
106
+ const forCells = (t, f) => {
107
+ const a = T[t * 3] * 3, b = T[t * 3 + 1] * 3, c = T[t * 3 + 2] * 3;
108
+ const x0 = Math.floor(Math.min(V[a], V[b], V[c]) / SLOT) + offsX, x1 = Math.floor(Math.max(V[a], V[b], V[c]) / SLOT) + offsX;
109
+ const z0 = Math.floor(Math.min(V[a + 2], V[b + 2], V[c + 2]) / SLOT) + offsZ, z1 = Math.floor(Math.max(V[a + 2], V[b + 2], V[c + 2]) / SLOT) + offsZ;
110
+ const ylo = Math.min(V[a + 1], V[b + 1], V[c + 1]), yhi = Math.max(V[a + 1], V[b + 1], V[c + 1]);
111
+ for (let z = Math.max(0, z0); z <= Math.min(sizeZ - 1, z1); z++) for (let x = Math.max(0, x0); x <= Math.min(sizeX - 1, x1); x++) {
112
+ const cell = z * sizeX + x;
113
+ if (slotOk[cell] && yhi >= yLo[cell] - 0.01 && ylo <= yHi[cell] + 0.01) f(cell);
114
+ }
115
+ };
116
+ for (let t = 0; t < nt; t++) forCells(t, (cell) => cnt[cell + 1]++);
117
+ for (let i = 0; i < sizeX * sizeZ; i++) cnt[i + 1] += cnt[i];
118
+ const fill = cnt.slice(0, sizeX * sizeZ), list = new Uint32Array(cnt[sizeX * sizeZ]);
119
+ for (let t = 0; t < nt; t++) forCells(t, (cell) => {
120
+ list[fill[cell]++] = t;
121
+ });
122
+ const triOf = models.map((m) => m.indices.length / 3);
123
+ const alpha = new Float32Array(16), ids = new Int32Array(4), sel = [];
124
+ const walk = (emit, needY) => {
125
+ for (let dz = 0; dz < sizeZ; dz++) for (let dx = 0; dx < sizeX; dx++) {
126
+ const cell = dz * sizeX + dx;
127
+ if (!slotOk[cell]) continue;
128
+ const o = cell * 16, a = slots.getUint32(o, true), b = slots.getUint32(o + 4, true);
129
+ ids[0] = a >>> 20 & 63;
130
+ ids[1] = a >>> 26 & 63;
131
+ ids[2] = b & 63;
132
+ ids[3] = b >>> 6 & 63;
133
+ for (let k = 0; k < 4; k++) {
134
+ const pal = slots.getUint16(o + 8 + k * 2, true);
135
+ for (let j = 0; j < 4; j++) alpha[k * 4 + j] = 255 * (pal >> j * 4 & 15) / 15;
136
+ }
137
+ const sx = dx - offsX, sz = dz - offsZ, seed = (302455168 ^ Math.imul(sx, sz)) >>> 0;
138
+ const rSel = new CRandom(seed), rJit = new CRandom(seed), rYaw = new CRandom(seed), rScl = new CRandom(seed);
139
+ const minX = sx * SLOT, minZ = sz * SLOT, top = yHi[cell], bottom = yLo[cell];
140
+ for (let z = 0; z <= dSize; z++) for (let x = 0; x <= dSize; x++) {
141
+ const shx = rJit.randI(16), shz = rJit.randI(16);
142
+ sel.length = 0;
143
+ const cx = Math.min(x, dSize - 1), cz = Math.min(z, dSize - 1), fx = cx / dSize, fz = cz / dSize;
144
+ for (let k = 0; k < 4; k++) {
145
+ if (ids[k] >= nObj) continue;
146
+ const c01 = alpha[k * 4] * (1 - fx) + alpha[k * 4 + 1] * fx, c02 = alpha[k * 4 + 2] * (1 - fx) + alpha[k * 4 + 3] * fx;
147
+ const c = Math.min(255, Math.max(0, Math.floor(c01 * (1 - fz) + c02 * fz + 0.5)));
148
+ if (c > DITHER[(cx + shx) % 16 * 16 + (cz + shz) % 16]) sel.push(k);
149
+ }
150
+ if (!sel.length) continue;
151
+ const obj = ids[sel.length === 1 ? sel[0] : sel[rSel.randI(sel.length)]];
152
+ const px = x / dSize * SLOT + minX + rJit.randFs(jitter), pz = z / dSize * SLOT + minZ + rJit.randFs(jitter);
153
+ let y = bottom - 5;
154
+ if (needY) {
155
+ for (let i = cnt[cell]; i < cnt[cell + 1]; i++) {
156
+ const t = list[i], A = T[t * 3] * 3, B = T[t * 3 + 1] * 3, C = T[t * 3 + 2] * 3;
157
+ const ax = V[A], az = V[A + 2], e1x = V[B] - ax, e1z = V[B + 2] - az, e2x = V[C] - ax, e2z = V[C + 2] - az;
158
+ const det = e1x * e2z - e2x * e1z;
159
+ if (Math.abs(det) < 1e-9) continue;
160
+ const qx = px - ax, qz = pz - az, u = (qx * e2z - e2x * qz) / det, v = (e1x * qz - qx * e1z) / det;
161
+ if (u < 0 || v < 0 || u + v > 1) continue;
162
+ const yt = V[A + 1] + u * (V[B + 1] - V[A + 1]) + v * (V[C + 1] - V[A + 1]);
163
+ if (yt <= top + 1e-3 && yt > y) y = yt;
164
+ }
165
+ if (y < bottom) continue;
166
+ }
167
+ const m = models[obj], scale = rScl.randF(m.minScale * 0.5, m.maxScale * 0.9), yaw = rYaw.randF(0, Math.PI * 2);
168
+ emit(obj, px, y, pz, scale, yaw, cell);
169
+ }
170
+ }
171
+ };
172
+ let candTris = 0, candidates = 0;
173
+ walk((obj) => {
174
+ candTris += triOf[obj];
175
+ candidates++;
176
+ }, false);
177
+ const keep = candTris > budget ? budget / candTris : 1;
178
+ const thin = new CRandom(24301);
179
+ const P = models.map(() => []), U = models.map(() => []), I = models.map(() => []);
180
+ let items = 0;
181
+ walk((obj, x, y, z, s, yaw) => {
182
+ if (keep < 1 && thin.next() / 32768 >= keep) return;
183
+ const m = models[obj], c = Math.cos(yaw), sn = Math.sin(yaw), p = P[obj], base = p.length / 3;
184
+ if (base + m.pos.length / 3 > 4e6) return;
185
+ for (let i = 0; i < m.pos.length; i += 3) {
186
+ const vx = m.pos[i] * s, vy = m.pos[i + 1] * s, vz = m.pos[i + 2] * s;
187
+ p.push(x + vx * c + vz * sn, y + vy, z - vx * sn + vz * c);
188
+ }
189
+ for (let i = 0; i < m.uv.length; i++) U[obj].push(m.uv[i]);
190
+ for (let i = 0; i < m.indices.length; i++) I[obj].push(base + m.indices[i]);
191
+ items++;
192
+ }, true);
193
+ const meshes = [];
194
+ models.forEach((m, k) => {
195
+ if (!I[k].length) return;
196
+ const n = P[k].length / 3, nor = new Float32Array(n * 3);
197
+ for (let i = 0; i < n; i++) nor[i * 3 + 1] = 1;
198
+ meshes.push({ name: `Трава · ${m.texture}`, texture: m.texture, shader: m.shader, vertexCount: n, pos: Float32Array.from(P[k]), nor, uv: Float32Array.from(U[k]), bone: new Float32Array(n * 4), weight: new Float32Array(n * 4).map((_, i) => i % 4 === 0 ? 1 : 0), indices: Uint32Array.from(I[k]), skinned: false, alphaTest: true });
199
+ });
200
+ return { meshes, items, candidates, keep };
201
+ }
202
+ export {
203
+ DETAIL_TRI_BUDGET,
204
+ buildDetails,
205
+ dmMesh,
206
+ parseCForm,
207
+ parseDM,
208
+ parseDetails
209
+ };
210
+ //# sourceMappingURL=details.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"details.js","sources":["../src/details.ts"],"sourcesContent":["// X-Ray grass and small details: .dm models and the level.details map (SoC, CS, CoP).\n// .dm: shader, texture (strings), u32 flags, f32 min/max scale, u32 vertices, u32 indices, vertices {f32 x,y,z,u,v}, u16 indices.\n// level.details is chunks: 0 header (u32 version 3, objects, offs_x, offs_z, size_x, size_z), 1 models (nested .dm chunks),\n// 2 cells of 2×2 m, 16 bytes each: u32 y_base:12 (×0.2 − 200 m), y_height:8 (×0.1 m), id0:6, id1:6; u32 id2:6, id3:6, light…;\n// 4×u16 densities (4 bits per cell corner). Cell (x, z) lies in the world at ((x − offs_x)·2, (z − offs_z)·2).\n// The game computes placement itself at display time (CDetailManager::cache_Decompress): a jittered point grid, model choice\n// by interpolated density with ordered dithering, height by a ray down onto the level collision (level.cform),\n// rotation about Y and scale are random. The same order is used here; with too many triangles the grass is thinned.\nimport type { Mesh } from '@gmvjs/core/types';\n\nexport interface DetailModel { shader: string; texture: string; flags: number; minScale: number; maxScale: number; pos: Float32Array; uv: Float32Array; indices: Uint16Array }\n\nconst dvOf = (u: Uint8Array) => new DataView(u.buffer, u.byteOffset, u.byteLength);\n\nfunction chunks(u: Uint8Array): Map<number, Uint8Array> {\n const d = dvOf(u), out = new Map<number, Uint8Array>();\n for (let p = 0; p + 8 <= u.length; ) {\n const id = d.getUint32(p, true) & 0x7fffffff, size = d.getUint32(p + 4, true);\n if (p + 8 + size > u.length) break;\n out.set(id, u.subarray(p + 8, p + 8 + size));\n p += 8 + size;\n }\n return out;\n}\n\nexport function parseDM(u: Uint8Array): DetailModel {\n const d = dvOf(u);\n let p = 0;\n const str = () => { const e = u.indexOf(0, p); if (e < 0) throw new Error('.dm: строка не закрыта'); const s = new TextDecoder('latin1').decode(u.subarray(p, e)); p = e + 1; return s; };\n const shader = str(), texture = str();\n if (p + 20 > u.length) throw new Error('.dm обрезан');\n const flags = d.getUint32(p, true), minScale = d.getFloat32(p + 4, true), maxScale = d.getFloat32(p + 8, true), nv = d.getUint32(p + 12, true), ni = d.getUint32(p + 16, true);\n p += 20;\n if (nv > 65536 || ni % 3 || p + nv * 20 + ni * 2 > u.length) throw new Error('.dm: неверное число вершин или индексов');\n const pos = new Float32Array(nv * 3), uv = new Float32Array(nv * 2), indices = new Uint16Array(ni);\n for (let i = 0; i < nv; i++, p += 20) { pos[i * 3] = d.getFloat32(p, true); pos[i * 3 + 1] = d.getFloat32(p + 4, true); pos[i * 3 + 2] = d.getFloat32(p + 8, true); uv[i * 2] = d.getFloat32(p + 12, true); uv[i * 2 + 1] = d.getFloat32(p + 16, true); }\n for (let i = 0; i < ni; i++, p += 2) indices[i] = d.getUint16(p, true);\n return { shader, texture, flags, minScale, maxScale, pos, uv, indices };\n}\n\n/** .dm model mesh (normals up, as grass is lit). */\nexport function dmMesh(m: DetailModel, name: string): Mesh {\n const n = m.pos.length / 3, nor = new Float32Array(n * 3);\n for (let i = 0; i < n; i++) nor[i * 3 + 1] = 1;\n return { name, texture: m.texture, shader: m.shader, vertexCount: n, pos: m.pos, nor, uv: m.uv, bone: new Float32Array(n * 4), weight: new Float32Array(n * 4).map((_, i) => (i % 4 === 0 ? 1 : 0)), indices: m.indices, skinned: false, alphaTest: true };\n}\n\nexport interface DetailsMap { version: number; offsX: number; offsZ: number; sizeX: number; sizeZ: number; models: DetailModel[]; slots: DataView }\n\nexport function parseDetails(u: Uint8Array): DetailsMap {\n const c = chunks(u), h = c.get(0), objs = c.get(1), sl = c.get(2);\n if (!h || h.length < 24 || !objs || !sl) throw new Error('level.details: нет заголовка, моделей или ячеек');\n const hd = dvOf(h), [version, , offsX, offsZ, sizeX, sizeZ] = [0, 4, 8, 12, 16, 20].map((o) => hd.getUint32(o, true)) as [number, number, number, number, number, number];\n if (version !== 3) throw new Error(`level.details версии ${version} не поддерживается`);\n if (sizeX * sizeZ * 16 > sl.length) throw new Error('level.details: ячейки обрезаны');\n const models = [...chunks(objs)].sort((a, b) => a[0] - b[0]).map(([, b]) => parseDM(b));\n return { version, offsX, offsZ, sizeX, sizeZ, models, slots: dvOf(sl) };\n}\n\n/** Level collision (level.cform): u32 version 4, vertices, triangles, box (6×f32); vertices f32×3; triangles {u32×3, u32 material}. */\nexport interface CForm { verts: Float32Array; tris: Uint32Array }\nexport function parseCForm(u: Uint8Array): CForm {\n const d = dvOf(u), ver = d.getUint32(0, true), nv = d.getUint32(4, true), nt = d.getUint32(8, true);\n if (ver !== 4 || 36 + nv * 12 + nt * 16 > u.length) throw new Error('level.cform: неизвестный формат');\n const verts = new Float32Array(nv * 3), tris = new Uint32Array(nt * 3);\n for (let i = 0, p = 36; i < nv * 3; i++, p += 4) verts[i] = d.getFloat32(p, true);\n for (let i = 0, p = 36 + nv * 12; i < nt; i++, p += 16) { tris[i * 3] = d.getUint32(p, true); tris[i * 3 + 1] = d.getUint32(p + 4, true); tris[i * 3 + 2] = d.getUint32(p + 8, true); }\n return { verts, tris };\n}\n\n/** The engine's CRandom (like MSVC rand()). */\nclass CRandom {\n constructor(private s: number) {}\n next() { this.s = (Math.imul(this.s, 214013) + 2531011) >>> 0; return (this.s >>> 16) & 0x7fff; }\n randI(n: number) { return this.next() % n; }\n randF(a: number, b: number) { return a + ((b - a) * this.next()) / 32767; }\n randFs(r: number) { return this.randF(-r, r); }\n}\n\n/** 16×16 ordered dithering (a Bayer matrix, thresholds 0..255). */\nconst DITHER = (() => {\n let m = [[0]];\n for (let n = 1; n < 16; n *= 2) m = Array.from({ length: n * 2 }, (_, y) => Array.from({ length: n * 2 }, (_, x) => 4 * m[y % n]![x % n]! + [[0, 2], [3, 1]][+(y >= n)]![+(x >= n)]!));\n return Uint8Array.from(m.flat());\n})();\nexport const DETAIL_TRI_BUDGET = 1_500_000;\n/** Default grass density (r__detail_density): the point grid step in metres. */\nconst DENSITY = 0.3, SLOT = 2;\n\nexport interface DetailsResult { meshes: Mesh[]; items: number; candidates: number; keep: number }\n\n/**\n * Detail placement from the map and collision; budget is the triangle limit (the excess is thinned uniformly).\n * Coordinates are as for the level geometry (left-handed, Y up).\n */\nexport function buildDetails(map: DetailsMap, cf: CForm, budget = DETAIL_TRI_BUDGET): DetailsResult {\n const { sizeX, sizeZ, offsX, offsZ, slots, models } = map, nObj = models.length;\n const dSize = Math.ceil(SLOT / DENSITY), jitter = DENSITY / 1.7;\n // non-empty cells and their heights\n const slotOk = new Uint8Array(sizeX * sizeZ), yLo = new Float32Array(sizeX * sizeZ), yHi = new Float32Array(sizeX * sizeZ);\n for (let i = 0; i < sizeX * sizeZ; i++) {\n const a = slots.getUint32(i * 16, true), b = slots.getUint32(i * 16 + 4, true);\n const ids = [(a >>> 20) & 63, (a >>> 26) & 63, b & 63, (b >>> 6) & 63];\n let any = false;\n for (let k = 0; k < 4; k++) if (ids[k]! < nObj && slots.getUint16(i * 16 + 8 + k * 2, true)) any = true;\n if (!any) continue;\n slotOk[i] = 1; yLo[i] = (a & 0xfff) * 0.2 - 200; yHi[i] = yLo[i]! + ((a >>> 12) & 0xff) * 0.1;\n }\n // collision triangles per cell (non-empty cells only)\n const { verts: V, tris: T } = cf, nt = T.length / 3;\n const cnt = new Uint32Array(sizeX * sizeZ + 1);\n const forCells = (t: number, f: (cell: number) => void) => {\n const a = T[t * 3]! * 3, b = T[t * 3 + 1]! * 3, c = T[t * 3 + 2]! * 3;\n const x0 = Math.floor(Math.min(V[a]!, V[b]!, V[c]!) / SLOT) + offsX, x1 = Math.floor(Math.max(V[a]!, V[b]!, V[c]!) / SLOT) + offsX;\n const z0 = Math.floor(Math.min(V[a + 2]!, V[b + 2]!, V[c + 2]!) / SLOT) + offsZ, z1 = Math.floor(Math.max(V[a + 2]!, V[b + 2]!, V[c + 2]!) / SLOT) + offsZ;\n const ylo = Math.min(V[a + 1]!, V[b + 1]!, V[c + 1]!), yhi = Math.max(V[a + 1]!, V[b + 1]!, V[c + 1]!);\n for (let z = Math.max(0, z0); z <= Math.min(sizeZ - 1, z1); z++) for (let x = Math.max(0, x0); x <= Math.min(sizeX - 1, x1); x++) {\n const cell = z * sizeX + x;\n if (slotOk[cell] && yhi >= yLo[cell]! - 0.01 && ylo <= yHi[cell]! + 0.01) f(cell);\n }\n };\n for (let t = 0; t < nt; t++) forCells(t, (cell) => cnt[cell + 1]!++);\n for (let i = 0; i < sizeX * sizeZ; i++) cnt[i + 1]! += cnt[i]!;\n const fill = cnt.slice(0, sizeX * sizeZ), list = new Uint32Array(cnt[sizeX * sizeZ]!);\n for (let t = 0; t < nt; t++) forCells(t, (cell) => { list[fill[cell]!++] = t; });\n\n // pass over cells: kind 0 = counting triangles, 1 = placement\n const triOf = models.map((m) => m.indices.length / 3);\n const alpha = new Float32Array(16), ids = new Int32Array(4), sel: number[] = [];\n const walk = (emit: (obj: number, x: number, y: number, z: number, scale: number, yaw: number, cell: number) => void, needY: boolean) => {\n for (let dz = 0; dz < sizeZ; dz++) for (let dx = 0; dx < sizeX; dx++) {\n const cell = dz * sizeX + dx;\n if (!slotOk[cell]) continue;\n const o = cell * 16, a = slots.getUint32(o, true), b = slots.getUint32(o + 4, true);\n ids[0] = (a >>> 20) & 63; ids[1] = (a >>> 26) & 63; ids[2] = b & 63; ids[3] = (b >>> 6) & 63;\n for (let k = 0; k < 4; k++) { const pal = slots.getUint16(o + 8 + k * 2, true); for (let j = 0; j < 4; j++) alpha[k * 4 + j] = (255 * ((pal >> (j * 4)) & 15)) / 15; }\n const sx = dx - offsX, sz = dz - offsZ, seed = (0x12071980 ^ Math.imul(sx, sz)) >>> 0;\n const rSel = new CRandom(seed), rJit = new CRandom(seed), rYaw = new CRandom(seed), rScl = new CRandom(seed);\n const minX = sx * SLOT, minZ = sz * SLOT, top = yHi[cell]!, bottom = yLo[cell]!;\n for (let z = 0; z <= dSize; z++) for (let x = 0; x <= dSize; x++) {\n const shx = rJit.randI(16), shz = rJit.randI(16);\n sel.length = 0;\n const cx = Math.min(x, dSize - 1), cz = Math.min(z, dSize - 1), fx = cx / dSize, fz = cz / dSize;\n for (let k = 0; k < 4; k++) {\n if (ids[k]! >= nObj) continue;\n const c01 = alpha[k * 4]! * (1 - fx) + alpha[k * 4 + 1]! * fx, c02 = alpha[k * 4 + 2]! * (1 - fx) + alpha[k * 4 + 3]! * fx;\n const c = Math.min(255, Math.max(0, Math.floor(c01 * (1 - fz) + c02 * fz + 0.5)));\n if (c > DITHER[((cx + shx) % 16) * 16 + ((cz + shz) % 16)]!) sel.push(k);\n }\n if (!sel.length) continue;\n const obj = ids[sel.length === 1 ? sel[0]! : sel[rSel.randI(sel.length)]!]!;\n const px = (x / dSize) * SLOT + minX + rJit.randFs(jitter), pz = (z / dSize) * SLOT + minZ + rJit.randFs(jitter);\n let y = bottom - 5;\n if (needY) {\n for (let i = cnt[cell]!; i < cnt[cell + 1]!; i++) {\n const t = list[i]!, A = T[t * 3]! * 3, B = T[t * 3 + 1]! * 3, C = T[t * 3 + 2]! * 3;\n // a vertical ray: barycentric coordinates in the XZ plane\n const ax = V[A]!, az = V[A + 2]!, e1x = V[B]! - ax, e1z = V[B + 2]! - az, e2x = V[C]! - ax, e2z = V[C + 2]! - az;\n const det = e1x * e2z - e2x * e1z;\n if (Math.abs(det) < 1e-9) continue;\n const qx = px - ax, qz = pz - az, u = (qx * e2z - e2x * qz) / det, v = (e1x * qz - qx * e1z) / det;\n if (u < 0 || v < 0 || u + v > 1) continue;\n const yt = V[A + 1]! + u * (V[B + 1]! - V[A + 1]!) + v * (V[C + 1]! - V[A + 1]!);\n if (yt <= top + 1e-3 && yt > y) y = yt;\n }\n if (y < bottom) continue;\n }\n const m = models[obj]!, scale = rScl.randF(m.minScale * 0.5, m.maxScale * 0.9), yaw = rYaw.randF(0, Math.PI * 2);\n emit(obj, px, y, pz, scale, yaw, cell);\n }\n }\n };\n let candTris = 0, candidates = 0;\n walk((obj) => { candTris += triOf[obj]!; candidates++; }, false);\n const keep = candTris > budget ? budget / candTris : 1;\n const thin = new CRandom(0x5eed);\n const P: number[][] = models.map(() => []), U: number[][] = models.map(() => []), I: number[][] = models.map(() => []);\n let items = 0;\n walk((obj, x, y, z, s, yaw) => {\n if (keep < 1 && thin.next() / 32768 >= keep) return;\n const m = models[obj]!, c = Math.cos(yaw), sn = Math.sin(yaw), p = P[obj]!, base = p.length / 3;\n if (base + m.pos.length / 3 > 4_000_000) return;\n for (let i = 0; i < m.pos.length; i += 3) {\n const vx = m.pos[i]! * s, vy = m.pos[i + 1]! * s, vz = m.pos[i + 2]! * s;\n p.push(x + vx * c + vz * sn, y + vy, z - vx * sn + vz * c); // rotation about Y (like Fmatrix::rotateY)\n }\n for (let i = 0; i < m.uv.length; i++) U[obj]!.push(m.uv[i]!);\n for (let i = 0; i < m.indices.length; i++) I[obj]!.push(base + m.indices[i]!);\n items++;\n }, true);\n const meshes: Mesh[] = [];\n models.forEach((m, k) => {\n if (!I[k]!.length) return;\n const n = P[k]!.length / 3, nor = new Float32Array(n * 3);\n for (let i = 0; i < n; i++) nor[i * 3 + 1] = 1;\n meshes.push({ name: `Трава · ${m.texture}`, texture: m.texture, shader: m.shader, vertexCount: n, pos: Float32Array.from(P[k]!), nor, uv: Float32Array.from(U[k]!), bone: new Float32Array(n * 4), weight: new Float32Array(n * 4).map((_, i) => (i % 4 === 0 ? 1 : 0)), indices: Uint32Array.from(I[k]!), skinned: false, alphaTest: true });\n });\n return { meshes, items, candidates, keep };\n}\n"],"names":["_","ids"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { DecodedImage, Model } from '@gmvjs/core/types';
2
+ export interface TextureForExport {
3
+ /** Source texture name (for the file name). */
4
+ name: string;
5
+ /** A source DDS is copied without re-encoding. */
6
+ dds?: Uint8Array;
7
+ /** Otherwise a decoded image. */
8
+ image?: DecodedImage | null;
9
+ }
10
+ export interface XrayExportOptions {
11
+ /** Level/object name: Latin letters, digits, "_". */
12
+ name: string;
13
+ /** Folder inside rawdata/objects and gamedata/textures. Default: "gmv\\<name>". */
14
+ prefix?: string;
15
+ /** Unit-to-metre multiplier (X-Ray: 1 = 1 m). */
16
+ scale?: number;
17
+ /** Split cell size, m; 0 = one object. Default: 0 for models, 32 m for scenes larger than 100 m or 60 thousand triangles, 64 m above 400 m. */
18
+ cell?: number;
19
+ /** Mesh texture by number (from the viewing session). */
20
+ texture?: (meshIndex: number) => Promise<TextureForExport | null>;
21
+ /** Hidden meshes are not exported. */
22
+ hidden?: ReadonlySet<number>;
23
+ /**
24
+ * Level objects (model.instances): true (default) = each model as one .object in "<prefix>\\props", the scene holds references with poses;
25
+ * false = objects are merged into the cell geometry.
26
+ */
27
+ instances?: boolean;
28
+ /**
29
+ * The source is X-Ray itself (a compiled level, OGF, .object): texture names, shaders and materials stay as in the game,
30
+ * textures are not copied (they are already in gamedata).
31
+ */
32
+ xraySource?: boolean;
33
+ }
34
+ export interface XrayExportResult {
35
+ files: Map<string, Uint8Array>;
36
+ report: string[];
37
+ }
38
+ export declare function exportXraySdk(model: Model, o: XrayExportOptions): Promise<XrayExportResult>;