@gmvjs/dunia1 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE 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,73 @@
1
- # Temporary Holding Version
1
+ # @gmvjs/dunia1 — Dunia Engine 1 (Far Cry 2)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
4
+
5
+ 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
+
7
+ An engine plugin for [`@gmvjs/core`](../core/README.md): formats, archives and game presets. Heavy parsers are loaded on demand (`import()` inside `load`), so adding the package costs almost nothing until a file of this engine is opened.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm i @gmvjs/core @gmvjs/dunia1 @gmvjs/codecs
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ The whole engine (all games):
18
+
19
+ ```ts
20
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
21
+ import { dunia1Engine } from '@gmvjs/dunia1';
22
+
23
+ const session = new ViewerSession(createRegistry([dunia1Engine]));
24
+ ```
25
+
26
+ One game: a package subpath (the game preset and an engine limited to that game):
27
+
28
+ ```ts
29
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
30
+ import farcry2 from '@gmvjs/dunia1/farcry2';
31
+
32
+ const session = new ViewerSession(createRegistry([farcry2.engine]), farcry2.preset.id);
33
+ ```
34
+
35
+ ## Games (subpaths)
36
+
37
+ | Import | Preset | Game | Status | Only for this game |
38
+ |---|---|---|---|---|
39
+ | `@gmvjs/dunia1/farcry2` | `farcry2` | Far Cry 2 (2008) | 🟡 beta | — |
40
+
41
+ ## Formats
42
+
43
+ | id | What | Files | Status |
44
+ |---|---|---|---|
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 |
47
+
48
+ ## Archives
49
+
50
+ - `dunia-fat` — Far Cry 2: .fat + .dat archives (`.fat`)
51
+
52
+ ## Textures
53
+
54
+ - `xbt` — `.xbt`
55
+
56
+ ## Dependencies
57
+
58
+ - `@gmvjs/core` — plugin API, types, ZIP/folder archives, zlib, DDS/BC, geometry
59
+ - `@gmvjs/codecs` — LZO
60
+
61
+ ## Not done yet
62
+
63
+ - Facial animation
64
+ - Havok
65
+ - Exact RealTree trees, sky, terrain layer blending
66
+
67
+ ## License
68
+
69
+ MIT
70
+
71
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
72
+
73
+ If this project helps you, consider supporting its development on Patreon ❤️
package/dist/fat.d.ts 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
+ /** Unpack a record; head: only the first head bytes (for recognition), without reading the whole record. */
11
+ export declare function readFatEntry(dat: BlobFile, e: FatEntry, head?: number): Promise<Uint8Array>;
12
+ /** Dunia (FC2) file name hash: CRC-32 of the lower-case path with "\" separators. */
13
+ export declare const duniaHash: (path: string) => number;
14
+ /** Paths from XML dependency lists (ID="…" and crc_ID). */
15
+ export declare function namesFromResourceXml(text: string): string[];
16
+ /** A .fat record together with its .dat. */
17
+ export interface FatRef {
18
+ fe: FatEntry;
19
+ dat: BlobFile;
20
+ }
21
+ /** World (level): worlds/<name>/generated/<name>.game.xml and .sectorsdep.fcb. */
22
+ export interface DuniaWorld {
23
+ name: string;
24
+ gameXml: FatRef;
25
+ /** The world's .dat and the offset of its .sectorsdep.fcb. */
26
+ dat: BlobFile;
27
+ end: number;
28
+ /** Unnamed world files (sectors, terrain): records of the same .dat between the end of the previous world and this .sectorsdep.fcb. */
29
+ files(): FatRef[];
30
+ }
31
+ export interface DuniaArchiveApi {
32
+ duniaWorld(name: string): DuniaWorld | null;
33
+ duniaWorlds(): DuniaWorld[];
34
+ /** All .dat records in offset order. */
35
+ datRefs(dat: BlobFile): FatRef[];
36
+ readRef(r: FatRef, head?: number): Promise<Uint8Array>;
37
+ /** Record by name hash (level collection references are model path hashes) and its name, if known. */
38
+ refByHash(h: number): FatRef | undefined;
39
+ nameOfHash(h: number): string | undefined;
40
+ }
41
+ /** Marker of a virtual level file (worlds/<world>/….fc2level): followed by JSON with the world and the sector area. */
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
+ * All .fat files from the selection (or the game folder) as one archive; opened via the first. Names come from XML resource lists.
54
+ */
55
+ export declare function openDuniaArchive(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;
56
+ /** Worlds of a Dunia archive (for levels), if the archive is 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
+ // a path from model/material references: by hash, even if it is not in the lists
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
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,14 @@
1
+ export interface FcbObject {
2
+ type: number;
3
+ values: Map<number, Uint8Array>;
4
+ children: FcbObject[];
5
+ }
6
+ /** CRC-32 of a class/field name, as in .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
+ /** Tree walk: all objects of type `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
@@ -0,0 +1 @@
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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"}
@@ -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) — 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.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { EngineTexts } from '@gmvjs/core/plugin';
2
+ export declare const EN: EngineTexts;
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
@@ -0,0 +1 @@
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;"}
@@ -0,0 +1 @@
1
+ export declare const dunia1Engine: import("@gmvjs/core/plugin").EnginePlugin;