@gmvjs/dunia1 0.1.0 → 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/README.md CHANGED
@@ -1,18 +1,20 @@
1
1
  # @gmvjs/dunia1 — Dunia Engine 1 (Far Cry 2)
2
2
 
3
- Dunia Engine 1 (Far Cry 2; Dunia 2 — Far Cry 3 и новее — пока не поддержан): архивы `.fat/.dat` (FAT2 v5, LZO), модели `.xbg`, текстуры `.xbt`, анимации `.mab` с корневым движением, наборы персонажей, уровни `.fc2level`.
3
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
4
4
 
5
- Плагин движка для [`@gmvjs/core`](../core/README.md): форматы, архивы и пресеты игр. Тяжёлые разборщики загружаются по требованию (`import()` внутри `load`), так что подключение пакета почти ничего не стоит, пока файл этого движка не открыт.
5
+ Dunia Engine 1 (Far Cry 2; Dunia 2, Far Cry 3 and later, is not supported yet): `.fat/.dat` archives (FAT2 v5, LZO), `.xbg` models, `.xbt` textures, `.mab` animations with root motion, character outfits, `.fc2level` levels.
6
6
 
7
- ## Установка
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
8
10
 
9
11
  ```sh
10
12
  npm i @gmvjs/core @gmvjs/dunia1 @gmvjs/codecs
11
13
  ```
12
14
 
13
- ## Подключение
15
+ ## Usage
14
16
 
15
- Весь движок (все игры):
17
+ The whole engine (all games):
16
18
 
17
19
  ```ts
18
20
  import { createRegistry, ViewerSession } from '@gmvjs/core';
@@ -21,7 +23,7 @@ import { dunia1Engine } from '@gmvjs/dunia1';
21
23
  const session = new ViewerSession(createRegistry([dunia1Engine]));
22
24
  ```
23
25
 
24
- Одна игра — подпуть пакета (пресет игры и движок, ограниченный этой игрой):
26
+ One game: a package subpath (the game preset and an engine limited to that game):
25
27
 
26
28
  ```ts
27
29
  import { createRegistry, ViewerSession } from '@gmvjs/core';
@@ -30,38 +32,42 @@ import farcry2 from '@gmvjs/dunia1/farcry2';
30
32
  const session = new ViewerSession(createRegistry([farcry2.engine]), farcry2.preset.id);
31
33
  ```
32
34
 
33
- ## Игры (подпути)
35
+ ## Games (subpaths)
34
36
 
35
- | Импорт | Пресет | Игра | Статус | Только для этой игры |
37
+ | Import | Preset | Game | Status | Only for this game |
36
38
  |---|---|---|---|---|
37
- | `@gmvjs/dunia1/farcry2` | `farcry2` | Far Cry 2 (2008) | 🟡 бета | — |
39
+ | `@gmvjs/dunia1/farcry2` | `farcry2` | Far Cry 2 (2008) | 🟡 beta | — |
38
40
 
39
- ## Форматы
41
+ ## Formats
40
42
 
41
- | id | Что | Файлы | Статус |
43
+ | id | What | Files | Status |
42
44
  |---|---|---|---|
