@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 +28 -22
- package/dist/fat.d.ts +12 -12
- package/dist/fat.js +1 -1
- package/dist/fat.js.map +1 -1
- package/dist/fcb.d.ts +2 -2
- package/dist/fcb.js.map +1 -1
- package/dist/games/farcry2.js.map +1 -1
- package/dist/i18n.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/level.d.ts +26 -26
- package/dist/level.js.map +1 -1
- package/dist/mab.d.ts +8 -8
- package/dist/mab.js.map +1 -1
- package/dist/realtree.d.ts +1 -1
- package/dist/realtree.js.map +1 -1
- package/dist/rml.d.ts +1 -1
- package/dist/rml.js.map +1 -1
- package/dist/xbg.d.ts +18 -18
- package/dist/xbg.js.map +1 -1
- package/dist/xbt.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# @gmvjs/dunia1 — Dunia Engine 1 (Far Cry 2)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.patreon.com/cw/shibisty)
|
|
4
4
|
|
|
5
|
-
|
|
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:
|
|
44
|
-
| `dunia-level` | Far Cry 2:
|
|
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:
|
|
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
|
|
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
|
-
-
|
|
65
|
+
- Exact RealTree trees, sky, terrain layer blending
|
|
64
66
|
|
|
65
|
-
##
|
|
67
|
+
## License
|
|
66
68
|
|
|
67
69
|
MIT
|
|
70
|
+
|
|
71
|
+
[](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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
14
|
+
/** Paths from XML dependency lists (ID="…" and crc_ID). */
|
|
15
15
|
export declare function namesFromResourceXml(text: string): string[];
|
|
16
|
-
/**
|
|
16
|
+
/** A .fat record together with its .dat. */
|
|
17
17
|
export interface FatRef {
|
|
18
18
|
fe: FatEntry;
|
|
19
19
|
dat: BlobFile;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
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
|
|
25
|
+
/** The world's .dat and the offset of its .sectorsdep.fcb. */
|
|
26
26
|
dat: BlobFile;
|
|
27
27
|
end: number;
|
|
28
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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":["//
|
|
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) —
|
|
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":["//
|
|
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):
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
70
|
-
* 0x114CFEE7
|
|
71
|
-
*
|
|
72
|
-
* 0xB0484974
|
|
73
|
-
* 0x98C678BD
|
|
74
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
-
*
|
|
113
|
-
*
|
|
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
|
|
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 {};
|