universal-game-modder 0.1.4

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 (92) hide show
  1. package/ACCEPTABLE_USE.md +49 -0
  2. package/CHANGELOG.md +119 -0
  3. package/LICENSE +21 -0
  4. package/LICENSE-EULA.md +62 -0
  5. package/README.md +162 -0
  6. package/VERIFICATION.md +132 -0
  7. package/dist/backends/child-pool.js +91 -0
  8. package/dist/backends/mcp-child.js +102 -0
  9. package/dist/backends/mcp-child.js.map +1 -0
  10. package/dist/core/config.d.ts +10 -0
  11. package/dist/core/config.js +48 -0
  12. package/dist/core/config.js.map +1 -0
  13. package/dist/core/engine-detector.d.ts +13 -0
  14. package/dist/core/engine-detector.js +247 -0
  15. package/dist/core/engine-detector.js.map +1 -0
  16. package/dist/core/event-bus.js +18 -0
  17. package/dist/core/file-classifier.js +394 -0
  18. package/dist/core/model-db.js +517 -0
  19. package/dist/core/model-db.js.map +1 -0
  20. package/dist/core/router.d.ts +6 -0
  21. package/dist/core/router.js +143 -0
  22. package/dist/core/router.js.map +1 -0
  23. package/dist/core/session.js +114 -0
  24. package/dist/decoders/audio-export.js +57 -0
  25. package/dist/decoders/mesh-export.d.ts +20 -0
  26. package/dist/decoders/mesh-export.js +174 -0
  27. package/dist/decoders/mesh-export.js.map +1 -0
  28. package/dist/decoders/native/disassembler.js +85 -0
  29. package/dist/decoders/native/disassembler.js.map +1 -0
  30. package/dist/decoders/png-encode.d.ts +17 -0
  31. package/dist/decoders/png-encode.js +33 -0
  32. package/dist/decoders/texture-decode.js +278 -0
  33. package/dist/decoders/texture-decode.js.map +1 -0
  34. package/dist/decoders/unity/audioclip.d.ts +55 -0
  35. package/dist/decoders/unity/audioclip.js +87 -0
  36. package/dist/decoders/unity/audioclip.js.map +1 -0
  37. package/dist/decoders/unity/binary-reader.d.ts +45 -0
  38. package/dist/decoders/unity/binary-reader.js +141 -0
  39. package/dist/decoders/unity/binary-reader.js.map +1 -0
  40. package/dist/decoders/unity/mesh.js +198 -0
  41. package/dist/decoders/unity/mesh.js.map +1 -0
  42. package/dist/decoders/unity/ress-reader.d.ts +33 -0
  43. package/dist/decoders/unity/ress-reader.js +82 -0
  44. package/dist/decoders/unity/ress-reader.js.map +1 -0
  45. package/dist/decoders/unity/serialized-file.d.ts +65 -0
  46. package/dist/decoders/unity/serialized-file.js +144 -0
  47. package/dist/decoders/unity/texture2d.js +185 -0
  48. package/dist/decoders/unity/vertex-data.d.ts +54 -0
  49. package/dist/decoders/unity/vertex-data.js +174 -0
  50. package/dist/decoders/unity/vertex-data.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.js +23 -0
  53. package/dist/server.d.ts +1 -0
  54. package/dist/server.js +66 -0
  55. package/dist/tools/decode-tools.d.ts +29 -0
  56. package/dist/tools/decode-tools.js +441 -0
  57. package/dist/tools/delegates/jar-editor.d.ts +2 -0
  58. package/dist/tools/delegates/jar-editor.js +276 -0
  59. package/dist/tools/delegates/unity-decompiler.d.ts +2 -0
  60. package/dist/tools/delegates/unity-decompiler.js +291 -0
  61. package/dist/tools/delegates/unreal-assets.d.ts +2 -0
  62. package/dist/tools/delegates/unreal-assets.js +122 -0
  63. package/dist/tools/detection-tools.d.ts +2 -0
  64. package/dist/tools/detection-tools.js +164 -0
  65. package/dist/tools/detection-tools.js.map +1 -0
  66. package/dist/tools/index.d.ts +8 -0
  67. package/dist/tools/index.js +30 -0
  68. package/dist/tools/index.js.map +1 -0
  69. package/dist/tools/native/index.d.ts +2 -0
  70. package/dist/tools/native/index.js +1201 -0
  71. package/dist/tools/native-comprehension-tools.js +298 -0
  72. package/dist/tools/native-comprehension-tools.js.map +1 -0
  73. package/dist/tools/unpack-tools.d.ts +2 -0
  74. package/dist/tools/unpack-tools.js +382 -0
  75. package/dist/tools/workflow-tools.d.ts +2 -0
  76. package/dist/tools/workflow-tools.js +434 -0
  77. package/dist/utils/logger.d.ts +11 -0
  78. package/dist/utils/logger.js +49 -0
  79. package/dist/utils/logger.js.map +1 -0
  80. package/dist/utils/path-utils.d.ts +15 -0
  81. package/dist/utils/path-utils.js +24 -0
  82. package/dist/utils/path-utils.js.map +1 -0
  83. package/dist/utils/steam-finder.d.ts +8 -0
  84. package/dist/utils/steam-finder.js +32 -0
  85. package/dist/web/web-server.d.ts +1 -0
  86. package/dist/web/web-server.js +369 -0
  87. package/licenses/THIRD-PARTY-NOTICES.md +35 -0
  88. package/llms-install.md +34 -0
  89. package/package.json +69 -0
  90. package/server.json +20 -0
  91. package/setup.ps1 +34 -0
  92. package/ugm.config.json +9 -0
