@gmvjs/core 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -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,101 @@
1
- # Temporary Holding Version
1
+ # @gmvjs/core — ядро Game Model Viewer
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
+ Ядро библиотеки чтения игровых моделей. Здесь нет ни одного игрового формата: только API плагинов, общие типы модели, реестр с автоопределением формата, сессия просмотра, архивы, текстуры и вспомогательная математика. Игры подключаются пакетами движков (`@gmvjs/xray-v1`, `@gmvjs/source1`…), рендер — пакетом `@gmvjs/renderer`.
4
+
5
+ Работает в браузере, в Web Worker (без DOM) и в Node ≥ 20. Только ESM, `sideEffects: false`.
6
+
7
+ ## Установка
8
+
9
+ ```sh
10
+ npm i @gmvjs/core
11
+ ```
12
+
13
+ ## Быстрый старт
14
+
15
+ ```ts
16
+ import { createRegistry, ViewerSession } from '@gmvjs/core';
17
+ import { gamebryoEngine } from '@gmvjs/gamebryo';
18
+
19
+ const session = new ViewerSession(createRegistry([gamebryoEngine]), 'skyrimse');
20
+ session.on('status', ({ text, error }) => console[error ? 'error' : 'log'](text));
21
+ session.on('model', async ({ model }) => {
22
+ for (let i = 0; i < model.meshes.length; i++) {
23
+ const t = await session.resolveTexture(i); // { image, label, ok }
24
+ }
25
+ });
26
+
27
+ await session.addArchives([meshesBsa, texturesBsa]);
28
+ await session.openEntry(session.archiveModels('chair').items[0]);
29
+ ```
30
+
31
+ Только разбор файла, без сессии (Node, Worker):
32
+
33
+ ```ts
34
+ import { createRegistry } from '@gmvjs/core';
35
+ import { idtech3Engine } from '@gmvjs/idtech3';
36
+
37
+ const reg = createRegistry([idtech3Engine]);
38
+ const det = await reg.detect(file); // { format, confidence, candidates }
39
+ const model = await det!.format.load(file, { files: [file], archives: [], sibling: async () => null, readPath: async () => null });
40
+ ```
41
+
42
+ ## Свой движок
43
+
44
+ ```ts
45
+ import { defineEngine, createRegistry, binary, type Model } from '@gmvjs/core';
46
+
47
+ export const myEngine = defineEngine({
48
+ id: 'my',
49
+ title: 'My Engine',
50
+ formats: [{
51
+ id: 'my-mesh', title: 'My mesh', engine: 'my', extensions: ['mym'], status: 'beta',
52
+ sniff: (head) => (binary.hasMagic(head, 'MYM1') ? 1 : 0),
53
+ load: async (main) => (await import('./parser')).parse(await main.arrayBuffer()) as Model,
54
+ }],
55
+ games: [{ id: 'my-game', group: 'Игры', title: 'My Game', engine: 'my', formats: ['my-mesh'] }],
56
+ i18n: { en: { title: 'My Engine', presets: { 'my-game': { title: 'My Game' } } } },
57
+ });
58
+ ```
59
+
60
+ Пакет движка по соглашению экспортирует `<id>Engine` из корня и по подпути на каждую игру: `@gmvjs/<движок>/<игра>` → `{ preset, engine }`, где `engine` — тот же движок, ограниченный пресетом этой игры (`pickGames(engine, id)`).
61
+
62
+ ## Что внутри
63
+
64
+ | Модуль | Назначение |
65
+ |---|---|
66
+ | `types` | `Model`, `Mesh`, `Motion` (`NodeMotion`, `VertexMotion`, расширяемые движками), `SkeletonRuntime`, `Lightmap`, `ScreenFx`, `InstancedScene`, `Archive`, `VFile` |
67
+ | `plugin` | `defineEngine`, `EnginePlugin`, `FormatPlugin`, `ArchivePlugin`, `ImageCodec`, `TextureDictionaryPlugin`, `MaterialResolver`, `ExporterPlugin`, `GamePreset`; новое — `pickGames()` |
68
+ | `registry` | `Registry`, `createRegistry()`: сбор плагинов, автоопределение по расширению, пути и первым байтам, пресет по формату и маркерам папки |
69
+ | `session` | `ViewerSession`: файлы, «Папка игры», архивы, загрузка модели, текстуры, нормали, PBR, внешние анимации, события `status` / `model` / `preset`; `loadSameDirMotions` |
70
+ | `textures` | `TextureLibrary`: поиск текстур по имени во всех открытых архивах, декодирование через кодеки движков |
71
+ | `normals`, `surface` | касательные, распознавание упаковки карт нормалей (RGB / RG / AG), сведение PBR-карт |
72
+ | `skeleton` | `NodeSkeleton` — поза для иерархий узлов: ключи, слои, аддитивные дорожки, смешивание по сетке |
73
+ | `scene`, `levelbuilder` | дерево сцены, выбор лучом, перемещение экземпляров; сборка уровня из экземпляров по материалам |
74
+ | `archives` | `createArchive`, папка (`openFolder`), ZIP (`openZip`; PK3/PK4/IWD/PAK) |
75
+ | `codecs` | zlib / raw deflate (`DecompressionStream`) |
76
+ | `images` | DDS (DXT1–5, несжатые), TGA, PCX; декодеры BC4/BC5 (`decodeBC45`) и BC7 (`decodeBC7`) для движков |
77
+ | `writers` | ZIP, DDS (RGBA), OBJ + MTL (выгрузка `obj`) |
78
+ | `i18n` | `setLocale('ru' / 'en')`, `tr()`, `presetText()`, `engineTitle()`, `exporterText()`: английские тексты пресетов берутся из `EnginePlugin.i18n` |
79
+ | `diagnostics` | `buildDiagnostics`, `hexDump` — отчёт о неудачной загрузке |
80
+ | `binary`, `math`, `geometry` | чтение двоичных данных, матрицы и кватернионы, рамки и нормали |
81
+ | `builtin` | `coreEngine`: архив ZIP, кодеки DDS/TGA/PCX, выгрузка OBJ — подключается `createRegistry` всегда |
82
+
83
+ ## Особенности API
84
+
85
+ - **`EnginePlugin.i18n`** — `{ en?: { title?, groups?, presets?: Record<id, { title?, hint?, archiveLabel?, motionLabel? }>, exporters?: Record<id, { title?, description?, fields? }> } }`. Реестр подключает тексты вместе с движком; `setLocale('en')`, затем `presetText` / `engineTitle` / `exporterText` отдают английские тексты.
86
+ - **`MotionKinds`** — виды анимаций по полю `kind` (в ядре `node` и `vertex`). Движок добавляет свой вид расширением интерфейса, и сужение по `m.kind` работает в коде приложения:
87
+
88
+ ```ts
89
+ declare module '@gmvjs/core/types' { interface MotionKinds { xray: XrayMotion } }
90
+ ```
91
+ - **`pickGames(engine, ...ids)`** — движок только с выбранными пресетами и форматами, на которые они ссылаются (архивы, кодеки и выгрузка — все). На нём построены подпути `@gmvjs/<движок>/<игра>`. Два среза одного движка в одном реестре объединяются.
92
+ - **`Registry.presetAvailable(preset)`** — есть ли формат пресета в реестре. Пресет-мост с форматом другого пакета (`@gmvjs/source2/gltf`, `@gmvjs/cryengine3/dae` → `@gmvjs/assimp`) без него недоступен, но реестр не ломается.
93
+ - **Подпути модулей** `@gmvjs/core/<модуль>` (`binary`, `geometry`, `math`, `levelbuilder`, `images/dds`, `archives/common`…) — для пакетов движков.
94
+
95
+ ## Зависимости
96
+
97
+ Нет.
98
+
99
+ ## Лицензия
100
+
101
+ MIT
@@ -0,0 +1,28 @@
1
+ import type { Archive, ArchiveEntry, BlobFile } from '../types';
2
+ /** Расширения, по которым запись попадает в индекс текстур (путь без расширения). */
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
+ /** Своя функция поиска (например, по суффиксу пути). */
11
+ lookup?(path: string): E | null;
12
+ byBase?: Map<string, E>;
13
+ /** Дополнительные ключи текстур (по умолчанию — полный путь без расширения). */
14
+ textureKeys?(entry: E): string[];
15
+ /** Имя файла для показа (по умолчанию — последний сегмент пути). */
16
+ displayName?(entry: E): string;
17
+ /** При повторе ключа текстуры оставлять первую запись (по умолчанию — последнюю, как в архивах игр). */
18
+ keepFirstTextureKey?: boolean;
19
+ /** Своё содержимое файла (без копии через read). */
20
+ fileBytes?(entry: E): Promise<ArrayBuffer>;
21
+ /** Исходный файл записи (папка). */
22
+ blobOf?(entry: E): BlobFile | null;
23
+ /** Текстура по точному пути, которой нет в списке записей. */
24
+ findTexture?(path: string): E | null;
25
+ }
26
+ export declare function createArchive<E extends ArchiveEntry>(spec: ArchiveSpec<E>): Archive;
27
+ /** Прочитать диапазон байт файла. */
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":["// Общая обвязка архива: индексы путей и текстур, «файл-подобные» объекты записей.\nimport { baseName, stripExt, toArrayBuffer } from '../binary';\nimport type { Archive, ArchiveEntry, BlobFile, VFile } from '../types';\n\n/** Расширения, по которым запись попадает в индекс текстур (путь без расширения). */\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 /** Своя функция поиска (например, по суффиксу пути). */\n lookup?(path: string): E | null;\n byBase?: Map<string, E>;\n /** Дополнительные ключи текстур (по умолчанию — полный путь без расширения). */\n textureKeys?(entry: E): string[];\n /** Имя файла для показа (по умолчанию — последний сегмент пути). */\n displayName?(entry: E): string;\n /** При повторе ключа текстуры оставлять первую запись (по умолчанию — последнюю, как в архивах игр). */\n keepFirstTextureKey?: boolean;\n /** Своё содержимое файла (без копии через read). */\n fileBytes?(entry: E): Promise<ArrayBuffer>;\n /** Исходный файл записи (папка). */\n blobOf?(entry: E): BlobFile | null;\n /** Текстура по точному пути, которой нет в списке записей. */\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); // повторный путь (моды, дописанные архивы): побеждает последняя запись\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/** Прочитать диапазон байт файла. */\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
+ /** То же, но кусками по chunk файлов с передачей управления браузеру (большие папки игр — сотни тысяч файлов). */
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
+ // ключи по всем суффиксам пути: a/b/c, b/c, c — без привязки к корню папки
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":["// «Папка игры»: loose-файлы выбранной папки как виртуальный архив (поиск по суффиксу пути, без чтения содержимого).\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/** Индекс папки: пути, полные и по суффиксам (собирается кусками — см. 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 // ключи по всем суффиксам пути: a/b/c, b/c, c — без привязки к корню папки\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/** То же, но кусками по chunk файлов с передачей управления браузеру (большие папки игр — сотни тысяч файлов). */\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 (pk3/pk4 id Tech, .pak CryEngine, .zip): читается центральный каталог, данные — по требованию (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-строка до нуля или до n байт. */
2
+ export declare function cstr(u8: Uint8Array, start: number, maxLen: number): string;
3
+ /** ASCII-строка в диапазоне [a, b) до первого нуля. */
4
+ export declare function cstrRange(u8: Uint8Array, a: number, b: number): string;
5
+ /** ASCII в диапазоне [a, b) без остановки на нуле. */
6
+ export declare function ascii(u8: Uint8Array, a: number, b: number): string;
7
+ /** Четыре символа как 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
+ /** Копия содержимого Uint8Array как отдельный ArrayBuffer. */
12
+ export declare function toArrayBuffer(u8: Uint8Array): ArrayBuffer;
13
+ /** Первые n байт файла (для автоопределения). */
14
+ export declare function readHead(file: Blob | {
15
+ arrayBuffer(): Promise<ArrayBuffer>;
16
+ }, n?: number): Promise<Uint8Array>;
17
+ /** Совпадают ли первые байты с ASCII-сигнатурой. */
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":["// Мелкие помощники чтения двоичных данных.\n\n/** ASCII-строка до нуля или до n байт. */\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-строка в диапазоне [a, b) до первого нуля. */\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 в диапазоне [a, b) без остановки на нуле. */\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/** Четыре символа как 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/** Копия содержимого Uint8Array как отдельный ArrayBuffer. */\nexport function toArrayBuffer(u8: Uint8Array): ArrayBuffer {\n return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;\n}\n\n/** Первые n байт файла (для автоопределения). */\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/** Совпадают ли первые байты с ASCII-сигнатурой. */\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":["// Английские тексты движка core (русские — в самих плагинах).\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
+ // .pak CryEngine (Far Cry, Crysis, KCD) в «Папке игры» открываются сами: читается только центральный каталог ZIP
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":["// Встроенный «движок» ядра: общие архивы (ZIP) и кодеки (DDS, TGA, PCX), которыми пользуются многие игры.\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 // .pak CryEngine (Far Cry, Crysis, KCD) в «Папке игры» открываются сами: читается только центральный каталог ZIP\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-поток (с заголовком 78 xx). */
2
+ export declare const inflateZlib: (u8: Uint8Array) => Promise<Uint8Array>;
3
+ /** Сырой deflate (ZIP, метод 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 через стандартный DecompressionStream (браузеры, 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-поток (с заголовком 78 xx). */\nexport const inflateZlib = (u8: Uint8Array): Promise<Uint8Array> => run(u8, 'deflate');\n/** Сырой deflate (ZIP, метод 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