@gmvjs/core 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -2
  3. package/dist/archives/common.d.ts +28 -0
  4. package/dist/archives/common.js +45 -0
  5. package/dist/archives/common.js.map +1 -0
  6. package/dist/archives/folder.d.ts +4 -0
  7. package/dist/archives/folder.js +65 -0
  8. package/dist/archives/folder.js.map +1 -0
  9. package/dist/archives/zip.d.ts +2 -0
  10. package/dist/archives/zip.js +42 -0
  11. package/dist/archives/zip.js.map +1 -0
  12. package/dist/binary.d.ts +24 -0
  13. package/dist/binary.js +62 -0
  14. package/dist/binary.js.map +1 -0
  15. package/dist/builtin-en.d.ts +2 -0
  16. package/dist/builtin-en.js +24 -0
  17. package/dist/builtin-en.js.map +1 -0
  18. package/dist/builtin.d.ts +1 -0
  19. package/dist/builtin.js +46 -0
  20. package/dist/builtin.js.map +1 -0
  21. package/dist/codecs/zlib.d.ts +4 -0
  22. package/dist/codecs/zlib.js +11 -0
  23. package/dist/codecs/zlib.js.map +1 -0
  24. package/dist/diagnostics.d.ts +10 -0
  25. package/dist/diagnostics.js +34 -0
  26. package/dist/diagnostics.js.map +1 -0
  27. package/dist/geometry.d.ts +19 -0
  28. package/dist/geometry.js +92 -0
  29. package/dist/geometry.js.map +1 -0
  30. package/dist/i18n.d.ts +27 -0
  31. package/dist/i18n.js +88 -0
  32. package/dist/i18n.js.map +1 -0
  33. package/dist/images/bc45.d.ts +2 -0
  34. package/dist/images/bc45.js +49 -0
  35. package/dist/images/bc45.js.map +1 -0
  36. package/dist/images/bc7.d.ts +2 -0
  37. package/dist/images/bc7.js +238 -0
  38. package/dist/images/bc7.js.map +1 -0
  39. package/dist/images/dds.d.ts +6 -0
  40. package/dist/images/dds.js +130 -0
  41. package/dist/images/dds.js.map +1 -0
  42. package/dist/images/pcx.d.ts +2 -0
  43. package/dist/images/pcx.js +44 -0
  44. package/dist/images/pcx.js.map +1 -0
  45. package/dist/images/tga.d.ts +2 -0
  46. package/dist/images/tga.js +58 -0
  47. package/dist/images/tga.js.map +1 -0
  48. package/dist/index.d.ts +30 -0
  49. package/dist/index.js +84 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/levelbuilder.d.ts +31 -0
  52. package/dist/levelbuilder.js +104 -0
  53. package/dist/levelbuilder.js.map +1 -0
  54. package/dist/math.d.ts +24 -0
  55. package/dist/math.js +118 -0
  56. package/dist/math.js.map +1 -0
  57. package/dist/normals.d.ts +15 -0
  58. package/dist/normals.js +110 -0
  59. package/dist/normals.js.map +1 -0
  60. package/dist/plugin.d.ts +241 -0
  61. package/dist/plugin.js +14 -0
  62. package/dist/plugin.js.map +1 -0
  63. package/dist/registry.d.ts +63 -0
  64. package/dist/registry.js +174 -0
  65. package/dist/registry.js.map +1 -0
  66. package/dist/scene.d.ts +52 -0
  67. package/dist/scene.js +135 -0
  68. package/dist/scene.js.map +1 -0
  69. package/dist/session.d.ts +153 -0
  70. package/dist/session.js +500 -0
  71. package/dist/session.js.map +1 -0
  72. package/dist/skeleton.d.ts +26 -0
  73. package/dist/skeleton.js +150 -0
  74. package/dist/skeleton.js.map +1 -0
  75. package/dist/surface.d.ts +10 -0
  76. package/dist/surface.js +53 -0
  77. package/dist/surface.js.map +1 -0
  78. package/dist/textures.d.ts +41 -0
  79. package/dist/textures.js +128 -0
  80. package/dist/textures.js.map +1 -0
  81. package/dist/types.d.ts +383 -0
  82. package/dist/types.js +7 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/writers/dds.d.ts +8 -0
  85. package/dist/writers/dds.js +53 -0
  86. package/dist/writers/dds.js.map +1 -0
  87. package/dist/writers/obj.d.ts +3 -0
  88. package/dist/writers/obj.js +97 -0
  89. package/dist/writers/obj.js.map +1 -0
  90. package/dist/writers/zip.d.ts +2 -0
  91. package/dist/writers/zip.js +69 -0
  92. package/dist/writers/zip.js.map +1 -0
  93. 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,107 @@
