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,394 @@
1
+ import { statSync } from 'fs';
2
+ import { basename, extname } from 'path';
3
+ const SOURCE_TOOL = 'file-classifier';
4
+ /* ----------------------------------------------------------------------------
5
+ * Extension tables. Lowercase, no leading dot. Order of evaluation in classifyFile:
6
+ * binary -> data_store -> asset (containers) -> asset (typed) -> asset (other)
7
+ * -------------------------------------------------------------------------- */
8
+ const BINARY_EXTS = new Set(['exe', 'dll', 'so', 'dylib', 'a', 'lib', 'bin']);
9
+ /** Container archives — catalogued AS containers, never walked/expanded (A7). */
10
+ const CONTAINER_EXTS = new Set(['pak', 'pck', 'bundle', 'assets', 'unity3d', 'resource', 'ress', 'asset', 'rpf']);
11
+ const TEXTURE_EXTS = new Set(['png', 'dds', 'astc', 'ktx', 'ktx2', 'tga', 'jpg', 'jpeg', 'bmp', 'psd', 'exr', 'hdr', 'webp', 'gif', 'pvr']);
12
+ const MESH_EXTS = new Set(['fbx', 'obj', 'gltf', 'glb', 'mesh', 'dae', '3ds', 'blend', 'ply', 'stl']);
13
+ const AUDIO_EXTS = new Set(['wav', 'ogg', 'mp3', 'bank', 'fsb', 'flac', 'aiff', 'aif', 'wem', 'xnb', 'm4a', 'opus']);
14
+ const SHADER_EXTS = new Set(['shader', 'glsl', 'hlsl', 'cg', 'cginc', 'spv', 'vert', 'frag', 'comp', 'gdshader', 'shadergraph']);
15
+ const LEVEL_EXTS = new Set(['umap', 'tscn', 'scn', 'level', 'map', 'world', 'tmx', 'unity']);
16
+ const FONT_EXTS = new Set(['ttf', 'otf', 'woff', 'woff2', 'fnt', 'fon']);
17
+ /** Configuration / save / data stores. */
18
+ const JSON_EXTS = new Set(['json', 'json5', 'jsonc']);
19
+ const INI_EXTS = new Set(['ini', 'cfg', 'conf', 'toml', 'yaml', 'yml', 'properties']);
20
+ const SQLITE_EXTS = new Set(['sqlite', 'sqlite3', 'db', 'db3', 's3db']);
21
+ const BLOB_EXTS = new Set(['dat', 'save', 'sav', 'bin', 'blob', 'pref', 'prefs', 'profile', 'slot']);
22
+ /**
23
+ * Files we deliberately SKIP — pure documentation/source/noise that is not a binary,
24
+ * asset, or data store the autopsy cares about. Kept narrow on purpose: when in doubt
25
+ * we catalogue as asset/type='other' (A3), we do NOT skip.
26
+ */
27
+ const SKIP_EXTS = new Set(['txt', 'md', 'log', 'pdb', 'mdb', 'rtf', 'html', 'htm', 'url', 'lnk', 'cs', 'manifest']);
28
+ /* ----------------------------------------------------------------------------
29
+ * Helpers — pure string heuristics over the filename.
30
+ * -------------------------------------------------------------------------- */
31
+ /** Lowercased extension with no leading dot. '' when there is no extension. */
32
+ function ext(absPath) {
33
+ const e = extname(absPath).toLowerCase();
34
+ return e.startsWith('.') ? e.slice(1) : e;
35
+ }
36
+ /** Map a binary extension to the schema's binary.kind. Defaults non-exe natives to 'so'. */
37
+ function binaryKind(extension) {
38
+ if (extension === 'exe')
39
+ return 'exe';
40
+ if (extension === 'dll')
41
+ return 'dll';
42
+ // .so/.dylib/.a/.lib/.bin native artifacts collapse to the schema's 'so' bucket.
43
+ return 'so';
44
+ }
45
+ /** Cheap arch hint from the filename. Returns undefined when nothing is implied. */
46
+ function archHint(nameLower) {
47
+ if (nameLower.includes('win64') || nameLower.includes('x64') || nameLower.includes('amd64') || nameLower.includes('x86_64'))
48
+ return 'x64';
49
+ if (nameLower.includes('arm64') || nameLower.includes('aarch64'))
50
+ return 'arm64';
51
+ if (nameLower.includes('win32') || nameLower.includes('x86') || nameLower.includes('i386'))
52
+ return 'x86';
53
+ return undefined;
54
+ }
55
+ /**
56
+ * Heuristic managed-vs-native guess WITHOUT reading the CLR header (PR-1 scope).
57
+ * .NET managed assemblies in game installs are overwhelmingly the well-known names below,
58
+ * or live under a Managed/ directory. Native engine DLLs (UnityPlayer, GameAssembly,
59
+ * d3d*, etc.) are not managed. This is a guess; caller stamps provenance='inferred'.
60
+ */
61
+ function guessManaged(absPath, nameLower, extension) {
62
+ // Only .dll / .exe can be managed CLR images in practice.
63
+ if (extension !== 'dll' && extension !== 'exe')
64
+ return false;
65
+ const pathLower = absPath.toLowerCase().replace(/\\/g, '/');
66
+ // Known-native engine binaries are never managed.
67
+ const knownNative = [
68
+ 'unityplayer', 'gameassembly', 'mono-2.0', 'mono-2_0', 'monobdwgc',
69
+ 'd3d', 'd3dcompiler', 'xinput', 'openvr', 'openal', 'steam_api',
70
+ 'sdl2', 'libcrypto', 'libssl', 'vcruntime', 'msvcp', 'msvcr',
71
+ 'concrt', 'ucrtbase', 'cri_', 'fmod', 'wwise', 'physx', 'nvtt',
72
+ ];
73
+ if (knownNative.some((n) => nameLower.includes(n)))
74
+ return false;
75
+ // Anything under a Mono Managed/ directory is a managed assembly.
76
+ if (pathLower.includes('/managed/'))
77
+ return true;
78
+ // Canonical Unity-Mono managed assemblies.
79
+ const knownManaged = [
80
+ 'assembly-csharp', 'assembly-unityscript', 'mscorlib', 'system.',
81
+ 'unityengine', 'unity.', 'netstandard', 'mono.security', 'mono.cecil',
82
+ 'newtonsoft.json', 'protobuf', 'bepinex', 'harmony', '0harmony',
83
+ ];
84
+ if (knownManaged.some((n) => nameLower.includes(n)))
85
+ return true;
86
+ // .NET app exes commonly ship a same-named .deps.json/.runtimeconfig.json beside them,
87
+ // but we don't probe siblings here. Conservative default: native.
88
+ return false;
89
+ }
90
+ /**
91
+ * Heuristic role for a binary: game-logic | shell | engine | lib | adsdk.
92
+ * Pure name-based; provenance='inferred'.
93
+ */
94
+ function guessRole(nameLower, managed) {
95
+ // Ad / analytics SDKs first — these are the noisiest false-"game-logic" sources.
96
+ const adMarkers = [
97
+ 'admob', 'admost', 'applovin', 'unityads', 'ironsource', 'vungle',
98
+ 'chartboost', 'tapjoy', 'adcolony', 'mopub', 'inmobi', 'facebook.ads',
99
+ 'firebase', 'gameanalytics', 'appsflyer', 'adjust', 'mintegral',
100
+ 'fyber', 'pangle', 'bytedance', 'analytics', 'crashlytics', 'sentry',
101
+ ];
102
+ if (adMarkers.some((n) => nameLower.includes(n)) || /(^|[^a-z])ad(s|sdk)?([^a-z]|$)/.test(nameLower)) {
103
+ return 'adsdk';
104
+ }
105
+ // Engine runtime binaries.
106
+ const engineMarkers = [
107
+ 'unityplayer', 'gameassembly', 'mono-2.0', 'mono-2_0', 'godot', 'libgodot',
108
+ 'd3d', 'd3dcompiler', 'dxgi', 'vulkan', 'opengl', 'openvr', 'openal',
109
+ 'fmod', 'wwise', 'physx', 'sdl2', 'glfw', 'bink', 'cri_', 'ue4', 'ue5',
110
+ ];
111
+ if (engineMarkers.some((n) => nameLower.includes(n)))
112
+ return 'engine';
113
+ // The primary game-logic assembly.
114
+ const gameLogicMarkers = ['assembly-csharp', 'gameassembly', 'assembly-unityscript'];
115
+ if (gameLogicMarkers.some((n) => nameLower.includes(n)))
116
+ return 'game-logic';
117
+ // Managed shell (the thin UWP/.NET launcher wrapping native logic, e.g. HCR's managed shell).
118
+ if (managed && (nameLower.includes('launcher') || nameLower.includes('bootstrap') || nameLower.includes('shell'))) {
119
+ return 'shell';
120
+ }
121
+ // Generic runtime/support libraries.
122
+ const libMarkers = [
123
+ 'vcruntime', 'msvcp', 'msvcr', 'ucrtbase', 'concrt', 'mscorlib', 'system.',
124
+ 'netstandard', 'newtonsoft', 'protobuf', 'libcrypto', 'libssl', 'zlib',
125
+ 'lua', 'sqlite', 'icu', 'freetype', 'mono.', 'unity.', 'unityengine.',
126
+ ];
127
+ if (libMarkers.some((n) => nameLower.includes(n)))
128
+ return 'lib';
129
+ // A managed assembly we couldn't otherwise place is most likely game logic.
130
+ if (managed)
131
+ return 'game-logic';
132
+ // An unplaced native binary is most likely a support library.
133
+ return 'lib';
134
+ }
135
+ /**
136
+ * A7 hard-edge heuristic: does this filename smell like an unreadable / possibly-encrypted
137
+ * managed binary (IL2CPP "garbage", obfuscated metadata, packed shell)? This is a NAME-level
138
+ * signal only — the walker that actually fails to read the bytes is the authoritative source.
139
+ * Returns true so classifyFile can stamp provenance='uncertain' + the standard note.
140
+ */
141
+ function looksUnreadableManaged(nameLower) {
142
+ // global-metadata.dat is IL2CPP's metadata blob — it is NOT a managed assembly we can
143
+ // decompile (KB 920f537e). It is handled as a data_store blob elsewhere, but if it ever
144
+ // reaches the binary path we flag it.
145
+ return (nameLower.includes('global-metadata') ||
146
+ nameLower.includes('il2cpp') ||
147
+ nameLower.endsWith('.dll.enc') ||
148
+ nameLower.endsWith('.dll.bytes'));
149
+ }
150
+ /** data_store.scope guess from path location. Pure path heuristic; provenance='inferred'. */
151
+ function guessScope(absPath, nameLower) {
152
+ const pathLower = absPath.toLowerCase().replace(/\\/g, '/');
153
+ // Roaming / per-user appdata locations.
154
+ if (pathLower.includes('/appdata/roaming/') ||
155
+ pathLower.includes('/appdata/local/') ||
156
+ pathLower.includes('/appdata/locallow/') ||
157
+ pathLower.includes('/saved/savegames/') ||
158
+ pathLower.includes('/userdata/')) {
159
+ return 'roaming';
160
+ }
161
+ // Save artifacts by name/dir.
162
+ if (nameLower.includes('save') ||
163
+ nameLower.includes('.sav') ||
164
+ nameLower.includes('profile') ||
165
+ nameLower.includes('slot') ||
166
+ nameLower.includes('progress') ||
167
+ pathLower.includes('/saves/') ||
168
+ pathLower.includes('/savegames/')) {
169
+ return 'save';
170
+ }
171
+ // Everything else co-located with the install is config.
172
+ return 'config';
173
+ }
174
+ /** data_store.kind guess from extension + name. */
175
+ function guessDataStoreKind(extension, nameLower) {
176
+ if (JSON_EXTS.has(extension))
177
+ return 'json';
178
+ if (INI_EXTS.has(extension))
179
+ return 'ini';
180
+ if (SQLITE_EXTS.has(extension))
181
+ return 'sqlite';
182
+ // Windows registry hive (e.g. HCR's settings.dat 512KB hive — KB) tends to be a .dat
183
+ // named like a settings/registry store. We can't confirm without reading magic bytes,
184
+ // so this stays 'binary-blob' unless the name strongly implies a hive.
185
+ if ((extension === 'dat' || extension === '') && (nameLower.includes('settings') || nameLower.includes('registry'))) {
186
+ return 'registry-hive';
187
+ }
188
+ return 'binary-blob';
189
+ }
190
+ function readSize(absPath) {
191
+ try {
192
+ const st = statSync(absPath);
193
+ return { size: st.size, verified: true };
194
+ }
195
+ catch (err) {
196
+ const msg = err instanceof Error ? err.message : String(err);
197
+ return { verified: false, note: `size unreadable (${msg})` };
198
+ }
199
+ }
200
+ /** Compose a final notes string, dropping empty fragments. */
201
+ function joinNotes(...parts) {
202
+ return parts.filter((p) => !!p && p.trim().length > 0).join('; ');
203
+ }
204
+ /**
205
+ * Resolve overall provenance for a classification.
206
+ * - Classification itself is heuristic => baseline 'inferred'.
207
+ * - If we genuinely could not read the file (statSync failed) OR it smells
208
+ * unreadable/encrypted => 'uncertain'.
209
+ * Note: the SIZE fact, when read, is 'verified', but the row's *classification*
210
+ * provenance reflects the weakest fact, which is the heuristic itself.
211
+ */
212
+ function resolveProvenance(unreadable) {
213
+ return unreadable ? 'uncertain' : 'inferred';
214
+ }
215
+ /* ----------------------------------------------------------------------------
216
+ * The classifier.
217
+ * -------------------------------------------------------------------------- */
218
+ /**
219
+ * Classify a single file by absolute path. Pure aside from one optional statSync (size).
220
+ * NEVER throws — every failure path degrades to a note + provenance, satisfying A3 & A7.
221
+ *
222
+ * @param absPath absolute path to a file inside the game install.
223
+ * @returns a discriminated FileClassification; switch on `.target`.
224
+ */
225
+ export function classifyFile(absPath) {
226
+ // Defensive: a malformed/empty path is skipped rather than throwing.
227
+ if (!absPath || typeof absPath !== 'string') {
228
+ return {
229
+ target: 'skip',
230
+ reason: 'empty or invalid path provided to classifyFile',
231
+ note: 'empty or invalid path provided to classifyFile',
232
+ provenance: 'uncertain',
233
+ source_tool: SOURCE_TOOL,
234
+ };
235
+ }
236
+ const name = basename(absPath);
237
+ const nameLower = name.toLowerCase();
238
+ const extension = ext(absPath);
239
+ const sizeRead = readSize(absPath);
240
+ const sizeNote = sizeRead.note; // undefined unless statSync failed
241
+ const unreadable = !sizeRead.verified;
242
+ /* -------------------- BINARY -------------------- */
243
+ if (BINARY_EXTS.has(extension)) {
244
+ const kind = binaryKind(extension);
245
+ const managed = guessManaged(absPath, nameLower, extension);
246
+ const role = guessRole(nameLower, managed);
247
+ const arch = archHint(nameLower);
248
+ // A7 hard edge: managed binary that looks encrypted/garbage OR that we couldn't read.
249
+ const smellsUnreadable = looksUnreadableManaged(nameLower);
250
+ if (smellsUnreadable || (managed && unreadable)) {
251
+ const notes = joinNotes('unreadable/possibly-encrypted metadata', sizeNote);
252
+ return {
253
+ target: 'binary',
254
+ kind,
255
+ managed: true, // A7: classify as managed even though we can't confirm contents
256
+ role,
257
+ arch,
258
+ size: sizeRead.size,
259
+ notes,
260
+ note: notes,
261
+ provenance: 'uncertain',
262
+ source_tool: SOURCE_TOOL,
263
+ reason: smellsUnreadable
264
+ ? `${name} matches an IL2CPP/encrypted-metadata signature; catalogued as managed+unreadable, not expanded`
265
+ : `${name} is a managed binary whose bytes could not be read; catalogued as managed+unreadable`,
266
+ };
267
+ }
268
+ const notes = joinNotes(managed ? 'managed-guess by name/path heuristic (CLR header not read in PR-1)' : 'native-guess by name/path heuristic', sizeNote);
269
+ return {
270
+ target: 'binary',
271
+ kind,
272
+ managed,
273
+ role,
274
+ arch,
275
+ size: sizeRead.size,
276
+ notes,
277
+ note: notes,
278
+ // The managed flag is a heuristic; size (if read) is verified, but the row's
279
+ // classification rests on the heuristic => 'inferred' (or 'uncertain' if unreadable).
280
+ provenance: resolveProvenance(unreadable),
281
+ source_tool: SOURCE_TOOL,
282
+ reason: `extension .${extension} -> binary kind=${kind}, role=${role}, managed=${managed} (name/path heuristic)`,
283
+ };
284
+ }
285
+ /* -------------------- CONTAINERS (catalogue, never expand) — A7 -------------------- */
286
+ // Checked BEFORE generic data_store/asset so a .pck/.pak/.bundle is flagged as a container
287
+ // asset and is never walked as Unity content (KB b0175341).
288
+ if (CONTAINER_EXTS.has(extension)) {
289
+ const notes = joinNotes(`CONTAINER (.${extension}) catalogued as container, NOT expanded in PR-1`, sizeNote);
290
+ return {
291
+ target: 'asset',
292
+ type: 'other',
293
+ assetType: 'other',
294
+ format: extension,
295
+ decoded: false,
296
+ isContainer: true,
297
+ size: sizeRead.size,
298
+ notes,
299
+ note: notes,
300
+ provenance: resolveProvenance(unreadable),
301
+ source_tool: SOURCE_TOOL,
302
+ reason: `.${extension} is a known container/archive format; catalogued as a container (decoded=false), expansion deferred to P1.2`,
303
+ };
304
+ }
305
+ /* -------------------- DATA_STORE -------------------- */
306
+ if (JSON_EXTS.has(extension) ||
307
+ INI_EXTS.has(extension) ||
308
+ SQLITE_EXTS.has(extension) ||
309
+ BLOB_EXTS.has(extension)) {
310
+ const kind = guessDataStoreKind(extension, nameLower);
311
+ const scope = guessScope(absPath, nameLower);
312
+ const notes = joinNotes(`data store kind=${kind}, scope=${scope} (name/path heuristic; contents not parsed in PR-1)`, sizeNote);
313
+ return {
314
+ target: 'data_store',
315
+ kind,
316
+ storeKind: kind,
317
+ scope,
318
+ schema_known: false,
319
+ size: sizeRead.size,
320
+ notes,
321
+ note: notes,
322
+ provenance: resolveProvenance(unreadable),
323
+ source_tool: SOURCE_TOOL,
324
+ reason: `extension .${extension} -> data_store kind=${kind}, scope=${scope}`,
325
+ };
326
+ }
327
+ /* -------------------- TYPED ASSETS -------------------- */
328
+ const assetType = assetTypeFor(extension);
329
+ if (assetType) {
330
+ const notes = joinNotes(`asset type=${assetType} by extension`, sizeNote);
331
+ return {
332
+ target: 'asset',
333
+ type: assetType,
334
+ assetType,
335
+ format: extension,
336
+ decoded: false,
337
+ isContainer: false,
338
+ size: sizeRead.size,
339
+ notes,
340
+ note: notes,
341
+ provenance: resolveProvenance(unreadable),
342
+ source_tool: SOURCE_TOOL,
343
+ reason: `extension .${extension} -> asset type=${assetType}`,
344
+ };
345
+ }
346
+ /* -------------------- SKIP (documentation / source / noise) -------------------- */
347
+ if (SKIP_EXTS.has(extension)) {
348
+ const reason = `extension .${extension} is documentation/source/noise (not a binary, asset, or data store)`;
349
+ return {
350
+ target: 'skip',
351
+ reason,
352
+ note: reason,
353
+ provenance: 'inferred',
354
+ source_tool: SOURCE_TOOL,
355
+ };
356
+ }
357
+ /* -------------------- UNKNOWN -> asset/type='other' (A3: never silently dropped) ----- */
358
+ const unknownNotes = joinNotes(extension
359
+ ? `unknown extension .${extension}; catalogued as asset/type=other so nothing is dropped (A3)`
360
+ : `no file extension; catalogued as asset/type=other so nothing is dropped (A3)`, sizeNote);
361
+ return {
362
+ target: 'asset',
363
+ type: 'other',
364
+ assetType: 'other',
365
+ format: extension,
366
+ decoded: false,
367
+ isContainer: false,
368
+ size: sizeRead.size,
369
+ notes: unknownNotes,
370
+ note: unknownNotes,
371
+ provenance: resolveProvenance(unreadable),
372
+ source_tool: SOURCE_TOOL,
373
+ reason: extension
374
+ ? `unrecognized extension .${extension}; catalogued as asset/type=other (A3 — never silently dropped)`
375
+ : `file has no extension; catalogued as asset/type=other (A3 — never silently dropped)`,
376
+ };
377
+ }
378
+ /** Map an extension to a typed asset.type, or null if not a known typed asset. */
379
+ function assetTypeFor(extension) {
380
+ if (TEXTURE_EXTS.has(extension))
381
+ return 'texture';
382
+ if (MESH_EXTS.has(extension))
383
+ return 'mesh';
384
+ if (AUDIO_EXTS.has(extension))
385
+ return 'audio';
386
+ if (SHADER_EXTS.has(extension))
387
+ return 'shader';
388
+ if (LEVEL_EXTS.has(extension))
389
+ return 'level';
390
+ if (FONT_EXTS.has(extension))
391
+ return 'font';
392
+ return null;
393
+ }
394
+ //# sourceMappingURL=file-classifier.js.map