@@ -0,0 +1,382 @@
1
+ import { createHash } from 'crypto';
2
+ import { readdirSync, readFileSync, realpathSync, statSync, mkdirSync, existsSync } from 'fs';
3
+ import { basename, dirname, join } from 'path';
4
+ import { detectEngine } from '../core/engine-detector.js';
5
+ import { AutopsyModel } from '../core/model-db.js';
6
+ import { classifyFile } from '../core/file-classifier.js';
7
+ import { extractGameName } from '../utils/path-utils.js';
8
+ const SHA256_OF_UNREADABLE = ''; // empty hash sentinel; the note carries the real reason
9
+ export function getUnpackTools() {
10
+ return [
11
+ {
12
+ name: 'unpack_game',
13
+ description: 'CATALOG-FIRST universal unpacker. Recursively walks an ENTIRE game install, classifies and sha256-hashes EVERY file, and persists the catalog into a SQLite <game>.autopsy.db (game/binary/asset/data_store tables, provenance on every row). Read-only: never writes game files. Containers (.pak/.pck/.bundle/...) are catalogued as single rows, never expanded (per-format extraction is a later slice). Idempotent: re-running on the same game re-opens the DB and upserts, no duplicate rows.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ game_path: {
18
+ type: 'string',
19
+ description: 'Full path to the game install directory to catalog.',
20
+ },
21
+ db_path: {
22
+ type: 'string',
23
+ description: 'Optional output path for the .autopsy.db. Defaults to <game_path>/.autopsy/<name>.autopsy.db.',
24
+ },
25
+ },
26
+ required: ['game_path'],
27
+ },
28
+ handler: async (args) => {
29
+ const gamePath = args.game_path;
30
+ const explicitDbPath = args.db_path;
31
+ if (!gamePath || !existsSync(gamePath)) {
32
+ return JSON.stringify({ error: `game_path does not exist: ${gamePath ?? '(missing)'}` }, null, 2);
33
+ }
34
+ const gameName = extractGameName(gamePath);
35
+ // A4: engine detection result recorded on the game row.
36
+ const detection = detectEngine(gamePath);
37
+ // Resolve DB location (writes confined to .autopsy/ unless caller overrides).
38
+ const dbPath = explicitDbPath ?? defaultDbPath(gamePath, gameName);
39
+ ensureDir(dirname(dbPath));
40
+ // A6: open existing DB if present; model-db upserts make this idempotent.
41
+ const model = new AutopsyModel(dbPath);
42
+ let gameId;
43
+ try {
44
+ gameId = model.upsertGame({
45
+ name: gameName,
46
+ install_path: gamePath,
47
+ engine: detection.engine,
48
+ runtime: detection.runtime,
49
+ arch: null, // arch is per-binary; left null at the header level
50
+ package_type: inferPackageType(gamePath),
51
+ autopsy_version: String(1), // V1 schema marker (model owns AUTOPSY_SCHEMA_VERSION)
52
+ provenance: detection.confidence, // detector self-reports verified|inferred|uncertain
53
+ source_tool: 'engine-detector',
54
+ });
55
+ const counts = {
56
+ binaries: 0,
57
+ assets: 0,
58
+ data_stores: 0,
59
+ total: 0,
60
+ skipped: 0,
61
+ errors: 0,
62
+ };
63
+ const visitedDirs = new Set();
64
+ walk(gamePath, gamePath, gameId, model, counts, visitedDirs);
65
+ const summary = {
66
+ game: gameName,
67
+ engine: detection.engine,
68
+ runtime: detection.runtime,
69
+ engine_confidence: detection.confidence,
70
+ db_path: dbPath,
71
+ counts: {
72
+ binaries: counts.binaries,
73
+ assets: counts.assets,
74
+ data_stores: counts.data_stores,
75
+ total: counts.total,
76
+ skipped: counts.skipped,
77
+ errors: counts.errors,
78
+ },
79
+ };
80
+ return JSON.stringify(summary, null, 2);
81
+ }
82
+ finally {
83
+ // Always release the DB handle, even if the walk threw.
84
+ model.close();
85
+ }
86
+ },
87
+ },
88
+ ];
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Recursive walk
92
+ // ---------------------------------------------------------------------------
93
+ /**
94
+ * Recursively walk `dir`, cataloguing every file. Containers are NOT descended
95
+ * into (they are files, catalogued as single rows). Symlink loops are broken
96
+ * via a visited-real-path set. No file is ever silently dropped.
97
+ */
98
+ function walk(dir, installRoot, gameId, model, counts, visitedDirs) {
99
+ // Symlink-loop guard: resolve the real path; if we've been here, stop.
100
+ let realDir;
101
+ try {
102
+ realDir = realpathSync(dir);
103
+ }
104
+ catch {
105
+ realDir = dir;
106
+ }
107
+ if (visitedDirs.has(realDir))
108
+ return;
109
+ visitedDirs.add(realDir);
110
+ let entries;
111
+ try {
112
+ entries = readdirSync(dir, { withFileTypes: true });
113
+ }
114
+ catch (err) {
115
+ // Unreadable directory (permissions, TrustedInstaller-locked, etc.) — record
116
+ // the directory itself as a data_store note so the gap is visible, not silent.
117
+ safeCatalogUnreadableDir(dir, gameId, model, counts, errString(err));
118
+ return;
119
+ }
120
+ for (const entry of entries) {
121
+ const fullPath = join(dir, entry.name);
122
+ // Do not follow symlinks as directories (loop & escape protection). A symlink
123
+ // is catalogued as a file-shaped note rather than traversed.
124
+ if (entry.isSymbolicLink()) {
125
+ catalogFile(fullPath, gameId, model, counts);
126
+ continue;
127
+ }
128
+ if (entry.isDirectory()) {
129
+ walk(fullPath, installRoot, gameId, model, counts, visitedDirs);
130
+ }
131
+ else if (entry.isFile()) {
132
+ catalogFile(fullPath, gameId, model, counts);
133
+ }
134
+ else {
135
+ // Block/char/fifo/socket — exotic, but still account for it (never drop).
136
+ catalogFile(fullPath, gameId, model, counts);
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Catalog a single file. Wrapped so one bad file can never abort the walk (A3/A7).
142
+ * The classifier decides the table; size+sha256 are direct-read facts (verified).
143
+ */
144
+ function catalogFile(absPath, gameId, model, counts) {
145
+ // 1) size (direct stat => verified fact). Failure is non-fatal.
146
+ let size = 0;
147
+ let sizeOk = true;
148
+ try {
149
+ size = statSync(absPath).size;
150
+ }
151
+ catch {
152
+ sizeOk = false;
153
+ }
154
+ // 2) classification (the classifier owns engine/format heuristics + provenance).
155
+ // A bad classifier call must not abort — fall back to a generic 'other' asset.
156
+ let cls;
157
+ try {
158
+ cls = classifyFile(absPath);
159
+ }
160
+ catch (err) {
161
+ cls = {
162
+ target: 'asset',
163
+ assetType: 'other',
164
+ provenance: 'uncertain',
165
+ note: `classifier failed: ${errString(err)}`,
166
+ };
167
+ counts.errors++;
168
+ }
169
+ // 3) sha256 (direct read => verified). Containers are hashed too (catalogued as
170
+ // one row), but never descended into. Unreadable/encrypted => empty hash + note,
171
+ // the row is STILL written (A7, KB 920f537e).
172
+ let sha256 = SHA256_OF_UNREADABLE;
173
+ let readNote = '';
174
+ if (cls.target !== 'skip') {
175
+ try {
176
+ sha256 = sha256OfFile(absPath);
177
+ }
178
+ catch (err) {
179
+ readNote = `unreadable (no hash): ${errString(err)}`;
180
+ counts.errors++;
181
+ }
182
+ }
183
+ if (!sizeOk) {
184
+ readNote = appendNote(readNote, 'size unavailable (stat failed)');
185
+ }
186
+ // 4) route into the schema. Each branch is itself guarded so a DB hiccup on one
187
+ // row cannot kill the walk.
188
+ try {
189
+ routeRow(absPath, gameId, size, sizeOk, sha256, cls, readNote, model, counts);
190
+ }
191
+ catch (err) {
192
+ // Last-resort safety net: never let a write error abort the catalog. Try to
193
+ // record it as a bare 'other' asset note so the file is not silently lost.
194
+ counts.errors++;
195
+ try {
196
+ model.addAsset({
197
+ game_id: gameId,
198
+ container_path: absPath, // the file on disk (NOT NULL natural-key part)
199
+ internal_path: '', // standalone file => empty internal path
200
+ type: 'other',
201
+ format: extOf(absPath),
202
+ decoded: false,
203
+ decoded_path: null,
204
+ size,
205
+ notes: appendNote(readNote, `catalog write failed, recorded as fallback: ${errString(err)}`),
206
+ provenance: 'uncertain',
207
+ source_tool: 'unpack_game',
208
+ });
209
+ counts.assets++;
210
+ counts.total++;
211
+ }
212
+ catch {
213
+ // If even the fallback write fails, we've already incremented errors; do not throw.
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Decide which table a classified file lands in and write it. The size/sha256
219
+ * facts are verified (direct reads); the routing provenance comes from the
220
+ * classifier (defaulting to 'inferred' for heuristic classification — A5).
221
+ */
222
+ function routeRow(absPath, gameId, size, sizeOk, sha256, cls, readNote, model, counts) {
223
+ // A5 provenance rule — the row's provenance reflects its strongest LOCATING fact:
224
+ // - 'verified' : we successfully size+sha256'd the file (direct byte-level read).
225
+ // The file demonstrably exists and we have proof of its contents.
226
+ // - 'uncertain' : the read failed (unreadable/encrypted/locked) OR the classifier
227
+ // itself flagged the routing uncertain (A7 garbage-managed case).
228
+ // - else : fall back to the classifier's own provenance (heuristic 'inferred').
229
+ // The *classification* (texture vs other, role guess) stays a heuristic and is
230
+ // expressed via type/role/notes — not by downgrading a verified file to 'inferred'.
231
+ const hashOk = sha256 !== SHA256_OF_UNREADABLE && sizeOk;
232
+ const classifierProv = cls.provenance ?? 'inferred';
233
+ const routingProv = classifierProv === 'uncertain' ? 'uncertain' : hashOk ? 'verified' : 'uncertain';
234
+ if (cls.target === 'skip') {
235
+ counts.skipped++;
236
+ return;
237
+ }
238
+ if (cls.target === 'binary') {
239
+ const managed = cls.managed === true;
240
+ // KB 920f537e: an unreadable/garbage managed binary (e.g. IL2CPP) is catalogued
241
+ // as managed+unreadable via the note, NOT thrown.
242
+ const notes = mergeNotes(cls.note, readNote);
243
+ model.addBinary({
244
+ game_id: gameId,
245
+ path: absPath,
246
+ kind: cls.kind ?? guessBinaryKind(absPath),
247
+ managed,
248
+ arch: cls.arch ?? null,
249
+ size,
250
+ sha256, // '' when the file could not be read — note explains why
251
+ role: cls.role ?? null,
252
+ notes,
253
+ // size/hash are verified reads; routing/role is the classifier's call.
254
+ provenance: routingProv,
255
+ source_tool: 'file-classifier',
256
+ });
257
+ counts.binaries++;
258
+ counts.total++;
259
+ return;
260
+ }
261
+ if (cls.target === 'data_store') {
262
+ const notes = mergeNotes(cls.note, readNote);
263
+ model.addDataStore({
264
+ game_id: gameId,
265
+ path: absPath,
266
+ kind: cls.storeKind ?? 'binary-blob',
267
+ scope: cls.scope ?? null,
268
+ schema_known: false, // schema discovery is a later slice; never assumed here
269
+ notes,
270
+ provenance: routingProv,
271
+ source_tool: 'file-classifier',
272
+ });
273
+ counts.data_stores++;
274
+ counts.total++;
275
+ return;
276
+ }
277
+ // Default + explicit 'asset'. A7: a container (.pak/.bundle/.pck) lands here as a
278
+ // single asset row (or as a data_store above, classifier's choice) — NEVER walked.
279
+ const baseNote = cls.isContainer
280
+ ? appendNote(cls.note ?? '', 'container catalogued as single row; not expanded (extraction deferred to P1.2)')
281
+ : cls.note ?? '';
282
+ // A3: an unknown type is NEVER dropped — it becomes type='other' WITH a reason.
283
+ const assetType = cls.assetType ?? 'other';
284
+ const notes = assetType === 'other' && !baseNote
285
+ ? mergeNotes('unrecognized file type — catalogued as other', readNote)
286
+ : mergeNotes(baseNote, readNote);
287
+ model.addAsset({
288
+ game_id: gameId,
289
+ container_path: absPath, // this IS the on-disk file (NOT NULL natural-key part)
290
+ internal_path: '', // standalone file => empty internal path; members come in P1.2
291
+ type: assetType,
292
+ format: cls.format ?? extOf(absPath),
293
+ decoded: false,
294
+ decoded_path: null,
295
+ size,
296
+ notes,
297
+ provenance: routingProv,
298
+ source_tool: 'file-classifier',
299
+ });
300
+ counts.assets++;
301
+ counts.total++;
302
+ }
303
+ /**
304
+ * Record an unreadable directory as a data_store note so a permission gap (e.g.
305
+ * TrustedInstaller-locked content) is visible in the catalog rather than silent.
306
+ */
307
+ function safeCatalogUnreadableDir(dir, gameId, model, counts, reason) {
308
+ try {
309
+ model.addDataStore({
310
+ game_id: gameId,
311
+ path: dir,
312
+ kind: 'binary-blob',
313
+ scope: null,
314
+ schema_known: false,
315
+ notes: `directory not enumerable: ${reason}`,
316
+ provenance: 'uncertain',
317
+ source_tool: 'unpack_game',
318
+ });
319
+ counts.data_stores++;
320
+ counts.total++;
321
+ }
322
+ catch {
323
+ // even this failed — count it, never throw.
324
+ }
325
+ counts.errors++;
326
+ }
327
+ // ---------------------------------------------------------------------------
328
+ // Helpers
329
+ // ---------------------------------------------------------------------------
330
+ function sha256OfFile(absPath) {
331
+ // Synchronous full read; for catalog purposes a whole-file hash is correct.
332
+ // (Streaming optimization can come later; correctness first.)
333
+ const buf = readFileSync(absPath);
334
+ return createHash('sha256').update(buf).digest('hex');
335
+ }
336
+ function defaultDbPath(gamePath, gameName) {
337
+ const safeName = gameName.replace(/[^A-Za-z0-9._-]+/g, '_') || 'game';
338
+ return join(gamePath, '.autopsy', `${safeName}.autopsy.db`);
339
+ }
340
+ function ensureDir(dir) {
341
+ if (!existsSync(dir)) {
342
+ mkdirSync(dir, { recursive: true });
343
+ }
344
+ }
345
+ function inferPackageType(gamePath) {
346
+ const lower = gamePath.toLowerCase();
347
+ if (lower.includes('steamapps'))
348
+ return 'steam';
349
+ if (lower.includes('windowsapps') || lower.includes('packages'))
350
+ return 'uwp';
351
+ return 'standalone';
352
+ }
353
+ function extOf(absPath) {
354
+ const name = basename(absPath);
355
+ const dot = name.lastIndexOf('.');
356
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
357
+ }
358
+ function guessBinaryKind(absPath) {
359
+ const ext = extOf(absPath);
360
+ if (ext === 'exe')
361
+ return 'exe';
362
+ if (ext === 'so')
363
+ return 'so';
364
+ return 'dll';
365
+ }
366
+ function errString(err) {
367
+ if (err instanceof Error)
368
+ return err.message;
369
+ return String(err);
370
+ }
371
+ function appendNote(existing, addition) {
372
+ if (!existing)
373
+ return addition;
374
+ if (!addition)
375
+ return existing;
376
+ return `${existing}; ${addition}`;
377
+ }
378
+ function mergeNotes(a, b) {
379
+ const merged = appendNote(a ?? '', b ?? '');
380
+ return merged.length > 0 ? merged : null;
381
+ }
382
+ //# sourceMappingURL=unpack-tools.js.map
@@ -0,0 +1,2 @@
1
+ import { ToolDefinition } from './index.js';
2
+ export declare function getWorkflowTools(): ToolDefinition[];