1
- # Temporary Holding Version
1
+ # @gmvjs/core — the Game Model Viewer core
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
+ The core of a library for reading game models. It has no game formats at all: only the plugin API, shared model types, a registry with format detection, a viewing session, archives, textures and helper math. Games are added with engine packages (`@gmvjs/xray-v1`, `@gmvjs/source1`…), rendering with the `@gmvjs/renderer` package.
6
+
7
+ Runs in the browser, in a Web Worker (no DOM) and in Node ≥ 20. ESM only, `sideEffects: false`.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm i @gmvjs/core
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
19
+ import { gamebryoEngine } from '@gmvjs/gamebryo';
20
+
21
+ const session = new ViewerSession(createRegistry([gamebryoEngine]), 'skyrimse');
22
+ session.on('status', ({ text, error }) => console[error ? 'error' : 'log'](text));
23
+ session.on('model', async ({ model }) => {
24
+ for (let i = 0; i < model.meshes.length; i++) {
25
+ const t = await session.resolveTexture(i); // { image, label, ok }
26
+ }
27
+ });
28
+
29
+ await session.addArchives([meshesBsa, texturesBsa]);
30
+ await session.openEntry(session.archiveModels('chair').items[0]);
31
+ ```
32
+
33
+ Parsing a file only, without a session (Node, Worker):
34
+
35
+ ```ts
36
+ import { createRegistry } from '@gmvjs/core';
37
+ import { idtech3Engine } from '@gmvjs/idtech3';
38
+
39
+ const reg = createRegistry([idtech3Engine]);
40
+ const det = await reg.detect(file); // { format, confidence, candidates }
41
+ const model = await det!.format.load(file, { files: [file], archives: [], sibling: async () => null, readPath: async () => null });
42
+ ```
43
+
44
+ ## Your own engine
45
+
46
+ ```ts
47
+ import { defineEngine, createRegistry, binary, type Model } from '@gmvjs/core';
48
+
49
+ export const myEngine = defineEngine({
50
+ id: 'my',
51
+ title: 'My Engine',
52
+ formats: [{
53
+ id: 'my-mesh', title: 'My mesh', engine: 'my', extensions: ['mym'], status: 'beta',
54
+ sniff: (head) => (binary.hasMagic(head, 'MYM1') ? 1 : 0),
55
+ load: async (main) => (await import('./parser')).parse(await main.arrayBuffer()) as Model,
56
+ }],
57
+ games: [{ id: 'my-game', group: 'Games', title: 'My Game', engine: 'my', formats: ['my-mesh'] }],
58
+ i18n: { en: { title: 'My Engine', presets: { 'my-game': { title: 'My Game' } } } },
59
+ });
60
+ ```
61
+
62
+ By convention an engine package exports `<id>Engine` from its root and a subpath per game: `@gmvjs/<engine>/<game>` → `{ preset, engine }`, where `engine` is the same engine limited to that game's preset (`pickGames(engine, id)`).
63
+
64
+ ## What's inside
65
+
66
+ | Module | Purpose |
67
+ |---|---|
68
+ | `types` | `Model`, `Mesh`, `Motion` (`NodeMotion`, `VertexMotion`, extensible by engines), `SkeletonRuntime`, `Lightmap`, `ScreenFx`, `InstancedScene`, `Archive`, `VFile` |
69
+ | `plugin` | `defineEngine`, `EnginePlugin`, `FormatPlugin`, `ArchivePlugin`, `ImageCodec`, `TextureDictionaryPlugin`, `MaterialResolver`, `ExporterPlugin`, `GamePreset`, `pickGames()` |
70
+ | `registry` | `Registry`, `createRegistry()`: plugin collection, detection by extension, path and first bytes, preset by format and folder markers |
71
+ | `session` | `ViewerSession`: files, "Game folder", archives, model loading, textures, normals, PBR, external animations, `status` / `model` / `preset` events; `loadSameDirMotions` |
72
+ | `textures` | `TextureLibrary`: texture lookup by name across all open archives, decoding through engine codecs |
73
+ | `normals`, `surface` | tangents, normal map packing detection (RGB / RG / AG), combining PBR maps |
74
+ | `skeleton` | `NodeSkeleton`: poses for node hierarchies: keys, layers, additive tracks, grid blending |
75
+ | `scene`, `levelbuilder` | scene tree, ray picking, moving instances; building a level from instances by material |
76
+ | `archives` | `createArchive`, folder (`openFolder`), ZIP (`openZip`; PK3/PK4/IWD/PAK) |
77
+ | `codecs` | zlib / raw deflate (`DecompressionStream`) |
78
+ | `images` | DDS (DXT1–5, uncompressed), TGA, PCX; BC4/BC5 (`decodeBC45`) and BC7 (`decodeBC7`) decoders for engines |
79
+ | `writers` | ZIP, DDS (RGBA), OBJ + MTL (the `obj` export) |
80
+ | `i18n` | `setLocale('ru' / 'en')`, `tr()`, `presetText()`, `engineTitle()`, `exporterText()`: English preset texts come from `EnginePlugin.i18n` |
81
+ | `diagnostics` | `buildDiagnostics`, `hexDump`: a report about a failed load |
82
+ | `binary`, `math`, `geometry` | reading binary data, matrices and quaternions, bounds and normals |
83
+ | `builtin` | `coreEngine`: the ZIP archive, DDS/TGA/PCX codecs, OBJ export; `createRegistry` always attaches it |
84
+
85
+ ## API notes
86
+
87
+ - **`EnginePlugin.i18n`** — `{ en?: { title?, groups?, presets?: Record<id, { title?, hint?, archiveLabel?, motionLabel? }>, exporters?: Record<id, { title?, description?, fields? }> } }`. The registry attaches the texts together with the engine; after `setLocale('en')`, `presetText` / `engineTitle` / `exporterText` return English texts.
88
+ - **`MotionKinds`** — animation kinds by the `kind` field (the core has `node` and `vertex`). An engine adds its own kind by extending the interface, and narrowing by `m.kind` works in application code:
89
+
90
+ ```ts
91
+ declare module '@gmvjs/core/types' { interface MotionKinds { xray: XrayMotion } }
92
+ ```
93
+ - **`pickGames(engine, ...ids)`** — an engine with only the chosen presets and the formats they reference (archives, codecs and export: all of them). The `@gmvjs/<engine>/<game>` subpaths are built on it. Two slices of one engine in the same registry are merged.
94
+ - **`Registry.presetAvailable(preset)`** — whether the registry has the preset's format. A bridge preset with another package's format (`@gmvjs/source2/gltf`, `@gmvjs/cryengine3/dae` → `@gmvjs/assimp`) is unavailable without it, but the registry does not break.
95
+ - **Module subpaths** `@gmvjs/core/<module>` (`binary`, `geometry`, `math`, `levelbuilder`, `images/dds`, `archives/common`…) are for engine packages.
96
+
97
+ ## Dependencies
98
+
99
+ None.
100
+
101
+ ## License
102
+
103
+ MIT
104
+
105
+ [![Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/cw/shibisty)
106
+
107
+ If this project helps you, consider supporting its development on Patreon ❤️
@@ -0,0 +1,28 @@
1
+ import type { Archive, ArchiveEntry, BlobFile } from '../types';
2
+ /** Extensions that put an entry into the texture index (path without extension). */
3
+ export declare const TEXTURE_RE: RegExp;
4
+ export interface ArchiveSpec<E extends ArchiveEntry> {
5
+ name: string;
6
+ kind: string;
7
+ version: number;
8
+ entries: E[];
9
+ read(entry: E): Promise<Uint8Array>;
10
+ /** Custom lookup function (for example, by path suffix). */
11
+ lookup?(path: string): E | null;
12
+ byBase?: Map<string, E>;
13
+ /** Extra texture keys (default: the full path without extension). */
14
+ textureKeys?(entry: E): string[];
15
+ /** Display file name (default: the last path segment). */
16
+ displayName?(entry: E): string;
17
+ /** On a duplicate texture key keep the first entry (default: the last one, as in game archives). */
18
+ keepFirstTextureKey?: boolean;
19
+ /** Custom file contents (without a copy via read). */
20
+ fileBytes?(entry: E): Promise<ArrayBuffer>;
21
+ /** Source file of the entry (folder). */
22
+ blobOf?(entry: E): BlobFile | null;
23
+ /** Texture at an exact path that is not in the entry list. */
24
+ findTexture?(path: string): E | null;
25
+ }
26
+ export declare function createArchive<E extends ArchiveEntry>(spec: ArchiveSpec<E>): Archive;
27
+ /** Read a byte range of the file. */
28
+ export declare const readRange: (file: Blob, start: number, end: number) => Promise<Uint8Array>;
@@ -0,0 +1,45 @@
1
+ import { stripExt, toArrayBuffer, baseName } from "../binary.js";
2
+ const TEXTURE_RE = /\.(dds|tga|png|jpe?g|pcx|bmp|webp|paa|pac|pct|tex|xbt)$/;
3
+ function createArchive(spec) {
4
+ const index = /* @__PURE__ */ new Map(), byKey = /* @__PURE__ */ new Map(), files = /* @__PURE__ */ new Map();
5
+ for (const e of spec.entries) {
6
+ index.set(e.path, e);
7
+ if (TEXTURE_RE.test(e.path)) {
8
+ for (const k of spec.textureKeys ? spec.textureKeys(e) : [stripExt(e.path)]) if (!spec.keepFirstTextureKey || !byKey.has(k)) byKey.set(k, e);
9
+ }
10
+ }
11
+ const arch = {
12
+ name: spec.name,
13
+ kind: spec.kind,
14
+ version: spec.version,
15
+ entries: spec.entries,
16
+ index,
17
+ byKey,
18
+ byBase: spec.byBase,
19
+ lookup: (p) => spec.lookup ? spec.lookup(p) : index.get(p) ?? null,
20
+ read: (e) => spec.read(e),
21
+ blobOf: spec.blobOf ? (e) => spec.blobOf(e) : void 0,
22
+ findTexture: spec.findTexture ? (p) => spec.findTexture(p) : void 0,
23
+ fileOf(e) {
24
+ let f = files.get(e);
25
+ if (!f) {
26
+ f = {
27
+ name: spec.displayName ? spec.displayName(e) : baseName(e.path),
28
+ path: e.path,
29
+ archive: arch,
30
+ arrayBuffer: spec.fileBytes ? () => spec.fileBytes(e) : async () => toArrayBuffer(await spec.read(e))
31
+ };
32
+ files.set(e, f);
33
+ }
34
+ return f;
35
+ }
36
+ };
37
+ return arch;
38
+ }
39
+ const readRange = async (file, start, end) => new Uint8Array(await file.slice(start, end).arrayBuffer());
40
+ export {
41
+ TEXTURE_RE,
42
+ createArchive,
43
+ readRange
44
+ };
45
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sources":["../../src/archives/common.ts"],"sourcesContent":["// Common archive plumbing: path and texture indexes, file-like entry objects.\nimport { baseName, stripExt, toArrayBuffer } from '../binary';\nimport type { Archive, ArchiveEntry, BlobFile, VFile } from '../types';\n\n/** Extensions that put an entry into the texture index (path without extension). */\nexport const TEXTURE_RE = /\\.(dds|tga|png|jpe?g|pcx|bmp|webp|paa|pac|pct|tex|xbt)$/;\n\nexport interface ArchiveSpec<E extends ArchiveEntry> {\n name: string;\n kind: string;\n version: number;\n entries: E[];\n read(entry: E): Promise<Uint8Array>;\n /** Custom lookup function (for example, by path suffix). */\n lookup?(path: string): E | null;\n byBase?: Map<string, E>;\n /** Extra texture keys (default: the full path without extension). */\n textureKeys?(entry: E): string[];\n /** Display file name (default: the last path segment). */\n displayName?(entry: E): string;\n /** On a duplicate texture key keep the first entry (default: the last one, as in game archives). */\n keepFirstTextureKey?: boolean;\n /** Custom file contents (without a copy via read). */\n fileBytes?(entry: E): Promise<ArrayBuffer>;\n /** Source file of the entry (folder). */\n blobOf?(entry: E): BlobFile | null;\n /** Texture at an exact path that is not in the entry list. */\n findTexture?(path: string): E | null;\n}\n\nexport function createArchive<E extends ArchiveEntry>(spec: ArchiveSpec<E>): Archive {\n const index = new Map<string, ArchiveEntry>(), byKey = new Map<string, ArchiveEntry>(), files = new Map<ArchiveEntry, VFile>();\n for (const e of spec.entries) {\n index.set(e.path, e); // duplicate path (mods, appended archives): the last entry wins\n if (TEXTURE_RE.test(e.path)) {\n for (const k of spec.textureKeys ? spec.textureKeys(e) : [stripExt(e.path)]) if (!spec.keepFirstTextureKey || !byKey.has(k)) byKey.set(k, e);\n }\n }\n const arch: Archive = {\n name: spec.name,\n kind: spec.kind,\n version: spec.version,\n entries: spec.entries,\n index,\n byKey,\n byBase: spec.byBase,\n lookup: (p) => (spec.lookup ? spec.lookup(p) : (index.get(p) as E | undefined) ?? null),\n read: (e) => spec.read(e as E),\n blobOf: spec.blobOf ? (e) => spec.blobOf!(e as E) : undefined,\n findTexture: spec.findTexture ? (p) => spec.findTexture!(p) : undefined,\n fileOf(e) {\n let f = files.get(e);\n if (!f) {\n f = {\n name: spec.displayName ? spec.displayName(e as E) : baseName(e.path),\n path: e.path,\n archive: arch,\n arrayBuffer: spec.fileBytes ? () => spec.fileBytes!(e as E) : async () => toArrayBuffer(await spec.read(e as E)),\n };\n files.set(e, f);\n }\n return f;\n },\n };\n return arch;\n}\n\n/** Read a byte range of the file. */\nexport const readRange = async (file: Blob, start: number, end: number): Promise<Uint8Array> =>\n new Uint8Array(await file.slice(start, end).arrayBuffer());\n"],"names":[],"mappings":";AAKO,MAAM,aAAa;AAyBnB,SAAS,cAAsC,MAA+B;AACnF,QAAM,QAAQ,oBAAI,OAA6B,4BAAY,IAAA,GAA6B,QAAQ,oBAAI,IAAA;AACpG,aAAW,KAAK,KAAK,SAAS;AAC5B,UAAM,IAAI,EAAE,MAAM,CAAC;AACnB,QAAI,WAAW,KAAK,EAAE,IAAI,GAAG;AAC3B,iBAAW,KAAK,KAAK,cAAc,KAAK,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAG,KAAI,CAAC,KAAK,uBAAuB,CAAC,MAAM,IAAI,CAAC,EAAG,OAAM,IAAI,GAAG,CAAC;AAAA,IAC7I;AAAA,EACF;AACA,QAAM,OAAgB;AAAA,IACpB,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd;AAAA,IACA;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,QAAQ,CAAC,MAAO,KAAK,SAAS,KAAK,OAAO,CAAC,IAAK,MAAM,IAAI,CAAC,KAAuB;AAAA,IAClF,MAAM,CAAC,MAAM,KAAK,KAAK,CAAM;AAAA,IAC7B,QAAQ,KAAK,SAAS,CAAC,MAAM,KAAK,OAAQ,CAAM,IAAI;AAAA,IACpD,aAAa,KAAK,cAAc,CAAC,MAAM,KAAK,YAAa,CAAC,IAAI;AAAA,IAC9D,OAAO,GAAG;AACR,UAAI,IAAI,MAAM,IAAI,CAAC;AACnB,UAAI,CAAC,GAAG;AACN,YAAI;AAAA,UACF,MAAM,KAAK,cAAc,KAAK,YAAY,CAAM,IAAI,SAAS,EAAE,IAAI;AAAA,UACnE,MAAM,EAAE;AAAA,UACR,SAAS;AAAA,UACT,aAAa,KAAK,YAAY,MAAM,KAAK,UAAW,CAAM,IAAI,YAAY,cAAc,MAAM,KAAK,KAAK,CAAM,CAAC;AAAA,QAAA;AAEjH,cAAM,IAAI,GAAG,CAAC;AAAA,MAChB;AACA,aAAO;AAAA,IACT;AAAA,EAAA;AAEF,SAAO;AACT;AAGO,MAAM,YAAY,OAAO,MAAY,OAAe,QACzD,IAAI,WAAW,MAAM,KAAK,MAAM,OAAO,GAAG,EAAE,aAAa;"}
@@ -0,0 +1,4 @@
1
+ import type { Archive, BlobFile } from '../types';
2
+ export declare function openFolder(files: readonly BlobFile[]): Archive;
3
+ /** The same, but in chunks of `chunk` files, yielding to the browser (large game folders have hundreds of thousands of files). */
4
+ export declare function openFolderAsync(files: readonly BlobFile[], onProgress?: (done: number, total: number) => void, chunk?: number): Promise<Archive>;
@@ -0,0 +1,65 @@
1
+ import { stripExt } from "../binary.js";
2
+ import { createArchive, TEXTURE_RE } from "./common.js";
3
+ const relPath = (f) => (f.webkitRelativePath || f.name).replace(/\\/g, "/");
4
+ class FolderIndex {
5
+ entries = [];
6
+ full = /* @__PURE__ */ new Map();
7
+ byBase = /* @__PURE__ */ new Map();
8
+ bySuffix = /* @__PURE__ */ new Map();
9
+ root = "";
10
+ add(f) {
11
+ const rel = relPath(f), path = rel.toLowerCase();
12
+ if (!this.root && rel.includes("/")) this.root = rel.split("/")[0];
13
+ const e = { path, size: f.size, src: f };
14
+ this.entries.push(e);
15
+ if (!this.full.has(path)) this.full.set(path, e);
16
+ const sp = path.split("/"), base = sp[sp.length - 1];
17
+ if (!this.byBase.has(base)) this.byBase.set(base, e);
18
+ for (let i = 0; i < sp.length; i++) {
19
+ const k = sp.slice(i).join("/");
20
+ if (!this.bySuffix.has(k)) this.bySuffix.set(k, e);
21
+ }
22
+ }
23
+ archive() {
24
+ const { full, bySuffix } = this;
25
+ return createArchive({
26
+ name: this.root || "папка",
27
+ kind: "folder",
28
+ version: 0,
29
+ entries: this.entries,
30
+ byBase: this.byBase,
31
+ lookup: (p) => full.get(p) ?? bySuffix.get(p) ?? null,
32
+ read: async (e) => new Uint8Array(await e.src.arrayBuffer()),
33
+ fileBytes: (e) => e.src.arrayBuffer(),
34
+ blobOf: (e) => e.src,
35
+ displayName: (e) => e.src.name,
36
+ keepFirstTextureKey: true,
37
+ // keys for every path suffix: a/b/c, b/c, c — not tied to the folder root
38
+ textureKeys: (e) => {
39
+ const parts = stripExt(e.path).split("/");
40
+ return TEXTURE_RE.test(e.path) ? parts.map((_, i) => parts.slice(i).join("/")) : [];
41
+ }
42
+ });
43
+ }
44
+ }
45
+ function openFolder(files) {
46
+ const ix = new FolderIndex();
47
+ for (const f of files) ix.add(f);
48
+ return ix.archive();
49
+ }
50
+ async function openFolderAsync(files, onProgress, chunk = 5e3) {
51
+ const ix = new FolderIndex();
52
+ for (let i = 0; i < files.length; i += chunk) {
53
+ onProgress?.(i, files.length);
54
+ await new Promise((r) => setTimeout(r, 0));
55
+ for (let k = i; k < Math.min(files.length, i + chunk); k++) ix.add(files[k]);
56
+ }
57
+ onProgress?.(files.length, files.length);
58
+ await new Promise((r) => setTimeout(r, 0));
59
+ return ix.archive();
60
+ }
61
+ export {
62
+ openFolder,
63
+ openFolderAsync
64
+ };
65
+ //# sourceMappingURL=folder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folder.js","sources":["../../src/archives/folder.ts"],"sourcesContent":["// \"Game folder\": loose files of the selected folder as a virtual archive (lookup by path suffix, without reading contents).\nimport { stripExt } from '../binary';\nimport type { Archive, ArchiveEntry, BlobFile } from '../types';\nimport { createArchive, TEXTURE_RE } from './common';\n\ninterface FolderEntry extends ArchiveEntry { src: BlobFile }\n\nconst relPath = (f: BlobFile): string => (f.webkitRelativePath || f.name).replace(/\\\\/g, '/');\n\n/** Folder index: paths, full and by suffix (built in chunks, see openFolderAsync). */\nclass FolderIndex {\n entries: FolderEntry[] = []; full = new Map<string, FolderEntry>(); byBase = new Map<string, FolderEntry>(); bySuffix = new Map<string, FolderEntry>(); root = '';\n add(f: BlobFile): void {\n const rel = relPath(f), path = rel.toLowerCase();\n if (!this.root && rel.includes('/')) this.root = rel.split('/')[0]!;\n const e: FolderEntry = { path, size: f.size, src: f };\n this.entries.push(e);\n if (!this.full.has(path)) this.full.set(path, e);\n const sp = path.split('/'), base = sp[sp.length - 1]!;\n if (!this.byBase.has(base)) this.byBase.set(base, e);\n for (let i = 0; i < sp.length; i++) { const k = sp.slice(i).join('/'); if (!this.bySuffix.has(k)) this.bySuffix.set(k, e); }\n }\n archive(): Archive {\n const { full, bySuffix } = this;\n return createArchive<FolderEntry>({\n name: this.root || 'папка',\n kind: 'folder',\n version: 0,\n entries: this.entries,\n byBase: this.byBase,\n lookup: (p) => full.get(p) ?? bySuffix.get(p) ?? null,\n read: async (e) => new Uint8Array(await e.src.arrayBuffer()),\n fileBytes: (e) => e.src.arrayBuffer(),\n blobOf: (e) => e.src,\n displayName: (e) => e.src.name,\n keepFirstTextureKey: true,\n // keys for every path suffix: a/b/c, b/c, c — not tied to the folder root\n textureKeys: (e) => {\n const parts = stripExt(e.path).split('/');\n return TEXTURE_RE.test(e.path) ? parts.map((_, i) => parts.slice(i).join('/')) : [];\n },\n });\n }\n}\n\nexport function openFolder(files: readonly BlobFile[]): Archive {\n const ix = new FolderIndex();\n for (const f of files) ix.add(f);\n return ix.archive();\n}\n\n/** The same, but in chunks of `chunk` files, yielding to the browser (large game folders have hundreds of thousands of files). */\nexport async function openFolderAsync(files: readonly BlobFile[], onProgress?: (done: number, total: number) => void, chunk = 5000): Promise<Archive> {\n const ix = new FolderIndex();\n for (let i = 0; i < files.length; i += chunk) {\n onProgress?.(i, files.length);\n await new Promise((r) => setTimeout(r, 0));\n for (let k = i; k < Math.min(files.length, i + chunk); k++) ix.add(files[k]!);\n }\n onProgress?.(files.length, files.length);\n await new Promise((r) => setTimeout(r, 0));\n return ix.archive();\n}\n"],"names":[],"mappings":";;AAOA,MAAM,UAAU,CAAC,OAAyB,EAAE,sBAAsB,EAAE,MAAM,QAAQ,OAAO,GAAG;AAG5F,MAAM,YAAY;AAAA,EAChB,UAAyB,CAAA;AAAA,EAAI,2BAAW,IAAA;AAAA,EAA4B,6BAAa,IAAA;AAAA,EAA4B,+BAAe,IAAA;AAAA,EAA4B,OAAO;AAAA,EAC/J,IAAI,GAAmB;AACrB,UAAM,MAAM,QAAQ,CAAC,GAAG,OAAO,IAAI,YAAA;AACnC,QAAI,CAAC,KAAK,QAAQ,IAAI,SAAS,GAAG,EAAG,MAAK,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AACjE,UAAM,IAAiB,EAAE,MAAM,MAAM,EAAE,MAAM,KAAK,EAAA;AAClD,SAAK,QAAQ,KAAK,CAAC;AACnB,QAAI,CAAC,KAAK,KAAK,IAAI,IAAI,EAAG,MAAK,KAAK,IAAI,MAAM,CAAC;AAC/C,UAAM,KAAK,KAAK,MAAM,GAAG,GAAG,OAAO,GAAG,GAAG,SAAS,CAAC;AACnD,QAAI,CAAC,KAAK,OAAO,IAAI,IAAI,EAAG,MAAK,OAAO,IAAI,MAAM,CAAC;AACnD,aAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AAAE,YAAM,IAAI,GAAG,MAAM,CAAC,EAAE,KAAK,GAAG;AAAG,UAAI,CAAC,KAAK,SAAS,IAAI,CAAC,EAAG,MAAK,SAAS,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,EAC7H;AAAA,EACA,UAAmB;AACjB,UAAM,EAAE,MAAM,SAAA,IAAa;AAC3B,WAAO,cAA2B;AAAA,MAChC,MAAM,KAAK,QAAQ;AAAA,MACnB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK;AAAA,MACjD,MAAM,OAAO,MAAM,IAAI,WAAW,MAAM,EAAE,IAAI,aAAa;AAAA,MAC3D,WAAW,CAAC,MAAM,EAAE,IAAI,YAAA;AAAA,MACxB,QAAQ,CAAC,MAAM,EAAE;AAAA,MACjB,aAAa,CAAC,MAAM,EAAE,IAAI;AAAA,MAC1B,qBAAqB;AAAA;AAAA,MAErB,aAAa,CAAC,MAAM;AAClB,cAAM,QAAQ,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG;AACxC,eAAO,WAAW,KAAK,EAAE,IAAI,IAAI,MAAM,IAAI,CAAC,GAAG,MAAM,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,IAAI,CAAA;AAAA,MACnF;AAAA,IAAA,CACD;AAAA,EACH;AACF;AAEO,SAAS,WAAW,OAAqC;AAC9D,QAAM,KAAK,IAAI,YAAA;AACf,aAAW,KAAK,MAAO,IAAG,IAAI,CAAC;AAC/B,SAAO,GAAG,QAAA;AACZ;AAGA,eAAsB,gBAAgB,OAA4B,YAAoD,QAAQ,KAAwB;AACpJ,QAAM,KAAK,IAAI,YAAA;AACf,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,OAAO;AAC5C,iBAAa,GAAG,MAAM,MAAM;AAC5B,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AACzC,aAAS,IAAI,GAAG,IAAI,KAAK,IAAI,MAAM,QAAQ,IAAI,KAAK,GAAG,IAAK,IAAG,IAAI,MAAM,CAAC,CAAE;AAAA,EAC9E;AACA,eAAa,MAAM,QAAQ,MAAM,MAAM;AACvC,QAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AACzC,SAAO,GAAG,QAAA;AACZ;"}
@@ -0,0 +1,2 @@
1
+ import type { Archive, BlobFile } from '../types';
2
+ export declare function openZip(file: BlobFile): Promise<Archive>;
@@ -0,0 +1,42 @@
1
+ import { normPath } from "../binary.js";
2
+ import { inflateRaw } from "../codecs/zlib.js";
3
+ import { readRange, createArchive } from "./common.js";
4
+ async function openZip(file) {
5
+ const tailLen = Math.min(file.size, 65557), tail = await readRange(file, file.size - tailLen, file.size);
6
+ const tdv = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);
7
+ let e = -1;
8
+ for (let i = tail.length - 22; i >= 0; i--) if (tdv.getUint32(i, true) === 101010256) {
9
+ e = i;
10
+ break;
11
+ }
12
+ if (e < 0) throw new Error(`${file.name}: не ZIP (нет конца каталога)`);
13
+ const cdSize = tdv.getUint32(e + 12, true), cdOff = tdv.getUint32(e + 16, true);
14
+ if (cdOff === 4294967295) throw new Error(`${file.name}: ZIP64 не поддерживается`);
15
+ const u8 = await readRange(file, cdOff, cdOff + cdSize), dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), entries = [];
16
+ for (let p = 0; p + 46 <= u8.length && dv.getUint32(p, true) === 33639248; ) {
17
+ const method = dv.getUint16(p + 10, true), comp = dv.getUint32(p + 20, true), size = dv.getUint32(p + 24, true);
18
+ const nl = dv.getUint16(p + 28, true), xl = dv.getUint16(p + 30, true), cl = dv.getUint16(p + 32, true), off = dv.getUint32(p + 42, true);
19
+ let n = "";
20
+ for (let i = 0; i < nl; i++) n += String.fromCharCode(u8[p + 46 + i]);
21
+ if (!n.endsWith("/")) entries.push({ path: normPath(n), method, comp, size, off });
22
+ p += 46 + nl + xl + cl;
23
+ }
24
+ return createArchive({
25
+ name: file.name,
26
+ kind: "zip",
27
+ version: 0,
28
+ entries,
29
+ async read(en) {
30
+ const lh = new DataView(await file.slice(en.off, en.off + 30).arrayBuffer());
31
+ const start = en.off + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);
32
+ const data = await readRange(file, start, start + en.comp);
33
+ if (en.method === 0) return data;
34
+ if (en.method === 8) return inflateRaw(data);
35
+ throw new Error("метод сжатия ZIP " + en.method + " не поддерживается");
36
+ }
37
+ });
38
+ }
39
+ export {
40
+ openZip
41
+ };
42
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.js","sources":["../../src/archives/zip.ts"],"sourcesContent":["// ZIP (id Tech pk3/pk4, CryEngine .pak, .zip): the central directory is read, data on demand (store / deflate).\nimport { normPath } from '../binary';\nimport { inflateRaw } from '../codecs/zlib';\nimport type { Archive, ArchiveEntry, BlobFile } from '../types';\nimport { createArchive, readRange } from './common';\n\ninterface ZipEntry extends ArchiveEntry { method: number; comp: number; off: number }\n\nexport async function openZip(file: BlobFile): Promise<Archive> {\n const tailLen = Math.min(file.size, 65557), tail = await readRange(file, file.size - tailLen, file.size);\n const tdv = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);\n let e = -1;\n for (let i = tail.length - 22; i >= 0; i--) if (tdv.getUint32(i, true) === 0x06054b50) { e = i; break; }\n if (e < 0) throw new Error(`${file.name}: не ZIP (нет конца каталога)`);\n const cdSize = tdv.getUint32(e + 12, true), cdOff = tdv.getUint32(e + 16, true);\n if (cdOff === 0xffffffff) throw new Error(`${file.name}: ZIP64 не поддерживается`);\n const u8 = await readRange(file, cdOff, cdOff + cdSize), dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), entries: ZipEntry[] = [];\n for (let p = 0; p + 46 <= u8.length && dv.getUint32(p, true) === 0x02014b50;) {\n const method = dv.getUint16(p + 10, true), comp = dv.getUint32(p + 20, true), size = dv.getUint32(p + 24, true);\n const nl = dv.getUint16(p + 28, true), xl = dv.getUint16(p + 30, true), cl = dv.getUint16(p + 32, true), off = dv.getUint32(p + 42, true);\n let n = '';\n for (let i = 0; i < nl; i++) n += String.fromCharCode(u8[p + 46 + i]!);\n if (!n.endsWith('/')) entries.push({ path: normPath(n), method, comp, size, off });\n p += 46 + nl + xl + cl;\n }\n return createArchive<ZipEntry>({\n name: file.name,\n kind: 'zip',\n version: 0,\n entries,\n async read(en) {\n const lh = new DataView(await file.slice(en.off, en.off + 30).arrayBuffer());\n const start = en.off + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);\n const data = await readRange(file, start, start + en.comp);\n if (en.method === 0) return data;\n if (en.method === 8) return inflateRaw(data);\n throw new Error('метод сжатия ZIP ' + en.method + ' не поддерживается');\n },\n });\n}\n"],"names":[],"mappings":";;;AAQA,eAAsB,QAAQ,MAAkC;AAC9D,QAAM,UAAU,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,OAAO,MAAM,UAAU,MAAM,KAAK,OAAO,SAAS,KAAK,IAAI;AACvG,QAAM,MAAM,IAAI,SAAS,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AACtE,MAAI,IAAI;AACR,WAAS,IAAI,KAAK,SAAS,IAAI,KAAK,GAAG,IAAK,KAAI,IAAI,UAAU,GAAG,IAAI,MAAM,WAAY;AAAE,QAAI;AAAG;AAAA,EAAO;AACvG,MAAI,IAAI,EAAG,OAAM,IAAI,MAAM,GAAG,KAAK,IAAI,+BAA+B;AACtE,QAAM,SAAS,IAAI,UAAU,IAAI,IAAI,IAAI,GAAG,QAAQ,IAAI,UAAU,IAAI,IAAI,IAAI;AAC9E,MAAI,UAAU,WAAY,OAAM,IAAI,MAAM,GAAG,KAAK,IAAI,2BAA2B;AACjF,QAAM,KAAK,MAAM,UAAU,MAAM,OAAO,QAAQ,MAAM,GAAG,KAAK,IAAI,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,UAAsB,CAAA;AAC3I,WAAS,IAAI,GAAG,IAAI,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,IAAI,MAAM,YAAa;AAC5E,UAAM,SAAS,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,OAAO,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,OAAO,GAAG,UAAU,IAAI,IAAI,IAAI;AAC9G,UAAM,KAAK,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,MAAM,GAAG,UAAU,IAAI,IAAI,IAAI;AACxI,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,IAAI,IAAK,MAAK,OAAO,aAAa,GAAG,IAAI,KAAK,CAAC,CAAE;AACrE,QAAI,CAAC,EAAE,SAAS,GAAG,WAAW,KAAK,EAAE,MAAM,SAAS,CAAC,GAAG,QAAQ,MAAM,MAAM,KAAK;AACjF,SAAK,KAAK,KAAK,KAAK;AAAA,EACtB;AACA,SAAO,cAAwB;AAAA,IAC7B,MAAM,KAAK;AAAA,IACX,MAAM;AAAA,IACN,SAAS;AAAA,IACT;AAAA,IACA,MAAM,KAAK,IAAI;AACb,YAAM,KAAK,IAAI,SAAS,MAAM,KAAK,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,EAAE,aAAa;AAC3E,YAAM,QAAQ,GAAG,MAAM,KAAK,GAAG,UAAU,IAAI,IAAI,IAAI,GAAG,UAAU,IAAI,IAAI;AAC1E,YAAM,OAAO,MAAM,UAAU,MAAM,OAAO,QAAQ,GAAG,IAAI;AACzD,UAAI,GAAG,WAAW,EAAG,QAAO;AAC5B,UAAI,GAAG,WAAW,EAAG,QAAO,WAAW,IAAI;AAC3C,YAAM,IAAI,MAAM,sBAAsB,GAAG,SAAS,oBAAoB;AAAA,IACxE;AAAA,EAAA,CACD;AACH;"}
@@ -0,0 +1,24 @@
1
+ /** ASCII string up to a zero byte or n bytes. */
2
+ export declare function cstr(u8: Uint8Array, start: number, maxLen: number): string;
3
+ /** ASCII string in [a, b) up to the first zero byte. */
4
+ export declare function cstrRange(u8: Uint8Array, a: number, b: number): string;
5
+ /** ASCII in [a, b) without stopping at zero bytes. */
6
+ export declare function ascii(u8: Uint8Array, a: number, b: number): string;
7
+ /** Four characters as a little-endian uint32. */
8
+ export declare const fourCC: (s: string) => number;
9
+ /** IEEE 754 half -> number. */
10
+ export declare function half(x: number): number;
11
+ /** Copy of a Uint8Array's contents as a separate ArrayBuffer. */
12
+ export declare function toArrayBuffer(u8: Uint8Array): ArrayBuffer;
13
+ /** First n bytes of a file (for format detection). */
14
+ export declare function readHead(file: Blob | {
15
+ arrayBuffer(): Promise<ArrayBuffer>;
16
+ }, n?: number): Promise<Uint8Array>;
17
+ /** Whether the first bytes match an ASCII signature. */
18
+ export declare function hasMagic(head: Uint8Array, magic: string, offset?: number): boolean;
19
+ export declare const u32le: (u8: Uint8Array, o: number) => number;
20
+ export declare const extOf: (name: string) => string;
21
+ export declare const normPath: (p: string) => string;
22
+ export declare const stripExt: (p: string) => string;
23
+ export declare const baseName: (p: string) => string;
24
+ export declare const decodeText: (data: Uint8Array | ArrayBuffer) => string;
package/dist/binary.js ADDED
@@ -0,0 +1,62 @@
1
+ function cstr(u8, start, maxLen) {
2
+ let s = "";
3
+ for (let i = 0; i < maxLen && u8[start + i]; i++) s += String.fromCharCode(u8[start + i]);
4
+ return s;
5
+ }
6
+ function cstrRange(u8, a, b) {
7
+ let s = "";
8
+ for (let i = a; i < b && u8[i]; i++) s += String.fromCharCode(u8[i]);
9
+ return s;
10
+ }
11
+ function ascii(u8, a, b) {
12
+ let s = "";
13
+ for (let i = a; i < b; i++) s += String.fromCharCode(u8[i]);
14
+ return s;
15
+ }
16
+ const fourCC = (s) => (s.charCodeAt(0) | s.charCodeAt(1) << 8 | s.charCodeAt(2) << 16 | s.charCodeAt(3) << 24) >>> 0;
17
+ function half(x) {
18
+ const s = x & 32768 ? -1 : 1, e = x >> 10 & 31, f = x & 1023;
19
+ if (e === 0) return s * 5960464477539063e-23 * f;
20
+ if (e === 31) return f ? NaN : s * Infinity;
21
+ return s * 2 ** (e - 15) * (1 + f / 1024);
22
+ }
23
+ function toArrayBuffer(u8) {
24
+ return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
25
+ }
26
+ async function readHead(file, n = 64) {
27
+ if (typeof Blob !== "undefined" && file instanceof Blob) return new Uint8Array(await file.slice(0, n).arrayBuffer());
28
+ const buf = await file.arrayBuffer();
29
+ return new Uint8Array(buf, 0, Math.min(n, buf.byteLength));
30
+ }
31
+ function hasMagic(head, magic, offset = 0) {
32
+ if (head.length < offset + magic.length) return false;
33
+ for (let i = 0; i < magic.length; i++) if (head[offset + i] !== magic.charCodeAt(i)) return false;
34
+ return true;
35
+ }
36
+ const u32le = (u8, o) => u8.length >= o + 4 ? (u8[o] | u8[o + 1] << 8 | u8[o + 2] << 16 | u8[o + 3] << 24) >>> 0 : -1;
37
+ const extOf = (name) => {
38
+ const i = name.lastIndexOf(".");
39
+ return i < 0 ? "" : name.slice(i + 1).toLowerCase();
40
+ };
41
+ const normPath = (p) => p.replace(/\\/g, "/").toLowerCase();
42
+ const stripExt = (p) => p.replace(/\.[^./]+$/, "");
43
+ const baseName = (p) => p.replace(/\\/g, "/").split("/").pop() ?? p;
44
+ const decoder = new TextDecoder();
45
+ const decodeText = (data) => decoder.decode(data);
46
+ export {
47
+ ascii,
48
+ baseName,
49
+ cstr,
50
+ cstrRange,
51
+ decodeText,
52
+ extOf,
53
+ fourCC,
54
+ half,
55
+ hasMagic,
56
+ normPath,
57
+ readHead,
58
+ stripExt,
59
+ toArrayBuffer,
60
+ u32le
61
+ };
62
+ //# sourceMappingURL=binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.js","sources":["../src/binary.ts"],"sourcesContent":["// Small helpers for reading binary data.\n\n/** ASCII string up to a zero byte or n bytes. */\nexport function cstr(u8: Uint8Array, start: number, maxLen: number): string {\n let s = '';\n for (let i = 0; i < maxLen && u8[start + i]; i++) s += String.fromCharCode(u8[start + i]!);\n return s;\n}\n\n/** ASCII string in [a, b) up to the first zero byte. */\nexport function cstrRange(u8: Uint8Array, a: number, b: number): string {\n let s = '';\n for (let i = a; i < b && u8[i]; i++) s += String.fromCharCode(u8[i]!);\n return s;\n}\n\n/** ASCII in [a, b) without stopping at zero bytes. */\nexport function ascii(u8: Uint8Array, a: number, b: number): string {\n let s = '';\n for (let i = a; i < b; i++) s += String.fromCharCode(u8[i]!);\n return s;\n}\n\n/** Four characters as a little-endian uint32. */\nexport const fourCC = (s: string): number =>\n (s.charCodeAt(0) | (s.charCodeAt(1) << 8) | (s.charCodeAt(2) << 16) | (s.charCodeAt(3) << 24)) >>> 0;\n\n/** IEEE 754 half -> number. */\nexport function half(x: number): number {\n const s = x & 0x8000 ? -1 : 1, e = (x >> 10) & 31, f = x & 1023;\n if (e === 0) return s * 5.960464477539063e-8 * f;\n if (e === 31) return f ? NaN : s * Infinity;\n return s * 2 ** (e - 15) * (1 + f / 1024);\n}\n\n/** Copy of a Uint8Array's contents as a separate ArrayBuffer. */\nexport function toArrayBuffer(u8: Uint8Array): ArrayBuffer {\n return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;\n}\n\n/** First n bytes of a file (for format detection). */\nexport async function readHead(file: Blob | { arrayBuffer(): Promise<ArrayBuffer> }, n = 64): Promise<Uint8Array> {\n if (typeof Blob !== 'undefined' && file instanceof Blob) return new Uint8Array(await file.slice(0, n).arrayBuffer());\n const buf = await file.arrayBuffer();\n return new Uint8Array(buf, 0, Math.min(n, buf.byteLength));\n}\n\n/** Whether the first bytes match an ASCII signature. */\nexport function hasMagic(head: Uint8Array, magic: string, offset = 0): boolean {\n if (head.length < offset + magic.length) return false;\n for (let i = 0; i < magic.length; i++) if (head[offset + i] !== magic.charCodeAt(i)) return false;\n return true;\n}\n\nexport const u32le = (u8: Uint8Array, o: number): number =>\n u8.length >= o + 4 ? (u8[o]! | (u8[o + 1]! << 8) | (u8[o + 2]! << 16) | (u8[o + 3]! << 24)) >>> 0 : -1;\n\nexport const extOf = (name: string): string => {\n const i = name.lastIndexOf('.');\n return i < 0 ? '' : name.slice(i + 1).toLowerCase();\n};\nexport const normPath = (p: string): string => p.replace(/\\\\/g, '/').toLowerCase();\nexport const stripExt = (p: string): string => p.replace(/\\.[^./]+$/, '');\nexport const baseName = (p: string): string => p.replace(/\\\\/g, '/').split('/').pop() ?? p;\n\nconst decoder = new TextDecoder();\nexport const decodeText = (data: Uint8Array | ArrayBuffer): string => decoder.decode(data);\n"],"names":[],"mappings":"AAGO,SAAS,KAAK,IAAgB,OAAe,QAAwB;AAC1E,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,UAAU,GAAG,QAAQ,CAAC,GAAG,UAAU,OAAO,aAAa,GAAG,QAAQ,CAAC,CAAE;AACzF,SAAO;AACT;AAGO,SAAS,UAAU,IAAgB,GAAW,GAAmB;AACtE,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,KAAK,GAAG,CAAC,GAAG,IAAK,MAAK,OAAO,aAAa,GAAG,CAAC,CAAE;AACpE,SAAO;AACT;AAGO,SAAS,MAAM,IAAgB,GAAW,GAAmB;AAClE,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,GAAG,UAAU,OAAO,aAAa,GAAG,CAAC,CAAE;AAC3D,SAAO;AACT;AAGO,MAAM,SAAS,CAAC,OACpB,EAAE,WAAW,CAAC,IAAK,EAAE,WAAW,CAAC,KAAK,IAAM,EAAE,WAAW,CAAC,KAAK,KAAO,EAAE,WAAW,CAAC,KAAK,QAAS;AAG9F,SAAS,KAAK,GAAmB;AACtC,QAAM,IAAI,IAAI,QAAS,KAAK,GAAG,IAAK,KAAK,KAAM,IAAI,IAAI,IAAI;AAC3D,MAAI,MAAM,EAAG,QAAO,IAAI,uBAAuB;AAC/C,MAAI,MAAM,GAAI,QAAO,IAAI,MAAM,IAAI;AACnC,SAAO,IAAI,MAAM,IAAI,OAAO,IAAI,IAAI;AACtC;AAGO,SAAS,cAAc,IAA6B;AACzD,SAAO,GAAG,OAAO,MAAM,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU;AACrE;AAGA,eAAsB,SAAS,MAAsD,IAAI,IAAyB;AAChH,MAAI,OAAO,SAAS,eAAe,gBAAgB,KAAM,QAAO,IAAI,WAAW,MAAM,KAAK,MAAM,GAAG,CAAC,EAAE,aAAa;AACnH,QAAM,MAAM,MAAM,KAAK,YAAA;AACvB,SAAO,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI,GAAG,IAAI,UAAU,CAAC;AAC3D;AAGO,SAAS,SAAS,MAAkB,OAAe,SAAS,GAAY;AAC7E,MAAI,KAAK,SAAS,SAAS,MAAM,OAAQ,QAAO;AAChD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,KAAI,KAAK,SAAS,CAAC,MAAM,MAAM,WAAW,CAAC,EAAG,QAAO;AAC5F,SAAO;AACT;AAEO,MAAM,QAAQ,CAAC,IAAgB,MACpC,GAAG,UAAU,IAAI,KAAK,GAAG,CAAC,IAAM,GAAG,IAAI,CAAC,KAAM,IAAM,GAAG,IAAI,CAAC,KAAM,KAAO,GAAG,IAAI,CAAC,KAAM,QAAS,IAAI;AAE/F,MAAM,QAAQ,CAAC,SAAyB;AAC7C,QAAM,IAAI,KAAK,YAAY,GAAG;AAC9B,SAAO,IAAI,IAAI,KAAK,KAAK,MAAM,IAAI,CAAC,EAAE,YAAA;AACxC;AACO,MAAM,WAAW,CAAC,MAAsB,EAAE,QAAQ,OAAO,GAAG,EAAE,YAAA;AAC9D,MAAM,WAAW,CAAC,MAAsB,EAAE,QAAQ,aAAa,EAAE;AACjE,MAAM,WAAW,CAAC,MAAsB,EAAE,QAAQ,OAAO,GAAG,EAAE,MAAM,GAAG,EAAE,IAAA,KAAS;AAEzF,MAAM,UAAU,IAAI,YAAA;AACb,MAAM,aAAa,CAAC,SAA2C,QAAQ,OAAO,IAAI;"}
@@ -0,0 +1,2 @@
1
+ import type { EngineTexts } from './plugin';
2
+ export declare const EN: EngineTexts;
@@ -0,0 +1,24 @@
1
+ const EN = {
2
+ "title": "Common formats",
3
+ "exporters": {
4
+ "obj": {
5
+ "title": "OBJ + MTL (Blender, 3ds Max, any 3D editor)",
6
+ "description": "Bind-pose geometry, UVs, normals and materials with DDS textures — one ZIP. OBJ does not store bones or animations.",
7
+ "fields": {
8
+ "name": {
9
+ "label": "File name"
10
+ },
11
+ "scale": {
12
+ "label": "Scale"
13
+ }
14
+ }
15
+ }
16
+ },
17
+ "groups": {
18
+ "Игры": "Games"
19
+ }
20
+ };
21
+ export {
22
+ EN
23
+ };
24
+ //# sourceMappingURL=builtin-en.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin-en.js","sources":["../src/builtin-en.ts"],"sourcesContent":["// English texts of the core engine (Russian ones live in the plugins themselves).\nimport type { EngineTexts } from './plugin';\n\nexport const EN: EngineTexts = {\n \"title\": \"Common formats\",\n \"exporters\": {\n \"obj\": {\n \"title\": \"OBJ + MTL (Blender, 3ds Max, any 3D editor)\",\n \"description\": \"Bind-pose geometry, UVs, normals and materials with DDS textures — one ZIP. OBJ does not store bones or animations.\",\n \"fields\": {\n \"name\": {\n \"label\": \"File name\"\n },\n \"scale\": {\n \"label\": \"Scale\"\n }\n }\n }\n },\n \"groups\": {\n \"Игры\": \"Games\"\n }\n};\n"],"names":[],"mappings":"AAGO,MAAM,KAAkB;AAAA,EAC7B,SAAS;AAAA,EACT,aAAa;AAAA,IACX,OAAO;AAAA,MACL,SAAS;AAAA,MACT,eAAe;AAAA,MACf,UAAU;AAAA,QACR,QAAQ;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,QAEX,SAAS;AAAA,UACP,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEF,UAAU;AAAA,IACR,QAAQ;AAAA,EAAA;AAEZ;"}
@@ -0,0 +1 @@
1
+ export declare const coreEngine: import("./plugin").EnginePlugin;
@@ -0,0 +1,46 @@
1
+ import { hasMagic } from "./binary.js";
2
+ import { openZip } from "./archives/zip.js";
3
+ import { decodeDDS } from "./images/dds.js";
4
+ import { decodePCX } from "./images/pcx.js";
5
+ import { decodeTGA } from "./images/tga.js";
6
+ import { defineEngine } from "./plugin.js";
7
+ import { EN } from "./builtin-en.js";
8
+ const coreEngine = defineEngine({
9
+ id: "core",
10
+ title: "Общие форматы",
11
+ i18n: { en: EN },
12
+ archives: [
13
+ {
14
+ id: "zip",
15
+ title: "ZIP (pk3, pk4, iwd, CryEngine .pak)",
16
+ engine: "core",
17
+ extensions: ["zip", "pk3", "pk4", "iwd", "pak"],
18
+ sniff: (h) => hasMagic(h, "PK") || hasMagic(h, "PK") ? 1 : 0,
19
+ // CryEngine .pak files (Far Cry, Crysis, KCD) open by themselves in the "Game folder": only the ZIP central directory is read
20
+ openFromFolder: (_h, name) => /\.pak$/i.test(name),
21
+ open: (file) => openZip(file)
22
+ }
23
+ ],
24
+ exporters: [
25
+ {
26
+ id: "obj",
27
+ title: "OBJ + MTL (Blender, 3ds Max, любой 3D-редактор)",
28
+ target: "obj",
29
+ description: "Геометрия в bind-позе, UV, нормали и материалы с текстурами DDS — одним ZIP. Кости и анимации OBJ не хранит.",
30
+ fields: [
31
+ { id: "name", label: "Имя файла", type: "text", default: (_m, base) => base },
32
+ { id: "scale", label: "Масштаб", type: "number", min: 0, step: 0.01, default: 1 }
33
+ ],
34
+ run: async (model, values, ctx) => (await import("./writers/obj.js")).exportObj(model, values, ctx)
35
+ }
36
+ ],
37
+ codecs: [
38
+ { id: "dds", extensions: ["dds"], decode: (b) => decodeDDS(b) },
39
+ { id: "tga", extensions: ["tga"], decode: (b) => decodeTGA(b) },
40
+ { id: "pcx", extensions: ["pcx"], decode: (b) => decodePCX(b) }
41
+ ]
42
+ });
43
+ export {
44
+ coreEngine
45
+ };
46
+ //# sourceMappingURL=builtin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.js","sources":["../src/builtin.ts"],"sourcesContent":["// The core's built-in \"engine\": shared archives (ZIP) and codecs (DDS, TGA, PCX) used by many games.\nimport { hasMagic } from './binary';\nimport { openZip } from './archives/zip';\nimport { decodeDDS } from './images/dds';\nimport { decodePCX } from './images/pcx';\nimport { decodeTGA } from './images/tga';\nimport { defineEngine } from './plugin';\nimport { EN } from './builtin-en';\n\nexport const coreEngine = defineEngine({\n id: 'core',\n title: 'Общие форматы',\n i18n: { en: EN },\n archives: [\n {\n id: 'zip',\n title: 'ZIP (pk3, pk4, iwd, CryEngine .pak)',\n engine: 'core',\n extensions: ['zip', 'pk3', 'pk4', 'iwd', 'pak'],\n sniff: (h) => (hasMagic(h, 'PK\\x03\\x04') || hasMagic(h, 'PK\\x05\\x06') ? 1 : 0),\n // CryEngine .pak files (Far Cry, Crysis, KCD) open by themselves in the \"Game folder\": only the ZIP central directory is read\n openFromFolder: (_h, name) => /\\.pak$/i.test(name),\n open: (file) => openZip(file),\n },\n ],\n exporters: [\n {\n id: 'obj',\n title: 'OBJ + MTL (Blender, 3ds Max, любой 3D-редактор)',\n target: 'obj',\n description: 'Геометрия в bind-позе, UV, нормали и материалы с текстурами DDS — одним ZIP. Кости и анимации OBJ не хранит.',\n fields: [\n { id: 'name', label: 'Имя файла', type: 'text', default: (_m, base) => base },\n { id: 'scale', label: 'Масштаб', type: 'number', min: 0, step: 0.01, default: 1 },\n ],\n run: async (model, values, ctx) => (await import('./writers/obj')).exportObj(model, values, ctx),\n },\n ],\n codecs: [\n { id: 'dds', extensions: ['dds'], decode: (b) => decodeDDS(b) },\n { id: 'tga', extensions: ['tga'], decode: (b) => decodeTGA(b) },\n { id: 'pcx', extensions: ['pcx'], decode: (b) => decodePCX(b) },\n ],\n});\n"],"names":[],"mappings":";;;;;;;AASO,MAAM,aAAa,aAAa;AAAA,EACrC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,MAAM,EAAE,IAAI,GAAA;AAAA,EACZ,UAAU;AAAA,IACR;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,YAAY,CAAC,OAAO,OAAO,OAAO,OAAO,KAAK;AAAA,MAC9C,OAAO,CAAC,MAAO,SAAS,GAAG,MAAY,KAAK,SAAS,GAAG,MAAY,IAAI,IAAI;AAAA;AAAA,MAE5E,gBAAgB,CAAC,IAAI,SAAS,UAAU,KAAK,IAAI;AAAA,MACjD,MAAM,CAAC,SAAS,QAAQ,IAAI;AAAA,IAAA;AAAA,EAC9B;AAAA,EAEF,WAAW;AAAA,IACT;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,QAAQ;AAAA,QACN,EAAE,IAAI,QAAQ,OAAO,aAAa,MAAM,QAAQ,SAAS,CAAC,IAAI,SAAS,KAAA;AAAA,QACvE,EAAE,IAAI,SAAS,OAAO,WAAW,MAAM,UAAU,KAAK,GAAG,MAAM,MAAM,SAAS,EAAA;AAAA,MAAE;AAAA,MAElF,KAAK,OAAO,OAAO,QAAQ,SAAS,MAAM,OAAO,kBAAe,GAAG,UAAU,OAAO,QAAQ,GAAG;AAAA,IAAA;AAAA,EACjG;AAAA,EAEF,QAAQ;AAAA,IACN,EAAE,IAAI,OAAO,YAAY,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,UAAU,CAAC,EAAA;AAAA,IAC5D,EAAE,IAAI,OAAO,YAAY,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,UAAU,CAAC,EAAA;AAAA,IAC5D,EAAE,IAAI,OAAO,YAAY,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,UAAU,CAAC,EAAA;AAAA,EAAE;AAElE,CAAC;"}
@@ -0,0 +1,4 @@
1
+ /** zlib stream (with a 78 xx header). */
2
+ export declare const inflateZlib: (u8: Uint8Array) => Promise<Uint8Array>;
3
+ /** Raw deflate (ZIP, method 8). */
4
+ export declare const inflateRaw: (u8: Uint8Array) => Promise<Uint8Array>;
@@ -0,0 +1,11 @@
1
+ async function run(u8, format) {
2
+ const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream(format));
3
+ return new Uint8Array(await new Response(stream).arrayBuffer());
4
+ }
5
+ const inflateZlib = (u8) => run(u8, "deflate");
6
+ const inflateRaw = (u8) => run(u8, "deflate-raw");
7
+ export {
8
+ inflateRaw,
9
+ inflateZlib
10
+ };
11
+ //# sourceMappingURL=zlib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zlib.js","sources":["../../src/codecs/zlib.ts"],"sourcesContent":["// zlib / raw deflate decompression via the standard DecompressionStream (browsers, Node 18+).\nasync function run(u8: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {\n const stream = new Blob([u8 as BlobPart]).stream().pipeThrough(new DecompressionStream(format));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n/** zlib stream (with a 78 xx header). */\nexport const inflateZlib = (u8: Uint8Array): Promise<Uint8Array> => run(u8, 'deflate');\n/** Raw deflate (ZIP, method 8). */\nexport const inflateRaw = (u8: Uint8Array): Promise<Uint8Array> => run(u8, 'deflate-raw');\n"],"names":[],"mappings":"AACA,eAAe,IAAI,IAAgB,QAAwD;AACzF,QAAM,SAAS,IAAI,KAAK,CAAC,EAAc,CAAC,EAAE,OAAA,EAAS,YAAY,IAAI,oBAAoB,MAAM,CAAC;AAC9F,SAAO,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,EAAE,aAAa;AAChE;AAEO,MAAM,cAAc,CAAC,OAAwC,IAAI,IAAI,SAAS;AAE9E,MAAM,aAAa,CAAC,OAAwC,IAAI,IAAI,aAAa;"}
@@ -0,0 +1,10 @@
1
+ import type { BlobFile, VFile } from './types';
2
+ export declare function hexDump(u8: Uint8Array, width?: number): string;
3
+ export interface DiagnosticsInput {
4
+ file: VFile | BlobFile;
5
+ format?: string;
6
+ preset?: string;
7
+ error: string;
8
+ appVersion?: string;
9
+ }
10
+ export declare function buildDiagnostics(d: DiagnosticsInput, headBytes?: number): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { readHead } from "./binary.js";
2
+ function hexDump(u8, width = 16) {
3
+ const lines = [];
4
+ for (let o = 0; o < u8.length; o += width) {
5
+ const row = u8.subarray(o, o + width);
6
+ const hex = Array.from(row, (b) => b.toString(16).padStart(2, "0")).join(" ").padEnd(width * 3 - 1);
7
+ const txt = Array.from(row, (b) => b >= 32 && b < 127 ? String.fromCharCode(b) : ".").join("");
8
+ lines.push(`${o.toString(16).padStart(4, "0")} ${hex} ${txt}`);
9
+ }
10
+ return lines.join("\n");
11
+ }
12
+ async function buildDiagnostics(d, headBytes = 64) {
13
+ let head = new Uint8Array(0);
14
+ try {
15
+ head = await readHead(d.file, headBytes);
16
+ } catch {
17
+ }
18
+ const size = typeof Blob !== "undefined" && d.file instanceof Blob ? d.file.size : void 0;
19
+ return [
20
+ `gmv ${d.appVersion ?? ""}`.trim(),
21
+ `Файл: ${"path" in d.file && d.file.path || d.file.name}${size !== void 0 ? ` (${size} байт)` : ""}`,
22
+ d.preset ? `Пресет: ${d.preset}` : "",
23
+ d.format ? `Формат: ${d.format}` : "",
24
+ `Ошибка: ${d.error}`,
25
+ `Сигнатура: ${Array.from(head.subarray(0, 4), (b) => b >= 32 && b < 127 ? String.fromCharCode(b) : ".").join("")}`,
26
+ `Заголовок (${head.length} байт):`,
27
+ hexDump(head)
28
+ ].filter(Boolean).join("\n");
29
+ }
30
+ export {
31
+ buildDiagnostics,
32
+ hexDump
33
+ };
34
+ //# sourceMappingURL=diagnostics.js.map