43
- | `dunia-xbg` | Far Cry 2: модель .xbg | `.xbg` + анимации `.mab` | 🟡 бета |
44
- | `dunia-level` | Far Cry 2: уровень (worlds/<мир>/maps/*.fc2level — карта 1×1 км с объектами; <мир>.fc2level — весь мир) | `.fc2level` | 🟡 бета |
45
+ | `dunia-xbg` | Far Cry 2: .xbg model | `.xbg` + `.mab` animations | 🟡 beta |
46
+ | `dunia-level` | Far Cry 2: level (worlds/<world>/maps/*.fc2level is a 1×1 km map with objects; <world>.fc2level is the whole world) | `.fc2level` | 🟡 beta |
45
47
 
46
- ## Архивы
48
+ ## Archives
47
49
 
48
- - `dunia-fat` — Far Cry 2: архивы .fat + .dat (`.fat`)
50
+ - `dunia-fat` — Far Cry 2: .fat + .dat archives (`.fat`)
49
51
 
50
- ## Текстуры
52
+ ## Textures
51
53
 
52
54
  - `xbt` — `.xbt`
53
55
 
54
- ## Зависимости
56
+ ## Dependencies
55
57
 
56
- - `@gmvjs/core` — API плагинов, типы, архивы ZIP/папка, zlib, DDS/BC, геометрия
58
+ - `@gmvjs/core` — plugin API, types, ZIP/folder archives, zlib, DDS/BC, geometry
57
59
  - `@gmvjs/codecs` — LZO
58
60
 
59
- ## Не сделано
61
+ ## Not done yet
60
62
 
61
- - Лицевая анимация
63
+ - Facial animation
62
64
  - Havok
63
- - Точные деревья RealTree, небо, смешивание слоёв рельефа
65
+ - Exact RealTree trees, sky, terrain layer blending
64
66
 
65
- ## Лицензия
67
+ ## License
66
68
 
67
69
  MIT
70
+
71
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
72
+
73
+ If this project helps you, consider supporting its development on Patreon ❤️
package/dist/fat.d.ts CHANGED
@@ -7,38 +7,38 @@ export interface FatEntry {
7
7
  off: number;
8
8
  }
9
9
  export declare function parseFat(u: Uint8Array): FatEntry[];
10
- /** Распаковать запись; head — только первые head байт (для распознавания), без чтения всей записи. */
10
+ /** Unpack a record; head: only the first head bytes (for recognition), without reading the whole record. */
11
11
  export declare function readFatEntry(dat: BlobFile, e: FatEntry, head?: number): Promise<Uint8Array>;
12
- /** Хеш имени файла Dunia (FC2): CRC-32 пути в нижнем регистре, разделитель «\». */
12
+ /** Dunia (FC2) file name hash: CRC-32 of the lower-case path with "\" separators. */
13
13
  export declare const duniaHash: (path: string) => number;
14
- /** Пути из XML-списков зависимостей (ID="…" и crc_ID). */
14
+ /** Paths from XML dependency lists (ID="…" and crc_ID). */
15
15
  export declare function namesFromResourceXml(text: string): string[];
16
- /** Запись .fat вместе с её .dat. */
16
+ /** A .fat record together with its .dat. */
17
17
  export interface FatRef {
18
18
  fe: FatEntry;
19
19
  dat: BlobFile;
20
20
  }
21
- /** Мир (уровень): worlds/<имя>/generated/<имя>.game.xml и .sectorsdep.fcb. */
21
+ /** World (level): worlds/<name>/generated/<name>.game.xml and .sectorsdep.fcb. */
22
22
  export interface DuniaWorld {
23
23
  name: string;
24
24
  gameXml: FatRef;
25
- /** .dat мира и смещение его .sectorsdep.fcb. */
25
+ /** The world's .dat and the offset of its .sectorsdep.fcb. */
26
26
  dat: BlobFile;
27
27
  end: number;
28
- /** Файлы мира без имён (сектора, ландшафт): записи того же .dat между концом предыдущего мира и этим .sectorsdep.fcb. */
28
+ /** Unnamed world files (sectors, terrain): records of the same .dat between the end of the previous world and this .sectorsdep.fcb. */
29
29
  files(): FatRef[];
30
30
  }
31
31
  export interface DuniaArchiveApi {
32
32
  duniaWorld(name: string): DuniaWorld | null;
33
33
  duniaWorlds(): DuniaWorld[];
34
- /** Все записи .dat по порядку смещений. */
34
+ /** All .dat records in offset order. */
35
35
  datRefs(dat: BlobFile): FatRef[];
36
36
  readRef(r: FatRef, head?: number): Promise<Uint8Array>;
37
- /** Запись по хешу имени (ссылки коллекций уровней — хеши путей моделей) и её имя, если известно. */
37
+ /** Record by name hash (level collection references are model path hashes) and its name, if known. */
38
38
  refByHash(h: number): FatRef | undefined;
39
39
  nameOfHash(h: number): string | undefined;
40
40
  }
41
- /** Метка виртуального файла уровня (worlds/<мир>/….fc2level): дальше JSON с миром и областью секторов. */
41
+ /** Marker of a virtual level file (worlds/<world>/….fc2level): followed by JSON with the world and the sector area. */
42
42
  export declare const LEVEL_MARK = "DUNIA-LEVEL\n";
43
43
  export interface LevelSpec {
44
44
  world: string;
@@ -50,8 +50,8 @@ export interface LevelSpec {
50
50
  terrainOnly?: boolean;
51
51
  }
52
52
  /**
53
- * Все .fat из выбранных (или из папки игры) одним архивом; открывается по первому. Имена — из XML-списков ресурсов.
53
+ * All .fat files from the selection (or the game folder) as one archive; opened via the first. Names come from XML resource lists.
54
54
  */
55
55
  export declare function openDuniaArchive(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;
56
- /** Миры архива Dunia (для уровней), если архив — Dunia. */
56
+ /** Worlds of a Dunia archive (for levels), if the archive is Dunia. */
57
57
  export declare const duniaApi: (a: unknown) => DuniaArchiveApi | null;
package/dist/fat.js CHANGED
@@ -203,7 +203,7 @@ async function openDuniaArchive(file, siblings) {
203
203
  kind: "dunia-fat",
204
204
  version: 5,
205
205
  entries,
206
- // путь из ссылок модели/материала — по хешу, даже если его нет в списках
206
+ // a path from model/material references: by hash, even if it is not in the lists
207
207
  lookup: (p) => {
208
208
  const k = p.replace(/\\/g, "/").toLowerCase();
209
209
  return levelIndex.get(k) ?? entryOf(duniaHash(p), k);
package/dist/fat.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fat.js","sources":["../src/fat.ts"],"sourcesContent":["// Архивы Dunia (Far Cry 2): пара <имя>.fat (оглавление) + <имя>.dat (данные).\n// .fat: «2TAF» (FAT2 в little-endian), u32 версия 5, u32 флаги (платформа/сжатие), u32 число записей, затем по 16 байт:\n// u32 хеш имени, u32 (размер после распаковки << 2 | схема: 0 — без сжатия, 1 — LZO1X),\n// u32 (младшие 2 бита смещения << 30 | размер в .dat), u32 смещение >> 2; в конце u32 0.\n// Имён в архиве нет — только хеш: CRC-32 пути в нижнем регистре с обратными косыми («graphics\\weapons\\…\\ak47.xbg»).\n// Имена берутся из самой игры: XML-списки зависимостей ресурсов («<root>» с CGeometryResource / CMaterialResource /\n// CTextureResource ID=\"…»), которые лежат в тех же архивах; остальные файлы доступны только по пути из ссылок (lookup).\n// Текстура «x.xbt» читается из «x_mip0.xbt», если он есть: там старший уровень (до 1024²) отдельным XBT.\nimport { lzo1xDecompress } from '@gmvjs/codecs';\nimport { crc32 } from '@gmvjs/core/writers/zip';\nimport { createArchive, readRange } from '@gmvjs/core/archives/common';\nimport type { Archive, ArchiveEntry, BlobFile } from '@gmvjs/core/types';\n\nexport interface FatEntry { hash: number; size: number; scheme: number; csize: number; off: number }\n\nexport function parseFat(u: Uint8Array): FatEntry[] {\n const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);\n if (u.length < 16 || dv.getUint32(0, true) !== 0x46415432) throw new Error('Не архив Dunia (нет «FAT2»)');\n const ver = dv.getUint32(4, true);\n if (ver !== 5) throw new Error(`Архив Dunia версии ${ver} не поддерживается (нужна 5 — Far Cry 2)`);\n const n = dv.getUint32(12, true);\n if (16 + n * 16 > u.length) throw new Error('.fat: оглавление обрезано');\n const out: FatEntry[] = [];\n for (let i = 0; i < n; i++) {\n const o = 16 + i * 16, a = dv.getUint32(o + 4, true), b = dv.getUint32(o + 8, true), c = dv.getUint32(o + 12, true);\n // у несжатых записей (схема 0) размер после распаковки не записан (0) — это размер в .dat\n const scheme = a & 3, csize = b & 0x3fffffff;\n out.push({ hash: dv.getUint32(o, true), size: scheme === 0 ? csize : a >>> 2, scheme, csize, off: c * 4 + (b >>> 30) });\n }\n return out;\n}\n\n/** Распаковать запись; head — только первые head байт (для распознавания), без чтения всей записи. */\nexport async function readFatEntry(dat: BlobFile, e: FatEntry, head = 0): Promise<Uint8Array> {\n if (e.scheme === 0) return readRange(dat, e.off, e.off + (head ? Math.min(head, e.csize) : e.csize));\n if (e.scheme !== 1) throw new Error(`Dunia: схема сжатия ${e.scheme} не поддерживается`);\n if (head) {\n // начало потока LZO: литералы и ссылки назад укладываются в первые байты; распаковка обрывается, начало уже готово\n // буфер с запасом: первая серия литералов длиннее head не помещается в маленький буфер и не копируется вовсе\n // малое начало (распознавание по заголовку) — сначала из 512 байт; если не хватило (первая серия длиннее) — из 2048\n for (const take of head <= 64 ? [512, 2048] : [2048]) {\n const out = new Uint8Array(Math.min(Math.max(head, 1024), e.size));\n try { lzo1xDecompress(await readRange(dat, e.off, e.off + Math.min(e.csize, take)), 0, out); } catch { /* оборван — это ожидаемо */ }\n const r = out.subarray(0, Math.min(head, out.length));\n if (take >= e.csize || r.some((b) => b !== 0)) return r;\n }\n return new Uint8Array(0);\n }\n const out = new Uint8Array(e.size);\n lzo1xDecompress(await readRange(dat, e.off, e.off + e.csize), 0, out);\n return out;\n}\n\n/** Хеш имени файла Dunia (FC2): CRC-32 пути в нижнем регистре, разделитель «\\». */\nexport const duniaHash = (path: string): number => crc32(new TextEncoder().encode(path.replace(/\\//g, '\\\\').toLowerCase())) >>> 0;\n\n/** Пути из XML-списков зависимостей (ID=\"…\" и crc_ID). */\nexport function namesFromResourceXml(text: string): string[] {\n const out: string[] = [];\n for (const m of text.matchAll(/ID=\"([^\"]+\\.[A-Za-z0-9]{2,5})\"/g)) out.push(m[1]!);\n return out;\n}\n\ninterface DuniaEntry extends ArchiveEntry { fe: FatEntry; dat: BlobFile; text?: string }\n\n/** Запись .fat вместе с её .dat. */\nexport interface FatRef { fe: FatEntry; dat: BlobFile }\n\n/** Мир (уровень): worlds/<имя>/generated/<имя>.game.xml и .sectorsdep.fcb. */\nexport interface DuniaWorld {\n name: string;\n gameXml: FatRef;\n /** .dat мира и смещение его .sectorsdep.fcb. */\n dat: BlobFile;\n end: number;\n /** Файлы мира без имён (сектора, ландшафт): записи того же .dat между концом предыдущего мира и этим .sectorsdep.fcb. */\n files(): FatRef[];\n}\nexport interface DuniaArchiveApi {\n duniaWorld(name: string): DuniaWorld | null;\n duniaWorlds(): DuniaWorld[];\n /** Все записи .dat по порядку смещений. */\n datRefs(dat: BlobFile): FatRef[];\n readRef(r: FatRef, head?: number): Promise<Uint8Array>;\n /** Запись по хешу имени (ссылки коллекций уровней — хеши путей моделей) и её имя, если известно. */\n refByHash(h: number): FatRef | undefined;\n nameOfHash(h: number): string | undefined;\n}\n\n/** Метка виртуального файла уровня (worlds/<мир>/….fc2level): дальше JSON с миром и областью секторов. */\nexport const LEVEL_MARK = 'DUNIA-LEVEL\\n';\nexport interface LevelSpec { world: string; name: string; x0: number; y0: number; w: number; h: number; terrainOnly?: boolean }\n\n/** Миры Far Cry 2 (одиночная игра, сетевые карты, DLC); дополняются найденными в списках ресурсов. */\nconst KNOWN_WORLDS = ['world1', 'world2', 'tmpla', 'mp_02_s_shanty', 'mp_06_m_ranch', 'mp_07_m_dogon', 'mp_08_m_colony', 'mp_10_l_fishingvillage',\n 'mp_11_l_savanna', 'mp_12_l_fueldepot', 'mp_14_woodlands', 'mp_15_mine', 'mp_16_airbase', 'mp_17_dunes', 'mp_18_greenhouse', 'mp_21_town',\n 'mp_24_dusttown', 'mp_25_prison', 'mp_dlc01_resort', 'mp_dlc02_lake', 'mp_dlc04_bridge', 'mp_dlc05_clinic', 'mp_dlc09_jungle', 'mp_dlc_gym'];\n\nconst FAT_RE = /\\.fat$/i;\n/** Звук и шейдеры: моделей и текстур в них нет. */\nconst SKIP_RE = /^(sound|sound_english|shadersobj)\\.fat$/i;\n/** Порядок: обновление patch — последним (перекрывает). */\nconst rank = (name: string) => (/^patch\\.fat$/i.test(name) ? 3 : /^common\\.fat$/i.test(name) ? 0 : /^worlds/i.test(name) ? 1 : 2);\nconst relPath = (f: BlobFile) => (f.webkitRelativePath || f.name).replace(/\\\\/g, '/');\n\n/** Кандидаты в XML-списки: сжатые (LZO) записи от 256 КБ, хорошо сжатые (текст). */\nconst isListCandidate = (e: FatEntry) => e.scheme === 1 && e.size >= 256 * 1024 && e.size / Math.max(1, e.csize) >= 3;\n\n/**\n * Все .fat из выбранных (или из папки игры) одним архивом; открывается по первому. Имена — из XML-списков ресурсов.\n */\nexport async function openDuniaArchive(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null> {\n const fats = siblings.filter((f) => FAT_RE.test(f.name) && !SKIP_RE.test(f.name));\n if (!fats.includes(file)) fats.push(file);\n fats.sort((a, b) => rank(a.name) - rank(b.name) || relPath(a).localeCompare(relPath(b)));\n if (fats[0] !== file) return null; // остальные .fat уже вошли в архив\n const byHash = new Map<number, { fe: FatEntry; dat: BlobFile }>();\n const missingDat: string[] = [];\n for (const f of fats) {\n const want = relPath(f).replace(FAT_RE, '.dat').toLowerCase();\n const dat = siblings.find((s) => relPath(s).toLowerCase() === want) ?? siblings.find((s) => s.name.toLowerCase() === f.name.replace(FAT_RE, '.dat').toLowerCase());\n if (!dat) { missingDat.push(f.name); continue; }\n for (const fe of parseFat(new Uint8Array(await f.arrayBuffer()))) byHash.set(fe.hash, { fe, dat });\n }\n if (!byHash.size) throw new Error(`Нет .dat рядом с ${missingDat.join(', ') || file.name}: выберите пары .fat + .dat или папку Data_Win32`);\n // имена из XML-списков ресурсов\n const names = new Map<number, string>(), seenSize = new Set<string>();\n for (const { fe, dat } of byHash.values()) {\n if (!isListCandidate(fe)) continue;\n const k = `${fe.size}/${fe.csize}`;\n if (seenSize.has(k)) continue; // копии одного списка в разных мирах\n let head: Uint8Array;\n try { head = await readFatEntry(dat, fe, 16); } catch { continue; }\n if (String.fromCharCode(...head.subarray(0, 6)) !== '<root>') continue;\n seenSize.add(k);\n try {\n const text = new TextDecoder('latin1').decode(await readFatEntry(dat, fe));\n for (const n of namesFromResourceXml(text)) { const h = duniaHash(n); if (byHash.has(h) && !names.has(h)) names.set(h, n); }\n } catch { /* список повреждён — пропускаем */ }\n }\n // миры: game.xml + sectorsdep.fcb; файлы мира — участок .dat до его sectorsdep (миры пишутся в .dat подряд)\n const worldNames = new Set(KNOWN_WORLDS);\n for (const n of names.values()) { const m = /^worlds\\\\([^\\\\]+)\\\\/i.exec(n); if (m) worldNames.add(m[1]!.toLowerCase()); }\n const worlds = new Map<string, { name: string; gx: FatRef; end: number; dat: BlobFile; start: number }>();\n for (const w of worldNames) {\n const gx = byHash.get(duniaHash(`worlds\\\\${w}\\\\generated\\\\${w}.game.xml`)), sd = byHash.get(duniaHash(`worlds\\\\${w}\\\\generated\\\\${w}.sectorsdep.fcb`));\n if (gx && sd) worlds.set(w, { name: w, gx, end: sd.fe.off, dat: sd.dat, start: 0 });\n }\n for (const w of worlds.values()) for (const o of worlds.values()) if (o.dat === w.dat && o.end < w.end && o.end > w.start) w.start = o.end;\n const levelEntries: DuniaEntry[] = [];\n await Promise.all([...worlds.values()].map(async (w) => {\n const spec = (name: string, x0: number, y0: number, cw: number, ch: number, terrainOnly = false): LevelSpec => ({ world: w.name, name, x0, y0, w: cw, h: ch, ...(terrainOnly ? { terrainOnly } : {}) });\n const add = (path: string, sp: LevelSpec) => { const text = LEVEL_MARK + JSON.stringify(sp); levelEntries.push({ path, size: text.length, fe: w.gx.fe, dat: w.gx.dat, text }); };\n let maps: Array<{ name: string; x: number; y: number; cx: number; cy: number }> = [], gx = 0, gy = 0;\n try {\n const { xmlElements } = await import('./rml');\n for (const el of xmlElements(await readFatEntry(w.gx.dat, w.gx.fe))) {\n if (el.name === 'GridMapSectors' && !gx) { gx = +el.attrs.CountX! || 0; gy = +el.attrs.CountY! || 0; }\n if (el.name === 'Map' && el.attrs.Name) maps.push({ name: el.attrs.Name.split(/[\\\\/]/).pop()!.replace(/\\.nomad$/i, ''), x: +el.attrs.SectorOffsetX! || 0, y: +el.attrs.SectorOffsetY! || 0, cx: +el.attrs.CountX! || 0, cy: +el.attrs.CountY! || 0 });\n }\n } catch { maps = []; }\n maps = maps.filter((m) => m.cx > 0 && m.cy > 0);\n if (maps.length > 1) {\n // большой мир: карты по 16×16 секторов (1 км) с объектами + весь мир одним ландшафтом\n const W = Math.max(...maps.map((m) => m.x + m.cx)), H = Math.max(...maps.map((m) => m.y + m.cy));\n add(`worlds/${w.name}/${w.name}.fc2level`, spec(w.name, 0, 0, W, H, true));\n for (const m of maps) add(`worlds/${w.name}/maps/${m.name.toLowerCase()}.fc2level`, spec(m.name, m.x, m.y, m.cx, m.cy));\n } else {\n const m = maps[0];\n add(`worlds/${w.name}/${w.name}.fc2level`, spec(m?.name ?? w.name, m?.x ?? 0, m?.y ?? 0, m?.cx || gx || 16, m?.cy || gy || 16));\n }\n }));\n const levelIndex = new Map(levelEntries.map((e) => [e.path, e]));\n const entries: DuniaEntry[] = [...levelEntries], byPath = new Map<number, DuniaEntry>();\n const entryOf = (h: number, path: string): DuniaEntry | null => {\n let e = byPath.get(h);\n if (e) return e;\n let x = byHash.get(h);\n if (!x) return null;\n // текстура высокого качества: старший уровень лежит отдельно в «<имя>_mip0.xbt» (полный XBT/DDS 1024²) — берём его\n if (/\\.xbt$/i.test(path)) { const hd = byHash.get(duniaHash(path.replace(/\\.xbt$/i, '_mip0.xbt'))); if (hd && hd.fe.size > 0) x = hd; }\n e = { path, size: x.fe.size, fe: x.fe, dat: x.dat };\n byPath.set(h, e);\n return e;\n };\n for (const [h, n] of names) entries.push(entryOf(h, n.replace(/\\\\/g, '/').toLowerCase())!);\n entries.sort((a, b) => a.path.localeCompare(b.path));\n const refs = [...byHash.values()];\n const byDat = new Map<BlobFile, FatRef[]>();\n const datRefs = (dat: BlobFile) => {\n let l = byDat.get(dat);\n if (!l) { l = refs.filter((r) => r.dat === dat).sort((a, b) => a.fe.off - b.fe.off); byDat.set(dat, l); }\n return l;\n };\n const worldOf = (w: { name: string; gx: FatRef; end: number; dat: BlobFile; start: number }): DuniaWorld => ({\n name: w.name, gameXml: w.gx, dat: w.dat, end: w.end, files: () => datRefs(w.dat).filter((r) => r.fe.off >= w.start && r.fe.off < w.end),\n });\n const api: DuniaArchiveApi = {\n duniaWorld(name) { const w = worlds.get(name.toLowerCase()); return w ? worldOf(w) : null; },\n duniaWorlds: () => [...worlds.values()].map(worldOf),\n datRefs,\n readRef: (r, head) => readFatEntry(r.dat, r.fe, head),\n refByHash: (h) => byHash.get(h >>> 0),\n nameOfHash: (h) => names.get(h >>> 0),\n };\n const arch = createArchive<DuniaEntry>({\n name: fats.length > 1 ? `${fats[0]!.name} … (${fats.length} .fat)` : file.name,\n kind: 'dunia-fat',\n version: 5,\n entries,\n // путь из ссылок модели/материала — по хешу, даже если его нет в списках\n lookup: (p) => { const k = p.replace(/\\\\/g, '/').toLowerCase(); return levelIndex.get(k) ?? entryOf(duniaHash(p), k); },\n read: (e) => (e.text !== undefined ? Promise.resolve(new TextEncoder().encode(e.text)) : readFatEntry(e.dat, e.fe)),\n findTexture: (p) => (/\\.xbt$/i.test(p) ? entryOf(duniaHash(p), p.replace(/\\\\/g, '/').toLowerCase()) : null),\n });\n return Object.assign(arch, api);\n}\n\n/** Миры архива Dunia (для уровней), если архив — Dunia. */\nexport const duniaApi = (a: unknown): DuniaArchiveApi | null => (a && typeof (a as DuniaArchiveApi).duniaWorld === 'function' ? (a as DuniaArchiveApi) : null);\n"],"names":["out"],"mappings":"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"}
1
+ {"version":3,"file":"fat.js","sources":["../src/fat.ts"],"sourcesContent":["// Dunia archives (Far Cry 2): a pair of <name>.fat (table of contents) + <name>.dat (data).\n// .fat: \"2TAF\" (FAT2 in little-endian), u32 version 5, u32 flags (platform/compression), u32 record count, then 16 bytes each:\n// u32 name hash, u32 (unpacked size << 2 | scheme: 0 = uncompressed, 1 = LZO1X),\n// u32 (low 2 bits of the offset << 30 | size in .dat), u32 offset >> 2; a trailing u32 0.\n// The archive has no names, only hashes: CRC-32 of the lower-case path with backslashes (\"graphics\\weapons\\…\\ak47.xbg\").\n// Names come from the game itself: XML resource dependency lists (\"<root>\" with CGeometryResource / CMaterialResource /\n// CTextureResource ID=\"…\") stored in the same archives; the remaining files are reachable only by paths from references (lookup).\n// Texture \"x.xbt\" is read from \"x_mip0.xbt\" if present: it holds the top level (up to 1024²) as a separate XBT.\nimport { lzo1xDecompress } from '@gmvjs/codecs';\nimport { crc32 } from '@gmvjs/core/writers/zip';\nimport { createArchive, readRange } from '@gmvjs/core/archives/common';\nimport type { Archive, ArchiveEntry, BlobFile } from '@gmvjs/core/types';\n\nexport interface FatEntry { hash: number; size: number; scheme: number; csize: number; off: number }\n\nexport function parseFat(u: Uint8Array): FatEntry[] {\n const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);\n if (u.length < 16 || dv.getUint32(0, true) !== 0x46415432) throw new Error('Не архив Dunia (нет «FAT2»)');\n const ver = dv.getUint32(4, true);\n if (ver !== 5) throw new Error(`Архив Dunia версии ${ver} не поддерживается (нужна 5 — Far Cry 2)`);\n const n = dv.getUint32(12, true);\n if (16 + n * 16 > u.length) throw new Error('.fat: оглавление обрезано');\n const out: FatEntry[] = [];\n for (let i = 0; i < n; i++) {\n const o = 16 + i * 16, a = dv.getUint32(o + 4, true), b = dv.getUint32(o + 8, true), c = dv.getUint32(o + 12, true);\n // uncompressed records (scheme 0) have no unpacked size (0): it equals the size in .dat\n const scheme = a & 3, csize = b & 0x3fffffff;\n out.push({ hash: dv.getUint32(o, true), size: scheme === 0 ? csize : a >>> 2, scheme, csize, off: c * 4 + (b >>> 30) });\n }\n return out;\n}\n\n/** Unpack a record; head: only the first head bytes (for recognition), without reading the whole record. */\nexport async function readFatEntry(dat: BlobFile, e: FatEntry, head = 0): Promise<Uint8Array> {\n if (e.scheme === 0) return readRange(dat, e.off, e.off + (head ? Math.min(head, e.csize) : e.csize));\n if (e.scheme !== 1) throw new Error(`Dunia: схема сжатия ${e.scheme} не поддерживается`);\n if (head) {\n // start of an LZO stream: literals and back references fit into the first bytes; decompression aborts once the start is ready\n // a buffer with margin: a first literal run longer than head does not fit into a small buffer and is not copied at all\n // a small start (header recognition): first from 512 bytes; if that is not enough (the first run is longer), from 2048\n for (const take of head <= 64 ? [512, 2048] : [2048]) {\n const out = new Uint8Array(Math.min(Math.max(head, 1024), e.size));\n try { lzo1xDecompress(await readRange(dat, e.off, e.off + Math.min(e.csize, take)), 0, out); } catch { /* truncated: expected */ }\n const r = out.subarray(0, Math.min(head, out.length));\n if (take >= e.csize || r.some((b) => b !== 0)) return r;\n }\n return new Uint8Array(0);\n }\n const out = new Uint8Array(e.size);\n lzo1xDecompress(await readRange(dat, e.off, e.off + e.csize), 0, out);\n return out;\n}\n\n/** Dunia (FC2) file name hash: CRC-32 of the lower-case path with \"\\\" separators. */\nexport const duniaHash = (path: string): number => crc32(new TextEncoder().encode(path.replace(/\\//g, '\\\\').toLowerCase())) >>> 0;\n\n/** Paths from XML dependency lists (ID=\"…\" and crc_ID). */\nexport function namesFromResourceXml(text: string): string[] {\n const out: string[] = [];\n for (const m of text.matchAll(/ID=\"([^\"]+\\.[A-Za-z0-9]{2,5})\"/g)) out.push(m[1]!);\n return out;\n}\n\ninterface DuniaEntry extends ArchiveEntry { fe: FatEntry; dat: BlobFile; text?: string }\n\n/** A .fat record together with its .dat. */\nexport interface FatRef { fe: FatEntry; dat: BlobFile }\n\n/** World (level): worlds/<name>/generated/<name>.game.xml and .sectorsdep.fcb. */\nexport interface DuniaWorld {\n name: string;\n gameXml: FatRef;\n /** The world's .dat and the offset of its .sectorsdep.fcb. */\n dat: BlobFile;\n end: number;\n /** Unnamed world files (sectors, terrain): records of the same .dat between the end of the previous world and this .sectorsdep.fcb. */\n files(): FatRef[];\n}\nexport interface DuniaArchiveApi {\n duniaWorld(name: string): DuniaWorld | null;\n duniaWorlds(): DuniaWorld[];\n /** All .dat records in offset order. */\n datRefs(dat: BlobFile): FatRef[];\n readRef(r: FatRef, head?: number): Promise<Uint8Array>;\n /** Record by name hash (level collection references are model path hashes) and its name, if known. */\n refByHash(h: number): FatRef | undefined;\n nameOfHash(h: number): string | undefined;\n}\n\n/** Marker of a virtual level file (worlds/<world>/….fc2level): followed by JSON with the world and the sector area. */\nexport const LEVEL_MARK = 'DUNIA-LEVEL\\n';\nexport interface LevelSpec { world: string; name: string; x0: number; y0: number; w: number; h: number; terrainOnly?: boolean }\n\n/** Far Cry 2 worlds (single player, multiplayer maps, DLC); extended with those found in resource lists. */\nconst KNOWN_WORLDS = ['world1', 'world2', 'tmpla', 'mp_02_s_shanty', 'mp_06_m_ranch', 'mp_07_m_dogon', 'mp_08_m_colony', 'mp_10_l_fishingvillage',\n 'mp_11_l_savanna', 'mp_12_l_fueldepot', 'mp_14_woodlands', 'mp_15_mine', 'mp_16_airbase', 'mp_17_dunes', 'mp_18_greenhouse', 'mp_21_town',\n 'mp_24_dusttown', 'mp_25_prison', 'mp_dlc01_resort', 'mp_dlc02_lake', 'mp_dlc04_bridge', 'mp_dlc05_clinic', 'mp_dlc09_jungle', 'mp_dlc_gym'];\n\nconst FAT_RE = /\\.fat$/i;\n/** Sound and shaders: no models or textures inside. */\nconst SKIP_RE = /^(sound|sound_english|shadersobj)\\.fat$/i;\n/** Order: the patch update goes last (overrides). */\nconst rank = (name: string) => (/^patch\\.fat$/i.test(name) ? 3 : /^common\\.fat$/i.test(name) ? 0 : /^worlds/i.test(name) ? 1 : 2);\nconst relPath = (f: BlobFile) => (f.webkitRelativePath || f.name).replace(/\\\\/g, '/');\n\n/** Candidate XML lists: compressed (LZO) records of 256 KB and more that compress well (text). */\nconst isListCandidate = (e: FatEntry) => e.scheme === 1 && e.size >= 256 * 1024 && e.size / Math.max(1, e.csize) >= 3;\n\n/**\n * All .fat files from the selection (or the game folder) as one archive; opened via the first. Names come from XML resource lists.\n */\nexport async function openDuniaArchive(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null> {\n const fats = siblings.filter((f) => FAT_RE.test(f.name) && !SKIP_RE.test(f.name));\n if (!fats.includes(file)) fats.push(file);\n fats.sort((a, b) => rank(a.name) - rank(b.name) || relPath(a).localeCompare(relPath(b)));\n if (fats[0] !== file) return null; // the other .fat files are already part of the archive\n const byHash = new Map<number, { fe: FatEntry; dat: BlobFile }>();\n const missingDat: string[] = [];\n for (const f of fats) {\n const want = relPath(f).replace(FAT_RE, '.dat').toLowerCase();\n const dat = siblings.find((s) => relPath(s).toLowerCase() === want) ?? siblings.find((s) => s.name.toLowerCase() === f.name.replace(FAT_RE, '.dat').toLowerCase());\n if (!dat) { missingDat.push(f.name); continue; }\n for (const fe of parseFat(new Uint8Array(await f.arrayBuffer()))) byHash.set(fe.hash, { fe, dat });\n }\n if (!byHash.size) throw new Error(`Нет .dat рядом с ${missingDat.join(', ') || file.name}: выберите пары .fat + .dat или папку Data_Win32`);\n // names from XML resource lists\n const names = new Map<number, string>(), seenSize = new Set<string>();\n for (const { fe, dat } of byHash.values()) {\n if (!isListCandidate(fe)) continue;\n const k = `${fe.size}/${fe.csize}`;\n if (seenSize.has(k)) continue; // copies of the same list in different worlds\n let head: Uint8Array;\n try { head = await readFatEntry(dat, fe, 16); } catch { continue; }\n if (String.fromCharCode(...head.subarray(0, 6)) !== '<root>') continue;\n seenSize.add(k);\n try {\n const text = new TextDecoder('latin1').decode(await readFatEntry(dat, fe));\n for (const n of namesFromResourceXml(text)) { const h = duniaHash(n); if (byHash.has(h) && !names.has(h)) names.set(h, n); }\n } catch { /* damaged list: skip */ }\n }\n // worlds: game.xml + sectorsdep.fcb; world files are the .dat range before its sectorsdep (worlds are written to .dat in sequence)\n const worldNames = new Set(KNOWN_WORLDS);\n for (const n of names.values()) { const m = /^worlds\\\\([^\\\\]+)\\\\/i.exec(n); if (m) worldNames.add(m[1]!.toLowerCase()); }\n const worlds = new Map<string, { name: string; gx: FatRef; end: number; dat: BlobFile; start: number }>();\n for (const w of worldNames) {\n const gx = byHash.get(duniaHash(`worlds\\\\${w}\\\\generated\\\\${w}.game.xml`)), sd = byHash.get(duniaHash(`worlds\\\\${w}\\\\generated\\\\${w}.sectorsdep.fcb`));\n if (gx && sd) worlds.set(w, { name: w, gx, end: sd.fe.off, dat: sd.dat, start: 0 });\n }\n for (const w of worlds.values()) for (const o of worlds.values()) if (o.dat === w.dat && o.end < w.end && o.end > w.start) w.start = o.end;\n const levelEntries: DuniaEntry[] = [];\n await Promise.all([...worlds.values()].map(async (w) => {\n const spec = (name: string, x0: number, y0: number, cw: number, ch: number, terrainOnly = false): LevelSpec => ({ world: w.name, name, x0, y0, w: cw, h: ch, ...(terrainOnly ? { terrainOnly } : {}) });\n const add = (path: string, sp: LevelSpec) => { const text = LEVEL_MARK + JSON.stringify(sp); levelEntries.push({ path, size: text.length, fe: w.gx.fe, dat: w.gx.dat, text }); };\n let maps: Array<{ name: string; x: number; y: number; cx: number; cy: number }> = [], gx = 0, gy = 0;\n try {\n const { xmlElements } = await import('./rml');\n for (const el of xmlElements(await readFatEntry(w.gx.dat, w.gx.fe))) {\n if (el.name === 'GridMapSectors' && !gx) { gx = +el.attrs.CountX! || 0; gy = +el.attrs.CountY! || 0; }\n if (el.name === 'Map' && el.attrs.Name) maps.push({ name: el.attrs.Name.split(/[\\\\/]/).pop()!.replace(/\\.nomad$/i, ''), x: +el.attrs.SectorOffsetX! || 0, y: +el.attrs.SectorOffsetY! || 0, cx: +el.attrs.CountX! || 0, cy: +el.attrs.CountY! || 0 });\n }\n } catch { maps = []; }\n maps = maps.filter((m) => m.cx > 0 && m.cy > 0);\n if (maps.length > 1) {\n // a large world: maps of 16×16 sectors (1 km) with objects + the whole world as one terrain\n const W = Math.max(...maps.map((m) => m.x + m.cx)), H = Math.max(...maps.map((m) => m.y + m.cy));\n add(`worlds/${w.name}/${w.name}.fc2level`, spec(w.name, 0, 0, W, H, true));\n for (const m of maps) add(`worlds/${w.name}/maps/${m.name.toLowerCase()}.fc2level`, spec(m.name, m.x, m.y, m.cx, m.cy));\n } else {\n const m = maps[0];\n add(`worlds/${w.name}/${w.name}.fc2level`, spec(m?.name ?? w.name, m?.x ?? 0, m?.y ?? 0, m?.cx || gx || 16, m?.cy || gy || 16));\n }\n }));\n const levelIndex = new Map(levelEntries.map((e) => [e.path, e]));\n const entries: DuniaEntry[] = [...levelEntries], byPath = new Map<number, DuniaEntry>();\n const entryOf = (h: number, path: string): DuniaEntry | null => {\n let e = byPath.get(h);\n if (e) return e;\n let x = byHash.get(h);\n if (!x) return null;\n // high-quality texture: the top level lives separately in \"<name>_mip0.xbt\" (a full 1024² XBT/DDS), take it\n if (/\\.xbt$/i.test(path)) { const hd = byHash.get(duniaHash(path.replace(/\\.xbt$/i, '_mip0.xbt'))); if (hd && hd.fe.size > 0) x = hd; }\n e = { path, size: x.fe.size, fe: x.fe, dat: x.dat };\n byPath.set(h, e);\n return e;\n };\n for (const [h, n] of names) entries.push(entryOf(h, n.replace(/\\\\/g, '/').toLowerCase())!);\n entries.sort((a, b) => a.path.localeCompare(b.path));\n const refs = [...byHash.values()];\n const byDat = new Map<BlobFile, FatRef[]>();\n const datRefs = (dat: BlobFile) => {\n let l = byDat.get(dat);\n if (!l) { l = refs.filter((r) => r.dat === dat).sort((a, b) => a.fe.off - b.fe.off); byDat.set(dat, l); }\n return l;\n };\n const worldOf = (w: { name: string; gx: FatRef; end: number; dat: BlobFile; start: number }): DuniaWorld => ({\n name: w.name, gameXml: w.gx, dat: w.dat, end: w.end, files: () => datRefs(w.dat).filter((r) => r.fe.off >= w.start && r.fe.off < w.end),\n });\n const api: DuniaArchiveApi = {\n duniaWorld(name) { const w = worlds.get(name.toLowerCase()); return w ? worldOf(w) : null; },\n duniaWorlds: () => [...worlds.values()].map(worldOf),\n datRefs,\n readRef: (r, head) => readFatEntry(r.dat, r.fe, head),\n refByHash: (h) => byHash.get(h >>> 0),\n nameOfHash: (h) => names.get(h >>> 0),\n };\n const arch = createArchive<DuniaEntry>({\n name: fats.length > 1 ? `${fats[0]!.name} … (${fats.length} .fat)` : file.name,\n kind: 'dunia-fat',\n version: 5,\n entries,\n // a path from model/material references: by hash, even if it is not in the lists\n lookup: (p) => { const k = p.replace(/\\\\/g, '/').toLowerCase(); return levelIndex.get(k) ?? entryOf(duniaHash(p), k); },\n read: (e) => (e.text !== undefined ? Promise.resolve(new TextEncoder().encode(e.text)) : readFatEntry(e.dat, e.fe)),\n findTexture: (p) => (/\\.xbt$/i.test(p) ? entryOf(duniaHash(p), p.replace(/\\\\/g, '/').toLowerCase()) : null),\n });\n return Object.assign(arch, api);\n}\n\n/** Worlds of a Dunia archive (for levels), if the archive is Dunia. */\nexport const duniaApi = (a: unknown): DuniaArchiveApi | null => (a && typeof (a as DuniaArchiveApi).duniaWorld === 'function' ? (a as DuniaArchiveApi) : null);\n"],"names":["out"],"mappings":"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"}
package/dist/fcb.d.ts CHANGED
@@ -3,12 +3,12 @@ export interface FcbObject {
3
3
  values: Map<number, Uint8Array>;
4
4
  children: FcbObject[];
5
5
  }
6
- /** CRC-32 имени класса/поля, как в .fcb. */
6
+ /** CRC-32 of a class/field name, as in .fcb. */
7
7
  export declare const fcbHash: (name: string) => number;
8
8
  export declare function parseFcb(u: Uint8Array): FcbObject;
9
9
  export declare const fcbString: (v: Uint8Array | undefined) => string | undefined;
10
10
  export declare const fcbU32: (v: Uint8Array | undefined) => number | undefined;
11
11
  export declare const fcbF32: (v: Uint8Array | undefined) => number | undefined;
12
12
  export declare const fcbVec3: (v: Uint8Array | undefined) => number[] | undefined;
13
- /** Обход дерева: все объекты с типом type. */
13
+ /** Tree walk: all objects of type `type`. */
14
14
  export declare function fcbFind(root: FcbObject, type: number, out?: FcbObject[], seen?: Set<FcbObject>): FcbObject[];
package/dist/fcb.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fcb.js","sources":["../src/fcb.ts"],"sourcesContent":["// Двоичные объекты Dunia (.fcb, «nbCF» — FCbn в little-endian), версия 2 (Far Cry 2).\n// Заголовок: u32 магия, u16 версия 2, u16 флаги 0, u32 всего объектов, u32 всего значений; затем корневой объект.\n// Объект: счётчик детей (u8; 0xFF — далее u32; 0xFE — u32 номер уже прочитанного объекта — ссылка),\n// u32 хеш типа (CRC-32 имени класса: «Sector», «Entity», «CGraphicComponent»…), счётчик значений,\n// значения: u32 хеш имени, размер (счётчик; 0xFE — u32 смещение назад до такого же значения), байты; затем дети.\n// Имён в файле нет — только CRC-32 (по описанию классов Gibbed.Dunia, configs/Far Cry 2/binary_classes.xml).\nimport { crc32 } from '@gmvjs/core/writers/zip';\n\nexport interface FcbObject { type: number; values: Map<number, Uint8Array>; children: FcbObject[] }\n\n/** CRC-32 имени класса/поля, как в .fcb. */\nexport const fcbHash = (name: string): number => crc32(new TextEncoder().encode(name)) >>> 0;\n\nexport function parseFcb(u: Uint8Array): FcbObject {\n const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);\n if (u.length < 16 || dv.getUint32(0, true) !== 0x4643626e) throw new Error('Не .fcb (нет «nbCF»)');\n if (dv.getUint16(4, true) !== 2) throw new Error(`.fcb версии ${dv.getUint16(4, true)} не поддерживается (нужна 2 — Far Cry 2)`);\n let p = 16;\n const need = (n: number) => { if (p + n > u.length) throw new Error('.fcb обрезан'); };\n const count = (): [number, boolean] => {\n need(1);\n const b = u[p++]!;\n if (b < 0xfe) return [b, false];\n need(4); const v = dv.getUint32(p, true); p += 4;\n return [v, b === 0xfe];\n };\n const pointers: FcbObject[] = [];\n const obj = (depth: number): FcbObject => {\n if (depth > 64) throw new Error('.fcb: слишком глубокая вложенность');\n const [nChildren, isRef] = count();\n if (isRef) { const o = pointers[nChildren]; if (!o) throw new Error('.fcb: ссылка на неизвестный объект'); return o; }\n const o: FcbObject = { type: 0, values: new Map(), children: [] };\n pointers.push(o);\n need(4); o.type = dv.getUint32(p, true); p += 4;\n const [nValues] = count();\n for (let i = 0; i < nValues; i++) {\n need(4); const name = dv.getUint32(p, true); p += 4;\n const at = p;\n const [size, back] = count();\n if (back) {\n // то же значение уже записано раньше: смещение назад от начала этого счётчика\n const save = p; p = at - size;\n const [s2, again] = count();\n if (again) throw new Error('.fcb: двойная ссылка значения');\n need(s2); o.values.set(name, u.subarray(p, p + s2)); p = save;\n } else { need(size); o.values.set(name, u.subarray(p, p + size)); p += size; }\n }\n for (let i = 0; i < nChildren; i++) o.children.push(obj(depth + 1));\n return o;\n };\n return obj(0);\n}\n\nconst dvOf = (v: Uint8Array) => new DataView(v.buffer, v.byteOffset, v.byteLength);\nexport const fcbString = (v: Uint8Array | undefined): string | undefined => {\n if (!v) return undefined;\n let e = v.indexOf(0); if (e < 0) e = v.length;\n return new TextDecoder('latin1').decode(v.subarray(0, e));\n};\nexport const fcbU32 = (v: Uint8Array | undefined): number | undefined => (v && v.length >= 4 ? dvOf(v).getUint32(0, true) : undefined);\nexport const fcbF32 = (v: Uint8Array | undefined): number | undefined => (v && v.length >= 4 ? dvOf(v).getFloat32(0, true) : undefined);\nexport const fcbVec3 = (v: Uint8Array | undefined): number[] | undefined => {\n if (!v || v.length < 12) return undefined;\n const d = dvOf(v); return [d.getFloat32(0, true), d.getFloat32(4, true), d.getFloat32(8, true)];\n};\n\n/** Обход дерева: все объекты с типом type. */\nexport function fcbFind(root: FcbObject, type: number, out: FcbObject[] = [], seen = new Set<FcbObject>()): FcbObject[] {\n if (seen.has(root)) return out;\n seen.add(root);\n if (root.type === type) out.push(root);\n for (const c of root.children) fcbFind(c, type, out, seen);\n return out;\n}\n"],"names":["o"],"mappings":"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"}
1
+ {"version":3,"file":"fcb.js","sources":["../src/fcb.ts"],"sourcesContent":["// Dunia binary objects (.fcb, \"nbCF\" = FCbn in little-endian), version 2 (Far Cry 2).\n// Header: u32 magic, u16 version 2, u16 flags 0, u32 total objects, u32 total values; then the root object.\n// Object: child counter (u8; 0xFF = u32 follows; 0xFE = u32 number of an already read object, a reference),\n// u32 type hash (CRC-32 of the class name: \"Sector\", \"Entity\", \"CGraphicComponent\"…), value counter,\n// values: u32 name hash, size (counter; 0xFE = u32 back offset to an identical value), bytes; then the children.\n// The file has no names, only CRC-32 (per the Gibbed.Dunia class descriptions, configs/Far Cry 2/binary_classes.xml).\nimport { crc32 } from '@gmvjs/core/writers/zip';\n\nexport interface FcbObject { type: number; values: Map<number, Uint8Array>; children: FcbObject[] }\n\n/** CRC-32 of a class/field name, as in .fcb. */\nexport const fcbHash = (name: string): number => crc32(new TextEncoder().encode(name)) >>> 0;\n\nexport function parseFcb(u: Uint8Array): FcbObject {\n const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);\n if (u.length < 16 || dv.getUint32(0, true) !== 0x4643626e) throw new Error('Не .fcb (нет «nbCF»)');\n if (dv.getUint16(4, true) !== 2) throw new Error(`.fcb версии ${dv.getUint16(4, true)} не поддерживается (нужна 2 — Far Cry 2)`);\n let p = 16;\n const need = (n: number) => { if (p + n > u.length) throw new Error('.fcb обрезан'); };\n const count = (): [number, boolean] => {\n need(1);\n const b = u[p++]!;\n if (b < 0xfe) return [b, false];\n need(4); const v = dv.getUint32(p, true); p += 4;\n return [v, b === 0xfe];\n };\n const pointers: FcbObject[] = [];\n const obj = (depth: number): FcbObject => {\n if (depth > 64) throw new Error('.fcb: слишком глубокая вложенность');\n const [nChildren, isRef] = count();\n if (isRef) { const o = pointers[nChildren]; if (!o) throw new Error('.fcb: ссылка на неизвестный объект'); return o; }\n const o: FcbObject = { type: 0, values: new Map(), children: [] };\n pointers.push(o);\n need(4); o.type = dv.getUint32(p, true); p += 4;\n const [nValues] = count();\n for (let i = 0; i < nValues; i++) {\n need(4); const name = dv.getUint32(p, true); p += 4;\n const at = p;\n const [size, back] = count();\n if (back) {\n // the same value was written earlier: back offset from the start of this counter\n const save = p; p = at - size;\n const [s2, again] = count();\n if (again) throw new Error('.fcb: двойная ссылка значения');\n need(s2); o.values.set(name, u.subarray(p, p + s2)); p = save;\n } else { need(size); o.values.set(name, u.subarray(p, p + size)); p += size; }\n }\n for (let i = 0; i < nChildren; i++) o.children.push(obj(depth + 1));\n return o;\n };\n return obj(0);\n}\n\nconst dvOf = (v: Uint8Array) => new DataView(v.buffer, v.byteOffset, v.byteLength);\nexport const fcbString = (v: Uint8Array | undefined): string | undefined => {\n if (!v) return undefined;\n let e = v.indexOf(0); if (e < 0) e = v.length;\n return new TextDecoder('latin1').decode(v.subarray(0, e));\n};\nexport const fcbU32 = (v: Uint8Array | undefined): number | undefined => (v && v.length >= 4 ? dvOf(v).getUint32(0, true) : undefined);\nexport const fcbF32 = (v: Uint8Array | undefined): number | undefined => (v && v.length >= 4 ? dvOf(v).getFloat32(0, true) : undefined);\nexport const fcbVec3 = (v: Uint8Array | undefined): number[] | undefined => {\n if (!v || v.length < 12) return undefined;\n const d = dvOf(v); return [d.getFloat32(0, true), d.getFloat32(4, true), d.getFloat32(8, true)];\n};\n\n/** Tree walk: all objects of type `type`. */\nexport function fcbFind(root: FcbObject, type: number, out: FcbObject[] = [], seen = new Set<FcbObject>()): FcbObject[] {\n if (seen.has(root)) return out;\n seen.add(root);\n if (root.type === type) out.push(root);\n for (const c of root.children) fcbFind(c, type, out, seen);\n return out;\n}\n"],"names":["o"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"farcry2.js","sources":["../../src/games/farcry2.ts"],"sourcesContent":["// Far Cry 2 (2008) — подпуть `@gmvjs/dunia1/farcry2`: пресет игры и движок только с ней.\nimport { pickGames } from '@gmvjs/core/plugin';\nimport { dunia1Engine } from '../index';\n\nexport const engine = pickGames(dunia1Engine, 'farcry2');\nexport const preset = engine.games![0]!;\nexport default { engine, preset };\n"],"names":[],"mappings":";;AAIO,MAAM,SAAS,UAAU,cAAc,SAAS;AAChD,MAAM,SAAS,OAAO,MAAO,CAAC;AACrC,MAAA,UAAe,EAAE,QAAQ,OAAA;"}
1
+ {"version":3,"file":"farcry2.js","sources":["../../src/games/farcry2.ts"],"sourcesContent":["// Far Cry 2 (2008) — subpath `@gmvjs/dunia1/farcry2`: the game preset and an engine limited to it.\nimport { pickGames } from '@gmvjs/core/plugin';\nimport { dunia1Engine } from '../index';\n\nexport const engine = pickGames(dunia1Engine, 'farcry2');\nexport const preset = engine.games![0]!;\nexport default { engine, preset };\n"],"names":[],"mappings":";;AAIO,MAAM,SAAS,UAAU,cAAc,SAAS;AAChD,MAAM,SAAS,OAAO,MAAO,CAAC;AACrC,MAAA,UAAe,EAAE,QAAQ,OAAA;"}
package/dist/i18n.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","sources":["../src/i18n.ts"],"sourcesContent":["// Английские тексты движка dunia1 (русские — в самих плагинах).\nimport type { EngineTexts } from '@gmvjs/core/plugin';\n\nexport const EN: EngineTexts = {\n \"presets\": {\n \"farcry2\": {\n \"hint\": \"Open the game folder (or Data_Win32): the .fat/.dat archives connect by themselves and file names are taken from the game's own resource lists. Models are graphics/…/*.xbg; levels are worlds/<world>/maps/*.fc2level (a 1×1 km map: terrain and objects) and worlds/<world>/<world>.fc2level (the whole world, terrain only).\",\n \"archiveLabel\": \"Open *.fat + *.dat…\"\n }\n }\n};\n"],"names":[],"mappings":"AAGO,MAAM,KAAkB;AAAA,EAC7B,WAAW;AAAA,IACT,WAAW;AAAA,MACT,QAAQ;AAAA,MACR,gBAAgB;AAAA,IAAA;AAAA,EAClB;AAEJ;"}
1
+ {"version":3,"file":"i18n.js","sources":["../src/i18n.ts"],"sourcesContent":["// English texts of the dunia1 engine (Russian ones live in the plugins themselves).\nimport type { EngineTexts } from '@gmvjs/core/plugin';\n\nexport const EN: EngineTexts = {\n \"presets\": {\n \"farcry2\": {\n \"hint\": \"Open the game folder (or Data_Win32): the .fat/.dat archives connect by themselves and file names are taken from the game's own resource lists. Models are graphics/…/*.xbg; levels are worlds/<world>/maps/*.fc2level (a 1×1 km map: terrain and objects) and worlds/<world>/<world>.fc2level (the whole world, terrain only).\",\n \"archiveLabel\": \"Open *.fat + *.dat…\"\n }\n }\n};\n"],"names":[],"mappings":"AAGO,MAAM,KAAkB;AAAA,EAC7B,WAAW;AAAA,IACT,WAAW;AAAA,MACT,QAAQ;AAAA,MACR,gBAAgB;AAAA,IAAA;AAAA,EAClB;AAEJ;"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["// Dunia Engine (Far Cry 2): архивы .fat/.dat (имена по хешу, из XML-списков ресурсов игры), модели .xbg, материалы .xbm,\n// текстуры .xbt, анимации .mab (скелет — *_ref.skeleton в папке модели или выше).\nimport { defineEngine, type GamePreset } from '@gmvjs/core/plugin';\nimport { EN } from './i18n';\n\nconst ui: GamePreset['ui'] = { archives: true, archiveExts: ['fat', 'dat'], archiveLabel: 'Открыть *.fat + *.dat…' };\n\nexport const dunia1Engine = defineEngine({\n id: 'dunia1',\n title: 'Dunia Engine 1 (Far Cry 2)',\n i18n: { en: EN },\n codecs: [{ id: 'xbt', extensions: ['xbt'], decode: async (buf) => (await import('./xbt')).decodeXbt(buf) }],\n formats: [\n {\n id: 'dunia-xbg',\n title: 'Far Cry 2: модель .xbg',\n engine: 'dunia1',\n extensions: ['xbg'],\n motionExtensions: ['mab'],\n status: 'beta',\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 4)) === 'HSEM' ? 1 : 0),\n load: async (main, ctx) => (await import('./xbg')).loadXbg(main, ctx),\n afterLoad: async (ctx) => {\n const arc = ctx.main.archive;\n if (!arc || !ctx.model.loadMotions) return;\n const list = (await import('./xbg')).listMabMotions(ctx.model, arc);\n if (list.length) ctx.addMotions(list, 'animations');\n },\n },\n {\n id: 'dunia-level',\n title: 'Far Cry 2: уровень (worlds/<мир>/maps/*.fc2level — карта 1×1 км с объектами; <мир>.fc2level — весь мир)',\n engine: 'dunia1',\n extensions: ['fc2level'],\n status: 'beta',\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 12)) === 'DUNIA-LEVEL\\n' ? 1 : 0),\n load: async (main, ctx) => {\n const text = new TextDecoder().decode(await main.arrayBuffer());\n return (await import('./level')).loadDuniaLevel(text, main, ctx);\n },\n },\n ],\n archives: [\n {\n id: 'dunia-fat',\n title: 'Far Cry 2: архивы .fat + .dat',\n engine: 'dunia1',\n extensions: ['fat'],\n companionExtensions: ['dat'],\n openFromFolder: true,\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 4)) === '2TAF' ? 1 : 0),\n open: async (file, siblings) => (await import('./fat')).openDuniaArchive(file, siblings),\n },\n ],\n games: [\n {\n id: 'farcry2',\n group: 'Игры',\n title: 'Far Cry 2 (2008)',\n engine: 'dunia1',\n formats: ['dunia-xbg', 'dunia-level'],\n hint: 'Откройте папку игры (или Data_Win32): архивы .fat/.dat подключатся сами, имена файлов берутся из списков ресурсов игры. Модели — graphics/…/*.xbg, уровни — worlds/<мир>/maps/*.fc2level (карта 1×1 км: ландшафт и объекты) и worlds/<мир>/<мир>.fc2level (весь мир — ландшафт).',\n ui,\n status: 'beta',\n markers: ['common.fat', 'worlds.fat'],\n },\n ],\n});\n"],"names":[],"mappings":";;AAKA,MAAM,KAAuB,EAAE,UAAU,MAAM,aAAa,CAAC,OAAO,KAAK,GAAG,cAAc,yBAAA;AAEnF,MAAM,eAAe,aAAa;AAAA,EACvC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,MAAM,EAAE,IAAI,GAAA;AAAA,EACZ,QAAQ,CAAC,EAAE,IAAI,OAAO,YAAY,CAAC,KAAK,GAAG,QAAQ,OAAO,SAAS,MAAM,OAAO,UAAO,GAAG,UAAU,GAAG,GAAG;AAAA,EAC1G,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,KAAK;AAAA,MAClB,kBAAkB,CAAC,KAAK;AAAA,MACxB,QAAQ;AAAA,MACR,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,MAAM,SAAS,IAAI;AAAA,MACzE,MAAM,OAAO,MAAM,SAAS,MAAM,OAAO,UAAO,GAAG,QAAQ,MAAM,GAAG;AAAA,MACpE,WAAW,OAAO,QAAQ;AACxB,cAAM,MAAM,IAAI,KAAK;AACrB,YAAI,CAAC,OAAO,CAAC,IAAI,MAAM,YAAa;AACpC,cAAM,QAAQ,MAAM,OAAO,UAAO,GAAG,eAAe,IAAI,OAAO,GAAG;AAClE,YAAI,KAAK,OAAQ,KAAI,WAAW,MAAM,YAAY;AAAA,MACpD;AAAA,IAAA;AAAA,IAEF;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,UAAU;AAAA,MACvB,QAAQ;AAAA,MACR,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,EAAE,CAAC,MAAM,kBAAkB,IAAI;AAAA,MACnF,MAAM,OAAO,MAAM,QAAQ;AACzB,cAAM,OAAO,IAAI,YAAA,EAAc,OAAO,MAAM,KAAK,aAAa;AAC9D,gBAAQ,MAAM,OAAO,YAAS,GAAG,eAAe,MAAM,MAAM,GAAG;AAAA,MACjE;AAAA,IAAA;AAAA,EACF;AAAA,EAEF,UAAU;AAAA,IACR;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,KAAK;AAAA,MAClB,qBAAqB,CAAC,KAAK;AAAA,MAC3B,gBAAgB;AAAA,MAChB,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,MAAM,SAAS,IAAI;AAAA,MACzE,MAAM,OAAO,MAAM,cAAc,MAAM,OAAO,UAAO,GAAG,iBAAiB,MAAM,QAAQ;AAAA,IAAA;AAAA,EACzF;AAAA,EAEF,OAAO;AAAA,IACL;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS,CAAC,aAAa,aAAa;AAAA,MACpC,MAAM;AAAA,MACN;AAAA,MACA,QAAQ;AAAA,MACR,SAAS,CAAC,cAAc,YAAY;AAAA,IAAA;AAAA,EACtC;AAEJ,CAAC;"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["// Dunia Engine 1 (Far Cry 2): .fat/.dat archives (names by hash, from the game's XML resource lists), .xbg models, .xbm materials,\n// .xbt textures, .mab animations (skeleton: *_ref.skeleton in the model folder or above).\nimport { defineEngine, type GamePreset } from '@gmvjs/core/plugin';\nimport { EN } from './i18n';\n\nconst ui: GamePreset['ui'] = { archives: true, archiveExts: ['fat', 'dat'], archiveLabel: 'Открыть *.fat + *.dat…' };\n\nexport const dunia1Engine = defineEngine({\n id: 'dunia1',\n title: 'Dunia Engine 1 (Far Cry 2)',\n i18n: { en: EN },\n codecs: [{ id: 'xbt', extensions: ['xbt'], decode: async (buf) => (await import('./xbt')).decodeXbt(buf) }],\n formats: [\n {\n id: 'dunia-xbg',\n title: 'Far Cry 2: модель .xbg',\n engine: 'dunia1',\n extensions: ['xbg'],\n motionExtensions: ['mab'],\n status: 'beta',\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 4)) === 'HSEM' ? 1 : 0),\n load: async (main, ctx) => (await import('./xbg')).loadXbg(main, ctx),\n afterLoad: async (ctx) => {\n const arc = ctx.main.archive;\n if (!arc || !ctx.model.loadMotions) return;\n const list = (await import('./xbg')).listMabMotions(ctx.model, arc);\n if (list.length) ctx.addMotions(list, 'animations');\n },\n },\n {\n id: 'dunia-level',\n title: 'Far Cry 2: уровень (worlds/<мир>/maps/*.fc2level — карта 1×1 км с объектами; <мир>.fc2level — весь мир)',\n engine: 'dunia1',\n extensions: ['fc2level'],\n status: 'beta',\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 12)) === 'DUNIA-LEVEL\\n' ? 1 : 0),\n load: async (main, ctx) => {\n const text = new TextDecoder().decode(await main.arrayBuffer());\n return (await import('./level')).loadDuniaLevel(text, main, ctx);\n },\n },\n ],\n archives: [\n {\n id: 'dunia-fat',\n title: 'Far Cry 2: архивы .fat + .dat',\n engine: 'dunia1',\n extensions: ['fat'],\n companionExtensions: ['dat'],\n openFromFolder: true,\n sniff: (h) => (String.fromCharCode(...h.subarray(0, 4)) === '2TAF' ? 1 : 0),\n open: async (file, siblings) => (await import('./fat')).openDuniaArchive(file, siblings),\n },\n ],\n games: [\n {\n id: 'farcry2',\n group: 'Игры',\n title: 'Far Cry 2 (2008)',\n engine: 'dunia1',\n formats: ['dunia-xbg', 'dunia-level'],\n hint: 'Откройте папку игры (или Data_Win32): архивы .fat/.dat подключатся сами, имена файлов берутся из списков ресурсов игры. Модели — graphics/…/*.xbg, уровни — worlds/<мир>/maps/*.fc2level (карта 1×1 км: ландшафт и объекты) и worlds/<мир>/<мир>.fc2level (весь мир — ландшафт).',\n ui,\n status: 'beta',\n markers: ['common.fat', 'worlds.fat'],\n },\n ],\n});\n"],"names":[],"mappings":";;AAKA,MAAM,KAAuB,EAAE,UAAU,MAAM,aAAa,CAAC,OAAO,KAAK,GAAG,cAAc,yBAAA;AAEnF,MAAM,eAAe,aAAa;AAAA,EACvC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,MAAM,EAAE,IAAI,GAAA;AAAA,EACZ,QAAQ,CAAC,EAAE,IAAI,OAAO,YAAY,CAAC,KAAK,GAAG,QAAQ,OAAO,SAAS,MAAM,OAAO,UAAO,GAAG,UAAU,GAAG,GAAG;AAAA,EAC1G,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,KAAK;AAAA,MAClB,kBAAkB,CAAC,KAAK;AAAA,MACxB,QAAQ;AAAA,MACR,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,MAAM,SAAS,IAAI;AAAA,MACzE,MAAM,OAAO,MAAM,SAAS,MAAM,OAAO,UAAO,GAAG,QAAQ,MAAM,GAAG;AAAA,MACpE,WAAW,OAAO,QAAQ;AACxB,cAAM,MAAM,IAAI,KAAK;AACrB,YAAI,CAAC,OAAO,CAAC,IAAI,MAAM,YAAa;AACpC,cAAM,QAAQ,MAAM,OAAO,UAAO,GAAG,eAAe,IAAI,OAAO,GAAG;AAClE,YAAI,KAAK,OAAQ,KAAI,WAAW,MAAM,YAAY;AAAA,MACpD;AAAA,IAAA;AAAA,IAEF;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,UAAU;AAAA,MACvB,QAAQ;AAAA,MACR,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,EAAE,CAAC,MAAM,kBAAkB,IAAI;AAAA,MACnF,MAAM,OAAO,MAAM,QAAQ;AACzB,cAAM,OAAO,IAAI,YAAA,EAAc,OAAO,MAAM,KAAK,aAAa;AAC9D,gBAAQ,MAAM,OAAO,YAAS,GAAG,eAAe,MAAM,MAAM,GAAG;AAAA,MACjE;AAAA,IAAA;AAAA,EACF;AAAA,EAEF,UAAU;AAAA,IACR;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,KAAK;AAAA,MAClB,qBAAqB,CAAC,KAAK;AAAA,MAC3B,gBAAgB;AAAA,MAChB,OAAO,CAAC,MAAO,OAAO,aAAa,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,MAAM,SAAS,IAAI;AAAA,MACzE,MAAM,OAAO,MAAM,cAAc,MAAM,OAAO,UAAO,GAAG,iBAAiB,MAAM,QAAQ;AAAA,IAAA;AAAA,EACzF;AAAA,EAEF,OAAO;AAAA,IACL;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS,CAAC,aAAa,aAAa;AAAA,MACpC,MAAM;AAAA,MACN;AAAA,MACA,QAAQ;AAAA,MACR,SAAS,CAAC,cAAc,YAAY;AAAA,IAAA;AAAA,EACtC;AAEJ,CAAC;"}
package/dist/level.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface TerrainLayer {
8
8
  normal: string;
9
9
  tiling: number;
10
10
  }
11
- /** Слои ландшафта и сетка мира из game.xml. */
11
+ /** Terrain layers and the world grid from game.xml. */
12
12
  export declare function worldDescriptor(u: Uint8Array): {
13
13
  layers: TerrainLayer[];
14
14
  countX: number;
@@ -23,19 +23,19 @@ export interface TerrainSector {
23
23
  mask: Uint8Array;
24
24
  water: number;
25
25
  }
26
- /** Ландшафт сектора (см. заголовок файла). */
26
+ /** Sector terrain (see the file header). */
27
27
  export declare function parseTerrainSector(u: Uint8Array): TerrainSector;
28
28
  /**
29
- * Слой вершины: старший установленный бит младшей тетрады маски, чей слот занят (верхний нарисованный слой);
30
- * пустой слот — 255; без бит (край сектора) — первый занятый слот.
29
+ * Vertex layer: the highest set bit of the mask's low nibble whose slot is used (the top painted layer);
30
+ * an empty slot is 255; with no bits (sector edge), the first used slot.
31
31
  */
32
32
  export declare function vertexLayer(mask: number, slots: readonly number[]): number;
33
33
  /**
34
- * Углы объекта (градусы) -> кватернион [w, x, y, z]: R = Rz(z)·Ry(y)·Rx(x) (сначала X, затем Y, затем Z — рыскание).
35
- * Проверено по объектам на склонах: ось «вверх» объекта совпадает с нормалью земли (см. тест уровней).
34
+ * Object angles (degrees) -> quaternion [w, x, y, z]: R = Rz(z)·Ry(y)·Rx(x) (X first, then Y, then Z, the yaw).
35
+ * Verified on objects on slopes: the object's "up" axis matches the ground normal (see the level test).
36
36
  */
37
37
  export declare function anglesQuat(a: readonly number[]): number[];
38
- /** Объект уровня: модель, видимые части (пусто — все), поза. */
38
+ /** Level object: model, visible parts (empty = all), pose. */
39
39
  export interface LevelEntity {
40
40
  name: string;
41
41
  model: string;
@@ -45,12 +45,12 @@ export interface LevelEntity {
45
45
  layer: string;
46
46
  archetype?: string;
47
47
  }
48
- /** Архетипы entitylibrary.fcb: hidName -> модель и части. */
48
+ /** entitylibrary.fcb archetypes: hidName -> model and parts. */
49
49
  export declare function entityLibrary(root: FcbObject): Map<string, {
50
50
  model: string;
51
51
  parts: string[];
52
52
  }>;
53
- /** Объекты сектора (WorldSector) с моделями; архетипы — по библиотеке. */
53
+ /** Sector objects (WorldSector) with models; archetypes from the library. */
54
54
  export interface LevelMarker {
55
55
  kind: 'particle' | 'light';
56
56
  name: string;
@@ -66,35 +66,35 @@ export declare function sectorEntities(root: FcbObject, lib: Map<string, {
66
66
  markers: LevelMarker[];
67
67
  };
68
68
  /**
69
- * Коллекции сектора (CCollectionComponent у CSectorEntity — камни, мусор, растительность, расставленные кистью редактора):
70
- * 0x114CFEE7 — набор (рамка 0x17E7C62E–0x2BEAF977), его данные 0xA18C8484: 0x89C8C095 — хеши путей моделей (как имена
71
- * в .fat), 0x58AD43E2 — число групп каждой модели, 0x798C33D5 — группы (u8 число экземпляров, далее номер первого),
72
- * 0xB0484974 — центры и радиусы групп; экземпляры: 0xD28100C0 — u16 x, u16 y (дециметры), 0x8E2752CA — f32 z,
73
- * 0x98C678BD — u32 (младшие 16 бит — поворот вокруг Z, полный круг = 65536; старшие — не разобраны), 0x9D1CAE67 — цвета.
74
- * Все массивы — u32 число, затем элементы.
69
+ * Sector collections (CCollectionComponent of CSectorEntity: rocks, debris, vegetation placed with the editor brush):
70
+ * 0x114CFEE7 is a set (frame 0x17E7C62E–0x2BEAF977), its data 0xA18C8484: 0x89C8C095 = model path hashes (like the names
71
+ * in .fat), 0x58AD43E2 = group count per model, 0x798C33D5 = groups (u8 instance count, then the first index),
72
+ * 0xB0484974 = group centers and radii; instances: 0xD28100C0 = u16 x, u16 y (decimetres), 0x8E2752CA = f32 z,
73
+ * 0x98C678BD = u32 (low 16 bits: rotation about Z, full circle = 65536; high bits not decoded), 0x9D1CAE67 = colors.
74
+ * Every array is a u32 count followed by the elements.
75
75
  */
76
76
  export declare function sectorCollections(root: FcbObject): Array<{
77
77
  hash: number;
78
78
  pos: number[];
79
79
  yaw: number;
80
80
  }>;
81
- /** Шаг сетки ландшафта (м): степень двойки, чтобы вершин было не больше бюджета. */
81
+ /** Terrain grid step (m): a power of two so that the vertex count stays within the budget. */
82
82
  export declare const terrainStep: (w: number, h: number, budget?: number) => number;
83
- /** Меши ландшафта области: по мешу на слой (треугольник — по слою его угла), UV — плитка слоя (Tiling повторов на сектор). */
83
+ /** Terrain meshes of an area: one mesh per layer (a triangle goes by the layer of its corner), UVs tile the layer (Tiling repeats per sector). */
84
84
  export declare function terrainMeshes(spec: {
85
85
  x0: number;
86
86
  y0: number;
87
87
  w: number;
88
88
  h: number;
89
89
  }, sectors: Map<number, TerrainSector>, widthInSectors: number, layers: TerrainLayer[], step: number): Mesh[];
90
- /** Вода: по сектору с уровнем воды — клетки сетки, где земля ниже уровня, плоскостью на уровне; полупрозрачная. */
90
+ /** Water: for each sector with a water level, grid cells where the ground is below it, as a plane at that level; translucent. */
91
91
  export declare function waterMesh(spec: {
92
92
  x0: number;
93
93
  y0: number;
94
94
  w: number;
95
95
  h: number;
96
96
  }, sectors: Map<number, TerrainSector>, widthInSectors: number, step: number): Mesh | null;
97
- /** Указатель мира: файлы мира, ландшафт по номеру сектора, файлы объектов (WorldSector) по X, Y. Строится один раз на архив. */
97
+ /** World index: world files, terrain by sector number, object files (WorldSector) by X, Y. Built once per archive. */
98
98
  interface WorldIndex {
99
99
  files: FatRef[];
100
100
  terrain: Map<number, FatRef>;
@@ -103,18 +103,18 @@ interface WorldIndex {
103
103
  full?: boolean;
104
104
  }
105
105
  /**
106
- * Указатель мира. Обычно файлы мира — участок .dat между .sectorsdep.fcb предыдущего мира и своим (worlds.dat). Если там
107
- * нет ландшафта или номера секторов повторяются (dlc1.dat: сначала сектора всех миров, потом их именованные файлы), k-й блок ландшафта .dat — k-го мира
108
- * по порядку .sectorsdep.fcb, а файлы мира — от начала его блока до начала следующего.
106
+ * World index. Normally a world's files are the .dat range between the previous world's .sectorsdep.fcb and its own (worlds.dat). If
107
+ * that range has no terrain or sector numbers repeat (dlc1.dat: first the sectors of all worlds, then their named files), the k-th .dat terrain block belongs to the k-th world
108
+ * in .sectorsdep.fcb order, and the world's files run from the start of its block to the start of the next.
109
109
  */
110
110
  export declare function worldIndex(api: DuniaArchiveApi, world: DuniaWorld): Promise<WorldIndex>;
111
111
  /**
112
- * Файлы объектов мира: .fcb с корнем WorldSector. У больших миров — после блока ландшафта (объекты слоёв миссий; рядом
113
- * с ландшафтом — только сектора-коллекции растительности), у малых — вперемешку с ландшафтом. X, Y — первые значения
114
- * корня, читаются из начала файла.
112
+ * World object files: .fcb with a WorldSector root. In large worlds they follow the terrain block (mission layer objects; next to
113
+ * the terrain are only vegetation collection sectors), in small worlds they are mixed with the terrain. X, Y are the root's first
114
+ * values, read from the start of the file.
115
115
  */
116
116
  export declare function worldObjects(api: DuniaArchiveApi, world: DuniaWorld, idx: WorldIndex): Promise<Map<number, FatRef[]>>;
117
- /** X, Y из начала .fcb с корнем WorldSector (иначе null). */
117
+ /** X, Y from the start of an .fcb with a WorldSector root (otherwise null). */
118
118
  export declare function worldSectorXY(h: Uint8Array): [number, number] | null;
119
119
  export declare function loadDuniaLevel(text: string, main: VFile, ctx: LoadContext): Promise<Model>;
120
120
  export {};