@gmvjs/dunia1 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/fat.d.ts +57 -0
- package/dist/fat.js +226 -0
- package/dist/fat.js.map +1 -0
- package/dist/fcb.d.ts +14 -0
- package/dist/fcb.js +86 -0
- package/dist/fcb.js.map +1 -0
- package/dist/games/farcry2.d.ts +7 -0
- package/dist/games/farcry2.js +11 -0
- package/dist/games/farcry2.js.map +1 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +12 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/level.d.ts +120 -0
- package/dist/level.js +751 -0
- package/dist/level.js.map +1 -0
- package/dist/mab.d.ts +40 -0
- package/dist/mab.js +228 -0
- package/dist/mab.js.map +1 -0
- package/dist/realtree.d.ts +27 -0
- package/dist/realtree.js +127 -0
- package/dist/realtree.js.map +1 -0
- package/dist/rml.d.ts +15 -0
- package/dist/rml.js +77 -0
- package/dist/rml.js.map +1 -0
- package/dist/xbg.d.ts +115 -0
- package/dist/xbg.js +524 -0
- package/dist/xbg.js.map +1 -0
- package/dist/xbt.d.ts +3 -0
- package/dist/xbt.js +14 -0
- package/dist/xbt.js.map +1 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Shibisty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gmvjs/dunia1 — Dunia Engine 1 (Far Cry 2)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dunia Engine 1 (Far Cry 2; Dunia 2 — Far Cry 3 и новее — пока не поддержан): архивы `.fat/.dat` (FAT2 v5, LZO), модели `.xbg`, текстуры `.xbt`, анимации `.mab` с корневым движением, наборы персонажей, уровни `.fc2level`.
|
|
4
|
+
|
|
5
|
+
Плагин движка для [`@gmvjs/core`](../core/README.md): форматы, архивы и пресеты игр. Тяжёлые разборщики загружаются по требованию (`import()` внутри `load`), так что подключение пакета почти ничего не стоит, пока файл этого движка не открыт.
|
|
6
|
+
|
|
7
|
+
## Установка
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @gmvjs/core @gmvjs/dunia1 @gmvjs/codecs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Подключение
|
|
14
|
+
|
|
15
|
+
Весь движок (все игры):
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createRegistry, ViewerSession } from '@gmvjs/core';
|
|
19
|
+
import { dunia1Engine } from '@gmvjs/dunia1';
|
|
20
|
+
|
|
21
|
+
const session = new ViewerSession(createRegistry([dunia1Engine]));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Одна игра — подпуть пакета (пресет игры и движок, ограниченный этой игрой):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createRegistry, ViewerSession } from '@gmvjs/core';
|
|
28
|
+
import farcry2 from '@gmvjs/dunia1/farcry2';
|
|
29
|
+
|
|
30
|
+
const session = new ViewerSession(createRegistry([farcry2.engine]), farcry2.preset.id);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Игры (подпути)
|
|
34
|
+
|
|
35
|
+
| Импорт | Пресет | Игра | Статус | Только для этой игры |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| `@gmvjs/dunia1/farcry2` | `farcry2` | Far Cry 2 (2008) | 🟡 бета | — |
|
|
38
|
+
|
|
39
|
+
## Форматы
|
|
40
|
+
|
|
41
|
+
| id | Что | Файлы | Статус |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `dunia-xbg` | Far Cry 2: модель .xbg | `.xbg` + анимации `.mab` | 🟡 бета |
|
|
44
|
+
| `dunia-level` | Far Cry 2: уровень (worlds/<мир>/maps/*.fc2level — карта 1×1 км с объектами; <мир>.fc2level — весь мир) | `.fc2level` | 🟡 бета |
|
|
45
|
+
|
|
46
|
+
## Архивы
|
|
47
|
+
|
|
48
|
+
- `dunia-fat` — Far Cry 2: архивы .fat + .dat (`.fat`)
|
|
49
|
+
|
|
50
|
+
## Текстуры
|
|
51
|
+
|
|
52
|
+
- `xbt` — `.xbt`
|
|
53
|
+
|
|
54
|
+
## Зависимости
|
|
55
|
+
|
|
56
|
+
- `@gmvjs/core` — API плагинов, типы, архивы ZIP/папка, zlib, DDS/BC, геометрия
|
|
57
|
+
- `@gmvjs/codecs` — LZO
|
|
58
|
+
|
|
59
|
+
## Не сделано
|
|
60
|
+
|
|
61
|
+
- Лицевая анимация
|
|
62
|
+
- Havok
|
|
63
|
+
- Точные деревья RealTree, небо, смешивание слоёв рельефа
|
|
64
|
+
|
|
65
|
+
## Лицензия
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/fat.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Archive, BlobFile } from '@gmvjs/core/types';
|
|
2
|
+
export interface FatEntry {
|
|
3
|
+
hash: number;
|
|
4
|
+
size: number;
|
|
5
|
+
scheme: number;
|
|
6
|
+
csize: number;
|
|
7
|
+
off: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseFat(u: Uint8Array): FatEntry[];
|
|
10
|
+
/** Распаковать запись; head — только первые head байт (для распознавания), без чтения всей записи. */
|
|
11
|
+
export declare function readFatEntry(dat: BlobFile, e: FatEntry, head?: number): Promise<Uint8Array>;
|
|
12
|
+
/** Хеш имени файла Dunia (FC2): CRC-32 пути в нижнем регистре, разделитель «\». */
|
|
13
|
+
export declare const duniaHash: (path: string) => number;
|
|
14
|
+
/** Пути из XML-списков зависимостей (ID="…" и crc_ID). */
|
|
15
|
+
export declare function namesFromResourceXml(text: string): string[];
|
|
16
|
+
/** Запись .fat вместе с её .dat. */
|
|
17
|
+
export interface FatRef {
|
|
18
|
+
fe: FatEntry;
|
|
19
|
+
dat: BlobFile;
|
|
20
|
+
}
|
|
21
|
+
/** Мир (уровень): worlds/<имя>/generated/<имя>.game.xml и .sectorsdep.fcb. */
|
|
22
|
+
export interface DuniaWorld {
|
|
23
|
+
name: string;
|
|
24
|
+
gameXml: FatRef;
|
|
25
|
+
/** .dat мира и смещение его .sectorsdep.fcb. */
|
|
26
|
+
dat: BlobFile;
|
|
27
|
+
end: number;
|
|
28
|
+
/** Файлы мира без имён (сектора, ландшафт): записи того же .dat между концом предыдущего мира и этим .sectorsdep.fcb. */
|
|
29
|
+
files(): FatRef[];
|
|
30
|
+
}
|
|
31
|
+
export interface DuniaArchiveApi {
|
|
32
|
+
duniaWorld(name: string): DuniaWorld | null;
|
|
33
|
+
duniaWorlds(): DuniaWorld[];
|
|
34
|
+
/** Все записи .dat по порядку смещений. */
|
|
35
|
+
datRefs(dat: BlobFile): FatRef[];
|
|
36
|
+
readRef(r: FatRef, head?: number): Promise<Uint8Array>;
|
|
37
|
+
/** Запись по хешу имени (ссылки коллекций уровней — хеши путей моделей) и её имя, если известно. */
|
|
38
|
+
refByHash(h: number): FatRef | undefined;
|
|
39
|
+
nameOfHash(h: number): string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Метка виртуального файла уровня (worlds/<мир>/….fc2level): дальше JSON с миром и областью секторов. */
|
|
42
|
+
export declare const LEVEL_MARK = "DUNIA-LEVEL\n";
|
|
43
|
+
export interface LevelSpec {
|
|
44
|
+
world: string;
|
|
45
|
+
name: string;
|
|
46
|
+
x0: number;
|
|
47
|
+
y0: number;
|
|
48
|
+
w: number;
|
|
49
|
+
h: number;
|
|
50
|
+
terrainOnly?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Все .fat из выбранных (или из папки игры) одним архивом; открывается по первому. Имена — из XML-списков ресурсов.
|
|
54
|
+
*/
|
|
55
|
+
export declare function openDuniaArchive(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;
|
|
56
|
+
/** Миры архива Dunia (для уровней), если архив — Dunia. */
|
|
57
|
+
export declare const duniaApi: (a: unknown) => DuniaArchiveApi | null;
|
package/dist/fat.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { lzo1xDecompress } from "@gmvjs/codecs";
|
|
2
|
+
import { crc32 } from "@gmvjs/core/writers/zip";
|
|
3
|
+
import { createArchive, readRange } from "@gmvjs/core/archives/common";
|
|
4
|
+
function parseFat(u) {
|
|
5
|
+
const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);
|
|
6
|
+
if (u.length < 16 || dv.getUint32(0, true) !== 1178686514) throw new Error("Не архив Dunia (нет «FAT2»)");
|
|
7
|
+
const ver = dv.getUint32(4, true);
|
|
8
|
+
if (ver !== 5) throw new Error(`Архив Dunia версии ${ver} не поддерживается (нужна 5 — Far Cry 2)`);
|
|
9
|
+
const n = dv.getUint32(12, true);
|
|
10
|
+
if (16 + n * 16 > u.length) throw new Error(".fat: оглавление обрезано");
|
|
11
|
+
const out = [];
|
|
12
|
+
for (let i = 0; i < n; i++) {
|
|
13
|
+
const o = 16 + i * 16, a = dv.getUint32(o + 4, true), b = dv.getUint32(o + 8, true), c = dv.getUint32(o + 12, true);
|
|
14
|
+
const scheme = a & 3, csize = b & 1073741823;
|
|
15
|
+
out.push({ hash: dv.getUint32(o, true), size: scheme === 0 ? csize : a >>> 2, scheme, csize, off: c * 4 + (b >>> 30) });
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
async function readFatEntry(dat, e, head = 0) {
|
|
20
|
+
if (e.scheme === 0) return readRange(dat, e.off, e.off + (head ? Math.min(head, e.csize) : e.csize));
|
|
21
|
+
if (e.scheme !== 1) throw new Error(`Dunia: схема сжатия ${e.scheme} не поддерживается`);
|
|
22
|
+
if (head) {
|
|
23
|
+
for (const take of head <= 64 ? [512, 2048] : [2048]) {
|
|
24
|
+
const out2 = new Uint8Array(Math.min(Math.max(head, 1024), e.size));
|
|
25
|
+
try {
|
|
26
|
+
lzo1xDecompress(await readRange(dat, e.off, e.off + Math.min(e.csize, take)), 0, out2);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
const r = out2.subarray(0, Math.min(head, out2.length));
|
|
30
|
+
if (take >= e.csize || r.some((b) => b !== 0)) return r;
|
|
31
|
+
}
|
|
32
|
+
return new Uint8Array(0);
|
|
33
|
+
}
|
|
34
|
+
const out = new Uint8Array(e.size);
|
|
35
|
+
lzo1xDecompress(await readRange(dat, e.off, e.off + e.csize), 0, out);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const duniaHash = (path) => crc32(new TextEncoder().encode(path.replace(/\//g, "\\").toLowerCase())) >>> 0;
|
|
39
|
+
function namesFromResourceXml(text) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const m of text.matchAll(/ID="([^"]+\.[A-Za-z0-9]{2,5})"/g)) out.push(m[1]);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
const LEVEL_MARK = "DUNIA-LEVEL\n";
|
|
45
|
+
const KNOWN_WORLDS = [
|
|
46
|
+
"world1",
|
|
47
|
+
"world2",
|
|
48
|
+
"tmpla",
|
|
49
|
+
"mp_02_s_shanty",
|
|
50
|
+
"mp_06_m_ranch",
|
|
51
|
+
"mp_07_m_dogon",
|
|
52
|
+
"mp_08_m_colony",
|
|
53
|
+
"mp_10_l_fishingvillage",
|
|
54
|
+
"mp_11_l_savanna",
|
|
55
|
+
"mp_12_l_fueldepot",
|
|
56
|
+
"mp_14_woodlands",
|
|
57
|
+
"mp_15_mine",
|
|
58
|
+
"mp_16_airbase",
|
|
59
|
+
"mp_17_dunes",
|
|
60
|
+
"mp_18_greenhouse",
|
|
61
|
+
"mp_21_town",
|
|
62
|
+
"mp_24_dusttown",
|
|
63
|
+
"mp_25_prison",
|
|
64
|
+
"mp_dlc01_resort",
|
|
65
|
+
"mp_dlc02_lake",
|
|
66
|
+
"mp_dlc04_bridge",
|
|
67
|
+
"mp_dlc05_clinic",
|
|
68
|
+
"mp_dlc09_jungle",
|
|
69
|
+
"mp_dlc_gym"
|
|
70
|
+
];
|
|
71
|
+
const FAT_RE = /\.fat$/i;
|
|
72
|
+
const SKIP_RE = /^(sound|sound_english|shadersobj)\.fat$/i;
|
|
73
|
+
const rank = (name) => /^patch\.fat$/i.test(name) ? 3 : /^common\.fat$/i.test(name) ? 0 : /^worlds/i.test(name) ? 1 : 2;
|
|
74
|
+
const relPath = (f) => (f.webkitRelativePath || f.name).replace(/\\/g, "/");
|
|
75
|
+
const isListCandidate = (e) => e.scheme === 1 && e.size >= 256 * 1024 && e.size / Math.max(1, e.csize) >= 3;
|
|
76
|
+
async function openDuniaArchive(file, siblings) {
|
|
77
|
+
const fats = siblings.filter((f) => FAT_RE.test(f.name) && !SKIP_RE.test(f.name));
|
|
78
|
+
if (!fats.includes(file)) fats.push(file);
|
|
79
|
+
fats.sort((a, b) => rank(a.name) - rank(b.name) || relPath(a).localeCompare(relPath(b)));
|
|
80
|
+
if (fats[0] !== file) return null;
|
|
81
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
82
|
+
const missingDat = [];
|
|
83
|
+
for (const f of fats) {
|
|
84
|
+
const want = relPath(f).replace(FAT_RE, ".dat").toLowerCase();
|
|
85
|
+
const dat = siblings.find((s) => relPath(s).toLowerCase() === want) ?? siblings.find((s) => s.name.toLowerCase() === f.name.replace(FAT_RE, ".dat").toLowerCase());
|
|
86
|
+
if (!dat) {
|
|
87
|
+
missingDat.push(f.name);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const fe of parseFat(new Uint8Array(await f.arrayBuffer()))) byHash.set(fe.hash, { fe, dat });
|
|
91
|
+
}
|
|
92
|
+
if (!byHash.size) throw new Error(`Нет .dat рядом с ${missingDat.join(", ") || file.name}: выберите пары .fat + .dat или папку Data_Win32`);
|
|
93
|
+
const names = /* @__PURE__ */ new Map(), seenSize = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const { fe, dat } of byHash.values()) {
|
|
95
|
+
if (!isListCandidate(fe)) continue;
|
|
96
|
+
const k = `${fe.size}/${fe.csize}`;
|
|
97
|
+
if (seenSize.has(k)) continue;
|
|
98
|
+
let head;
|
|
99
|
+
try {
|
|
100
|
+
head = await readFatEntry(dat, fe, 16);
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (String.fromCharCode(...head.subarray(0, 6)) !== "<root>") continue;
|
|
105
|
+
seenSize.add(k);
|
|
106
|
+
try {
|
|
107
|
+
const text = new TextDecoder("latin1").decode(await readFatEntry(dat, fe));
|
|
108
|
+
for (const n of namesFromResourceXml(text)) {
|
|
109
|
+
const h = duniaHash(n);
|
|
110
|
+
if (byHash.has(h) && !names.has(h)) names.set(h, n);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const worldNames = new Set(KNOWN_WORLDS);
|
|
116
|
+
for (const n of names.values()) {
|
|
117
|
+
const m = /^worlds\\([^\\]+)\\/i.exec(n);
|
|
118
|
+
if (m) worldNames.add(m[1].toLowerCase());
|
|
119
|
+
}
|
|
120
|
+
const worlds = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const w of worldNames) {
|
|
122
|
+
const gx = byHash.get(duniaHash(`worlds\\${w}\\generated\\${w}.game.xml`)), sd = byHash.get(duniaHash(`worlds\\${w}\\generated\\${w}.sectorsdep.fcb`));
|
|
123
|
+
if (gx && sd) worlds.set(w, { name: w, gx, end: sd.fe.off, dat: sd.dat, start: 0 });
|
|
124
|
+
}
|
|
125
|
+
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;
|
|
126
|
+
const levelEntries = [];
|
|
127
|
+
await Promise.all([...worlds.values()].map(async (w) => {
|
|
128
|
+
const spec = (name, x0, y0, cw, ch, terrainOnly = false) => ({ world: w.name, name, x0, y0, w: cw, h: ch, ...terrainOnly ? { terrainOnly } : {} });
|
|
129
|
+
const add = (path, sp) => {
|
|
130
|
+
const text = LEVEL_MARK + JSON.stringify(sp);
|
|
131
|
+
levelEntries.push({ path, size: text.length, fe: w.gx.fe, dat: w.gx.dat, text });
|
|
132
|
+
};
|
|
133
|
+
let maps = [], gx = 0, gy = 0;
|
|
134
|
+
try {
|
|
135
|
+
const { xmlElements } = await import("./rml.js");
|
|
136
|
+
for (const el of xmlElements(await readFatEntry(w.gx.dat, w.gx.fe))) {
|
|
137
|
+
if (el.name === "GridMapSectors" && !gx) {
|
|
138
|
+
gx = +el.attrs.CountX || 0;
|
|
139
|
+
gy = +el.attrs.CountY || 0;
|
|
140
|
+
}
|
|
141
|
+
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 });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
maps = [];
|
|
145
|
+
}
|
|
146
|
+
maps = maps.filter((m) => m.cx > 0 && m.cy > 0);
|
|
147
|
+
if (maps.length > 1) {
|
|
148
|
+
const W = Math.max(...maps.map((m) => m.x + m.cx)), H = Math.max(...maps.map((m) => m.y + m.cy));
|
|
149
|
+
add(`worlds/${w.name}/${w.name}.fc2level`, spec(w.name, 0, 0, W, H, true));
|
|
150
|
+
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));
|
|
151
|
+
} else {
|
|
152
|
+
const m = maps[0];
|
|
153
|
+
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));
|
|
154
|
+
}
|
|
155
|
+
}));
|
|
156
|
+
const levelIndex = new Map(levelEntries.map((e) => [e.path, e]));
|
|
157
|
+
const entries = [...levelEntries], byPath = /* @__PURE__ */ new Map();
|
|
158
|
+
const entryOf = (h, path) => {
|
|
159
|
+
let e = byPath.get(h);
|
|
160
|
+
if (e) return e;
|
|
161
|
+
let x = byHash.get(h);
|
|
162
|
+
if (!x) return null;
|
|
163
|
+
if (/\.xbt$/i.test(path)) {
|
|
164
|
+
const hd = byHash.get(duniaHash(path.replace(/\.xbt$/i, "_mip0.xbt")));
|
|
165
|
+
if (hd && hd.fe.size > 0) x = hd;
|
|
166
|
+
}
|
|
167
|
+
e = { path, size: x.fe.size, fe: x.fe, dat: x.dat };
|
|
168
|
+
byPath.set(h, e);
|
|
169
|
+
return e;
|
|
170
|
+
};
|
|
171
|
+
for (const [h, n] of names) entries.push(entryOf(h, n.replace(/\\/g, "/").toLowerCase()));
|
|
172
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
173
|
+
const refs = [...byHash.values()];
|
|
174
|
+
const byDat = /* @__PURE__ */ new Map();
|
|
175
|
+
const datRefs = (dat) => {
|
|
176
|
+
let l = byDat.get(dat);
|
|
177
|
+
if (!l) {
|
|
178
|
+
l = refs.filter((r) => r.dat === dat).sort((a, b) => a.fe.off - b.fe.off);
|
|
179
|
+
byDat.set(dat, l);
|
|
180
|
+
}
|
|
181
|
+
return l;
|
|
182
|
+
};
|
|
183
|
+
const worldOf = (w) => ({
|
|
184
|
+
name: w.name,
|
|
185
|
+
gameXml: w.gx,
|
|
186
|
+
dat: w.dat,
|
|
187
|
+
end: w.end,
|
|
188
|
+
files: () => datRefs(w.dat).filter((r) => r.fe.off >= w.start && r.fe.off < w.end)
|
|
189
|
+
});
|
|
190
|
+
const api = {
|
|
191
|
+
duniaWorld(name) {
|
|
192
|
+
const w = worlds.get(name.toLowerCase());
|
|
193
|
+
return w ? worldOf(w) : null;
|
|
194
|
+
},
|
|
195
|
+
duniaWorlds: () => [...worlds.values()].map(worldOf),
|
|
196
|
+
datRefs,
|
|
197
|
+
readRef: (r, head) => readFatEntry(r.dat, r.fe, head),
|
|
198
|
+
refByHash: (h) => byHash.get(h >>> 0),
|
|
199
|
+
nameOfHash: (h) => names.get(h >>> 0)
|
|
200
|
+
};
|
|
201
|
+
const arch = createArchive({
|
|
202
|
+
name: fats.length > 1 ? `${fats[0].name} … (${fats.length} .fat)` : file.name,
|
|
203
|
+
kind: "dunia-fat",
|
|
204
|
+
version: 5,
|
|
205
|
+
entries,
|
|
206
|
+
// путь из ссылок модели/материала — по хешу, даже если его нет в списках
|
|
207
|
+
lookup: (p) => {
|
|
208
|
+
const k = p.replace(/\\/g, "/").toLowerCase();
|
|
209
|
+
return levelIndex.get(k) ?? entryOf(duniaHash(p), k);
|
|
210
|
+
},
|
|
211
|
+
read: (e) => e.text !== void 0 ? Promise.resolve(new TextEncoder().encode(e.text)) : readFatEntry(e.dat, e.fe),
|
|
212
|
+
findTexture: (p) => /\.xbt$/i.test(p) ? entryOf(duniaHash(p), p.replace(/\\/g, "/").toLowerCase()) : null
|
|
213
|
+
});
|
|
214
|
+
return Object.assign(arch, api);
|
|
215
|
+
}
|
|
216
|
+
const duniaApi = (a) => a && typeof a.duniaWorld === "function" ? a : null;
|
|
217
|
+
export {
|
|
218
|
+
LEVEL_MARK,
|
|
219
|
+
duniaApi,
|
|
220
|
+
duniaHash,
|
|
221
|
+
namesFromResourceXml,
|
|
222
|
+
openDuniaArchive,
|
|
223
|
+
parseFat,
|
|
224
|
+
readFatEntry
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=fat.js.map
|
package/dist/fat.js.map
ADDED
|
@@ -0,0 +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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"}
|
package/dist/fcb.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FcbObject {
|
|
2
|
+
type: number;
|
|
3
|
+
values: Map<number, Uint8Array>;
|
|
4
|
+
children: FcbObject[];
|
|
5
|
+
}
|
|
6
|
+
/** CRC-32 имени класса/поля, как в .fcb. */
|
|
7
|
+
export declare const fcbHash: (name: string) => number;
|
|
8
|
+
export declare function parseFcb(u: Uint8Array): FcbObject;
|
|
9
|
+
export declare const fcbString: (v: Uint8Array | undefined) => string | undefined;
|
|
10
|
+
export declare const fcbU32: (v: Uint8Array | undefined) => number | undefined;
|
|
11
|
+
export declare const fcbF32: (v: Uint8Array | undefined) => number | undefined;
|
|
12
|
+
export declare const fcbVec3: (v: Uint8Array | undefined) => number[] | undefined;
|
|
13
|
+
/** Обход дерева: все объекты с типом type. */
|
|
14
|
+
export declare function fcbFind(root: FcbObject, type: number, out?: FcbObject[], seen?: Set<FcbObject>): FcbObject[];
|
package/dist/fcb.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { crc32 } from "@gmvjs/core/writers/zip";
|
|
2
|
+
const fcbHash = (name) => crc32(new TextEncoder().encode(name)) >>> 0;
|
|
3
|
+
function parseFcb(u) {
|
|
4
|
+
const dv = new DataView(u.buffer, u.byteOffset, u.byteLength);
|
|
5
|
+
if (u.length < 16 || dv.getUint32(0, true) !== 1178821230) throw new Error("Не .fcb (нет «nbCF»)");
|
|
6
|
+
if (dv.getUint16(4, true) !== 2) throw new Error(`.fcb версии ${dv.getUint16(4, true)} не поддерживается (нужна 2 — Far Cry 2)`);
|
|
7
|
+
let p = 16;
|
|
8
|
+
const need = (n) => {
|
|
9
|
+
if (p + n > u.length) throw new Error(".fcb обрезан");
|
|
10
|
+
};
|
|
11
|
+
const count = () => {
|
|
12
|
+
need(1);
|
|
13
|
+
const b = u[p++];
|
|
14
|
+
if (b < 254) return [b, false];
|
|
15
|
+
need(4);
|
|
16
|
+
const v = dv.getUint32(p, true);
|
|
17
|
+
p += 4;
|
|
18
|
+
return [v, b === 254];
|
|
19
|
+
};
|
|
20
|
+
const pointers = [];
|
|
21
|
+
const obj = (depth) => {
|
|
22
|
+
if (depth > 64) throw new Error(".fcb: слишком глубокая вложенность");
|
|
23
|
+
const [nChildren, isRef] = count();
|
|
24
|
+
if (isRef) {
|
|
25
|
+
const o2 = pointers[nChildren];
|
|
26
|
+
if (!o2) throw new Error(".fcb: ссылка на неизвестный объект");
|
|
27
|
+
return o2;
|
|
28
|
+
}
|
|
29
|
+
const o = { type: 0, values: /* @__PURE__ */ new Map(), children: [] };
|
|
30
|
+
pointers.push(o);
|
|
31
|
+
need(4);
|
|
32
|
+
o.type = dv.getUint32(p, true);
|
|
33
|
+
p += 4;
|
|
34
|
+
const [nValues] = count();
|
|
35
|
+
for (let i = 0; i < nValues; i++) {
|
|
36
|
+
need(4);
|
|
37
|
+
const name = dv.getUint32(p, true);
|
|
38
|
+
p += 4;
|
|
39
|
+
const at = p;
|
|
40
|
+
const [size, back] = count();
|
|
41
|
+
if (back) {
|
|
42
|
+
const save = p;
|
|
43
|
+
p = at - size;
|
|
44
|
+
const [s2, again] = count();
|
|
45
|
+
if (again) throw new Error(".fcb: двойная ссылка значения");
|
|
46
|
+
need(s2);
|
|
47
|
+
o.values.set(name, u.subarray(p, p + s2));
|
|
48
|
+
p = save;
|
|
49
|
+
} else {
|
|
50
|
+
need(size);
|
|
51
|
+
o.values.set(name, u.subarray(p, p + size));
|
|
52
|
+
p += size;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < nChildren; i++) o.children.push(obj(depth + 1));
|
|
56
|
+
return o;
|
|
57
|
+
};
|
|
58
|
+
return obj(0);
|
|
59
|
+
}
|
|
60
|
+
const dvOf = (v) => new DataView(v.buffer, v.byteOffset, v.byteLength);
|
|
61
|
+
const fcbString = (v) => {
|
|
62
|
+
if (!v) return void 0;
|
|
63
|
+
let e = v.indexOf(0);
|
|
64
|
+
if (e < 0) e = v.length;
|
|
65
|
+
return new TextDecoder("latin1").decode(v.subarray(0, e));
|
|
66
|
+
};
|
|
67
|
+
const fcbVec3 = (v) => {
|
|
68
|
+
if (!v || v.length < 12) return void 0;
|
|
69
|
+
const d = dvOf(v);
|
|
70
|
+
return [d.getFloat32(0, true), d.getFloat32(4, true), d.getFloat32(8, true)];
|
|
71
|
+
};
|
|
72
|
+
function fcbFind(root, type, out = [], seen = /* @__PURE__ */ new Set()) {
|
|
73
|
+
if (seen.has(root)) return out;
|
|
74
|
+
seen.add(root);
|
|
75
|
+
if (root.type === type) out.push(root);
|
|
76
|
+
for (const c of root.children) fcbFind(c, type, out, seen);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
fcbFind,
|
|
81
|
+
fcbHash,
|
|
82
|
+
fcbString,
|
|
83
|
+
fcbVec3,
|
|
84
|
+
parseFcb
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=fcb.js.map
|
package/dist/fcb.js.map
ADDED
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const engine: import("@gmvjs/core/plugin").EnginePlugin;
|
|
2
|
+
export declare const preset: import("@gmvjs/core/plugin").GamePreset;
|
|
3
|
+
declare const _default: {
|
|
4
|
+
engine: import("@gmvjs/core/plugin").EnginePlugin;
|
|
5
|
+
preset: import("@gmvjs/core/plugin").GamePreset;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pickGames } from "@gmvjs/core/plugin";
|
|
2
|
+
import { dunia1Engine } from "../index.js";
|
|
3
|
+
const engine = pickGames(dunia1Engine, "farcry2");
|
|
4
|
+
const preset = engine.games[0];
|
|
5
|
+
const farcry2 = { engine, preset };
|
|
6
|
+
export {
|
|
7
|
+
farcry2 as default,
|
|
8
|
+
engine,
|
|
9
|
+
preset
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=farcry2.js.map
|
|
@@ -0,0 +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;"}
|
package/dist/i18n.d.ts
ADDED
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const EN = {
|
|
2
|
+
"presets": {
|
|
3
|
+
"farcry2": {
|
|
4
|
+
"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).",
|
|
5
|
+
"archiveLabel": "Open *.fat + *.dat…"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
EN
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
ADDED
|
@@ -0,0 +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;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const dunia1Engine: import("@gmvjs/core/plugin").EnginePlugin;
|