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,1201 @@
1
+ import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import { session } from '../../core/session.js';
4
+ import { logger } from '../../utils/logger.js';
5
+ /**
6
+ * Native tool implementations ported from binary-analyzer-ultra and smart-binary-analyzer.
7
+ * These run directly in the Node.js process without spawning child MCP servers.
8
+ */
9
+ function nativeTool(name, description, inputSchema, handler) {
10
+ return {
11
+ name,
12
+ description,
13
+ inputSchema,
14
+ handler: async (args) => {
15
+ const start = Date.now();
16
+ logger.toolStart(name, 'native');
17
+ try {
18
+ const result = await handler(args);
19
+ const duration = Date.now() - start;
20
+ logger.toolComplete(name, 'native', duration, true);
21
+ session.recordToolCall({ tool: name, timestamp: start, duration, success: true, backend: 'native' });
22
+ return result;
23
+ }
24
+ catch (err) {
25
+ const duration = Date.now() - start;
26
+ const msg = err instanceof Error ? err.message : String(err);
27
+ logger.toolComplete(name, 'native', duration, false);
28
+ session.recordToolCall({ tool: name, timestamp: start, duration, success: false, backend: 'native' });
29
+ return `ERROR: ${msg}`;
30
+ }
31
+ },
32
+ };
33
+ }
34
+ // ============================================================
35
+ // PE / Binary Analysis Tools
36
+ // ============================================================
37
+ function readPEHeaders(buffer) {
38
+ if (buffer.length < 64)
39
+ return { error: 'File too small for PE' };
40
+ const magic = buffer.readUInt16LE(0);
41
+ if (magic !== 0x5A4D)
42
+ return { error: 'Not a PE file (missing MZ signature)' };
43
+ const peOffset = buffer.readUInt32LE(60);
44
+ if (peOffset + 4 > buffer.length)
45
+ return { error: 'Invalid PE offset' };
46
+ const peSignature = buffer.readUInt32LE(peOffset);
47
+ if (peSignature !== 0x00004550)
48
+ return { error: 'Invalid PE signature' };
49
+ const coffOffset = peOffset + 4;
50
+ const machine = buffer.readUInt16LE(coffOffset);
51
+ const numberOfSections = buffer.readUInt16LE(coffOffset + 2);
52
+ const timeDateStamp = buffer.readUInt32LE(coffOffset + 4);
53
+ const sizeOfOptionalHeader = buffer.readUInt16LE(coffOffset + 16);
54
+ const characteristics = buffer.readUInt16LE(coffOffset + 18);
55
+ const optionalOffset = coffOffset + 20;
56
+ const optionalMagic = buffer.readUInt16LE(optionalOffset);
57
+ const is64Bit = optionalMagic === 0x20B;
58
+ // Read Data Directories
59
+ let dataDirectoryOffset;
60
+ let numberOfRvaAndSizes;
61
+ if (is64Bit) {
62
+ dataDirectoryOffset = optionalOffset + 112;
63
+ numberOfRvaAndSizes = buffer.readUInt32LE(optionalOffset + 108);
64
+ }
65
+ else {
66
+ dataDirectoryOffset = optionalOffset + 96;
67
+ numberOfRvaAndSizes = buffer.readUInt32LE(optionalOffset + 92);
68
+ }
69
+ const dataDirectories = [];
70
+ const ddNames = [
71
+ 'Export', 'Import', 'Resource', 'Exception', 'Security', 'BaseRelocation',
72
+ 'Debug', 'Architecture', 'GlobalPtr', 'TLS', 'LoadConfig', 'BoundImport',
73
+ 'IAT', 'DelayImport', 'CLR Runtime Header', 'Reserved',
74
+ ];
75
+ for (let i = 0; i < Math.min(numberOfRvaAndSizes, 16); i++) {
76
+ const offset = dataDirectoryOffset + i * 8;
77
+ if (offset + 8 > buffer.length)
78
+ break;
79
+ dataDirectories.push({
80
+ name: ddNames[i] || `Directory ${i}`,
81
+ rva: buffer.readUInt32LE(offset),
82
+ size: buffer.readUInt32LE(offset + 4),
83
+ });
84
+ }
85
+ // Read Sections
86
+ const sectionOffset = optionalOffset + sizeOfOptionalHeader;
87
+ const sections = [];
88
+ for (let i = 0; i < numberOfSections; i++) {
89
+ const off = sectionOffset + i * 40;
90
+ if (off + 40 > buffer.length)
91
+ break;
92
+ const nameBytes = buffer.subarray(off, off + 8);
93
+ const name = nameBytes.toString('ascii').replace(/\0+$/, '');
94
+ sections.push({
95
+ name,
96
+ virtualSize: buffer.readUInt32LE(off + 8),
97
+ virtualAddress: buffer.readUInt32LE(off + 12),
98
+ rawDataSize: buffer.readUInt32LE(off + 16),
99
+ rawDataPointer: buffer.readUInt32LE(off + 20),
100
+ characteristics: buffer.readUInt32LE(off + 36),
101
+ });
102
+ }
103
+ const machineTypes = {
104
+ 0x14C: 'x86',
105
+ 0x8664: 'x64',
106
+ 0xAA64: 'ARM64',
107
+ 0x1C0: 'ARM',
108
+ };
109
+ const clrDirectory = dataDirectories.find(d => d.name === 'CLR Runtime Header');
110
+ const isManaged = clrDirectory && clrDirectory.rva > 0;
111
+ return {
112
+ architecture: machineTypes[machine] || `Unknown (0x${machine.toString(16)})`,
113
+ is64Bit,
114
+ isManaged,
115
+ isDLL: (characteristics & 0x2000) !== 0,
116
+ numberOfSections,
117
+ timeDateStamp: new Date(timeDateStamp * 1000).toISOString(),
118
+ dataDirectories: dataDirectories.filter(d => d.rva > 0 || d.size > 0),
119
+ sections,
120
+ };
121
+ }
122
+ export function getNativeToolDefinitions() {
123
+ return [
124
+ // === PE Analysis ===
125
+ nativeTool('analyze_pe_full', 'Full PE (Portable Executable) analysis. Shows headers, sections, data directories, and whether it is managed (.NET) or native.', {
126
+ type: 'object',
127
+ properties: {
128
+ file_path: { type: 'string', description: 'Path to PE file (.exe or .dll)' },
129
+ },
130
+ required: ['file_path'],
131
+ }, async (args) => {
132
+ const filePath = args.file_path;
133
+ const buffer = readFileSync(filePath);
134
+ const result = readPEHeaders(buffer);
135
+ return JSON.stringify({
136
+ file: basename(filePath),
137
+ size: buffer.length,
138
+ ...result,
139
+ }, null, 2);
140
+ }),
141
+ nativeTool('analyze_file_format', 'Detect file format from magic bytes. Works with PE, ELF, Mach-O, ZIP, Java class, and other formats.', {
142
+ type: 'object',
143
+ properties: {
144
+ file_path: { type: 'string', description: 'Path to the file' },
145
+ },
146
+ required: ['file_path'],
147
+ }, async (args) => {
148
+ const filePath = args.file_path;
149
+ const buffer = readFileSync(filePath, { flag: 'r' });
150
+ const header = buffer.subarray(0, Math.min(16, buffer.length));
151
+ const formats = [
152
+ { magic: [0x4D, 0x5A], name: 'PE', description: 'Windows Portable Executable (EXE/DLL)' },
153
+ { magic: [0x7F, 0x45, 0x4C, 0x46], name: 'ELF', description: 'Linux/Unix Executable' },
154
+ { magic: [0xCA, 0xFE, 0xBA, 0xBE], name: 'Java Class / Mach-O FAT', description: 'Java class file or Mach-O fat binary' },
155
+ { magic: [0x50, 0x4B, 0x03, 0x04], name: 'ZIP/JAR', description: 'ZIP archive (possibly JAR, APK, or EPUB)' },
156
+ { magic: [0x52, 0x61, 0x72, 0x21], name: 'RAR', description: 'RAR archive' },
157
+ { magic: [0x89, 0x50, 0x4E, 0x47], name: 'PNG', description: 'PNG image' },
158
+ ];
159
+ for (const fmt of formats) {
160
+ if (fmt.magic.every((b, i) => header[i] === b)) {
161
+ const info = {
162
+ file: basename(filePath),
163
+ size: buffer.length,
164
+ format: fmt.name,
165
+ description: fmt.description,
166
+ };
167
+ // Extra PE analysis
168
+ if (fmt.name === 'PE') {
169
+ const pe = readPEHeaders(buffer);
170
+ info.pe = pe;
171
+ }
172
+ return JSON.stringify(info, null, 2);
173
+ }
174
+ }
175
+ return JSON.stringify({
176
+ file: basename(filePath),
177
+ size: buffer.length,
178
+ format: 'Unknown',
179
+ headerHex: header.toString('hex'),
180
+ headerAscii: header.toString('ascii').replace(/[^\x20-\x7e]/g, '.'),
181
+ }, null, 2);
182
+ }),
183
+ // === Hex Operations ===
184
+ nativeTool('hex_read', 'Read raw bytes from a file at a specific offset.', {
185
+ type: 'object',
186
+ properties: {
187
+ file_path: { type: 'string', description: 'Path to the file' },
188
+ offset: { type: 'number', description: 'Byte offset to start reading' },
189
+ length: { type: 'number', description: 'Number of bytes to read (default 256)' },
190
+ },
191
+ required: ['file_path', 'offset'],
192
+ }, async (args) => {
193
+ const filePath = args.file_path;
194
+ const offset = args.offset;
195
+ const length = args.length || 256;
196
+ const fd = readFileSync(filePath);
197
+ const data = fd.subarray(offset, offset + length);
198
+ // Format as hex dump
199
+ const lines = [];
200
+ for (let i = 0; i < data.length; i += 16) {
201
+ const chunk = data.subarray(i, Math.min(i + 16, data.length));
202
+ const hex = Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' ');
203
+ const ascii = Array.from(chunk).map(b => (b >= 0x20 && b <= 0x7e) ? String.fromCharCode(b) : '.').join('');
204
+ lines.push(`${(offset + i).toString(16).padStart(8, '0')} ${hex.padEnd(48)} ${ascii}`);
205
+ }
206
+ return lines.join('\n');
207
+ }),
208
+ nativeTool('hex_write', 'Write raw bytes to a file at a specific offset.', {
209
+ type: 'object',
210
+ properties: {
211
+ file_path: { type: 'string', description: 'Path to the file' },
212
+ offset: { type: 'number', description: 'Byte offset to write at' },
213
+ hex_data: { type: 'string', description: 'Hex string to write (e.g. "90 90 90" or "909090")' },
214
+ },
215
+ required: ['file_path', 'offset', 'hex_data'],
216
+ }, async (args) => {
217
+ const filePath = args.file_path;
218
+ const offset = args.offset;
219
+ const hexStr = args.hex_data.replace(/\s+/g, '');
220
+ const bytes = Buffer.from(hexStr, 'hex');
221
+ const fd = readFileSync(filePath);
222
+ bytes.copy(fd, offset);
223
+ writeFileSync(filePath, fd);
224
+ return `Wrote ${bytes.length} bytes at offset 0x${offset.toString(16)}`;
225
+ }),
226
+ nativeTool('hex_search', 'Search for a hex pattern in a binary file.', {
227
+ type: 'object',
228
+ properties: {
229
+ file_path: { type: 'string', description: 'Path to the file' },
230
+ hex_pattern: { type: 'string', description: 'Hex pattern to search for (e.g. "48 8B 05 ?? ?? ?? ??")' },
231
+ max_results: { type: 'number', description: 'Max results (default 20)' },
232
+ },
233
+ required: ['file_path', 'hex_pattern'],
234
+ }, async (args) => {
235
+ const filePath = args.file_path;
236
+ const pattern = args.hex_pattern.trim();
237
+ const maxResults = args.max_results || 20;
238
+ const buffer = readFileSync(filePath);
239
+ const patternBytes = pattern.split(/\s+/).map(b => b === '??' ? null : parseInt(b, 16));
240
+ const results = [];
241
+ for (let i = 0; i <= buffer.length - patternBytes.length && results.length < maxResults; i++) {
242
+ let match = true;
243
+ for (let j = 0; j < patternBytes.length; j++) {
244
+ if (patternBytes[j] !== null && buffer[i + j] !== patternBytes[j]) {
245
+ match = false;
246
+ break;
247
+ }
248
+ }
249
+ if (match)
250
+ results.push(i);
251
+ }
252
+ return JSON.stringify({
253
+ file: basename(filePath),
254
+ pattern,
255
+ matchCount: results.length,
256
+ offsets: results.map(o => `0x${o.toString(16)}`),
257
+ }, null, 2);
258
+ }),
259
+ nativeTool('hex_replace', 'Replace bytes at a specific offset or replace a pattern throughout the file.', {
260
+ type: 'object',
261
+ properties: {
262
+ file_path: { type: 'string', description: 'Path to the file' },
263
+ search_hex: { type: 'string', description: 'Hex pattern to find' },
264
+ replace_hex: { type: 'string', description: 'Hex pattern to replace with (must be same length)' },
265
+ offset: { type: 'number', description: 'Specific offset to replace at (if provided, ignores search_hex)' },
266
+ },
267
+ required: ['file_path', 'replace_hex'],
268
+ }, async (args) => {
269
+ const filePath = args.file_path;
270
+ const replaceHex = args.replace_hex.replace(/\s+/g, '');
271
+ const replaceBytes = Buffer.from(replaceHex, 'hex');
272
+ const buffer = readFileSync(filePath);
273
+ if (args.offset !== undefined) {
274
+ const offset = args.offset;
275
+ replaceBytes.copy(buffer, offset);
276
+ writeFileSync(filePath, buffer);
277
+ return `Replaced ${replaceBytes.length} bytes at offset 0x${offset.toString(16)}`;
278
+ }
279
+ const searchHex = (args.search_hex || '').replace(/\s+/g, '');
280
+ if (!searchHex)
281
+ return 'ERROR: Either offset or search_hex must be provided';
282
+ const searchBytes = Buffer.from(searchHex, 'hex');
283
+ if (searchBytes.length !== replaceBytes.length) {
284
+ return `ERROR: Search (${searchBytes.length} bytes) and replace (${replaceBytes.length} bytes) must be same length`;
285
+ }
286
+ let count = 0;
287
+ for (let i = 0; i <= buffer.length - searchBytes.length; i++) {
288
+ if (buffer.subarray(i, i + searchBytes.length).equals(searchBytes)) {
289
+ replaceBytes.copy(buffer, i);
290
+ count++;
291
+ i += searchBytes.length - 1;
292
+ }
293
+ }
294
+ writeFileSync(filePath, buffer);
295
+ return `Replaced ${count} occurrence(s) of pattern`;
296
+ }),
297
+ // === Pattern Scan ===
298
+ nativeTool('pattern_scan', 'IDA-style pattern scan with wildcards. Searches for byte patterns in PE sections.', {
299
+ type: 'object',
300
+ properties: {
301
+ file_path: { type: 'string', description: 'Path to PE file' },
302
+ pattern: { type: 'string', description: 'Pattern with wildcards (e.g. "48 8B 05 ?? ?? ?? ?? 48 85 C0")' },
303
+ section: { type: 'string', description: 'PE section to search (default: .text)' },
304
+ max_results: { type: 'number', description: 'Max results (default 10)' },
305
+ },
306
+ required: ['file_path', 'pattern'],
307
+ }, async (args) => {
308
+ const filePath = args.file_path;
309
+ const pattern = args.pattern.trim();
310
+ const maxResults = args.max_results || 10;
311
+ const buffer = readFileSync(filePath);
312
+ const patternBytes = pattern.split(/\s+/).map(b => (b === '??' || b === '?') ? null : parseInt(b, 16));
313
+ const results = [];
314
+ for (let i = 0; i <= buffer.length - patternBytes.length && results.length < maxResults; i++) {
315
+ let match = true;
316
+ for (let j = 0; j < patternBytes.length; j++) {
317
+ if (patternBytes[j] !== null && buffer[i + j] !== patternBytes[j]) {
318
+ match = false;
319
+ break;
320
+ }
321
+ }
322
+ if (match) {
323
+ const ctx = buffer.subarray(i, Math.min(i + 32, buffer.length));
324
+ results.push({
325
+ offset: `0x${i.toString(16)}`,
326
+ context: Array.from(ctx).map(b => b.toString(16).padStart(2, '0')).join(' '),
327
+ });
328
+ }
329
+ }
330
+ return JSON.stringify({
331
+ file: basename(filePath),
332
+ pattern,
333
+ matchCount: results.length,
334
+ matches: results,
335
+ }, null, 2);
336
+ }),
337
+ nativeTool('pattern_scan_all', 'Scan entire file for a pattern (not limited to a section).', {
338
+ type: 'object',
339
+ properties: {
340
+ file_path: { type: 'string', description: 'Path to file' },
341
+ pattern: { type: 'string', description: 'Pattern with wildcards' },
342
+ max_results: { type: 'number', description: 'Max results (default 50)' },
343
+ },
344
+ required: ['file_path', 'pattern'],
345
+ }, async (args) => {
346
+ // Same as pattern_scan but explicitly full-file
347
+ const filePath = args.file_path;
348
+ const pattern = args.pattern.trim();
349
+ const maxResults = args.max_results || 50;
350
+ const buffer = readFileSync(filePath);
351
+ const patternBytes = pattern.split(/\s+/).map(b => (b === '??' || b === '?') ? null : parseInt(b, 16));
352
+ const offsets = [];
353
+ for (let i = 0; i <= buffer.length - patternBytes.length && offsets.length < maxResults; i++) {
354
+ let match = true;
355
+ for (let j = 0; j < patternBytes.length; j++) {
356
+ if (patternBytes[j] !== null && buffer[i + j] !== patternBytes[j]) {
357
+ match = false;
358
+ break;
359
+ }
360
+ }
361
+ if (match)
362
+ offsets.push(`0x${i.toString(16)}`);
363
+ }
364
+ return JSON.stringify({ file: basename(filePath), pattern, matchCount: offsets.length, offsets }, null, 2);
365
+ }),
366
+ // === String Extraction ===
367
+ nativeTool('extract_strings_advanced', 'Extract ASCII and UTF-16 strings from a binary file with filtering.', {
368
+ type: 'object',
369
+ properties: {
370
+ file_path: { type: 'string', description: 'Path to the binary file' },
371
+ filter: { type: 'string', description: 'Optional filter (case-insensitive substring match)' },
372
+ min_length: { type: 'number', description: 'Minimum string length (default 4)' },
373
+ max_results: { type: 'number', description: 'Max results (default 500)' },
374
+ encoding: { type: 'string', description: 'Encoding: ascii, utf16, both (default: both)' },
375
+ },
376
+ required: ['file_path'],
377
+ }, async (args) => {
378
+ const filePath = args.file_path;
379
+ const filter = args.filter?.toLowerCase();
380
+ const minLength = args.min_length || 4;
381
+ const maxResults = args.max_results || 500;
382
+ const encoding = args.encoding || 'both';
383
+ const buffer = readFileSync(filePath);
384
+ const strings = [];
385
+ // ASCII strings
386
+ if (encoding === 'ascii' || encoding === 'both') {
387
+ let current = '';
388
+ let startOffset = 0;
389
+ for (let i = 0; i < buffer.length && strings.length < maxResults; i++) {
390
+ const b = buffer[i];
391
+ if (b >= 0x20 && b <= 0x7e) {
392
+ if (current.length === 0)
393
+ startOffset = i;
394
+ current += String.fromCharCode(b);
395
+ }
396
+ else {
397
+ if (current.length >= minLength) {
398
+ if (!filter || current.toLowerCase().includes(filter)) {
399
+ strings.push({ offset: `0x${startOffset.toString(16)}`, encoding: 'ASCII', value: current });
400
+ }
401
+ }
402
+ current = '';
403
+ }
404
+ }
405
+ }
406
+ // UTF-16LE strings
407
+ if (encoding === 'utf16' || encoding === 'both') {
408
+ let current = '';
409
+ let startOffset = 0;
410
+ for (let i = 0; i < buffer.length - 1 && strings.length < maxResults; i += 2) {
411
+ const ch = buffer.readUInt16LE(i);
412
+ if (ch >= 0x20 && ch <= 0x7e) {
413
+ if (current.length === 0)
414
+ startOffset = i;
415
+ current += String.fromCharCode(ch);
416
+ }
417
+ else {
418
+ if (current.length >= minLength) {
419
+ if (!filter || current.toLowerCase().includes(filter)) {
420
+ strings.push({ offset: `0x${startOffset.toString(16)}`, encoding: 'UTF-16LE', value: current });
421
+ }
422
+ }
423
+ current = '';
424
+ }
425
+ }
426
+ }
427
+ return JSON.stringify({
428
+ file: basename(filePath),
429
+ totalFound: strings.length,
430
+ filter: filter || 'none',
431
+ strings: strings.slice(0, maxResults),
432
+ }, null, 2);
433
+ }),
434
+ // === Smart Binary Analyzer ports ===
435
+ nativeTool('extract_dll_classes', 'Extract class names from a .NET DLL using stream-based analysis. Works with very large files.', {
436
+ type: 'object',
437
+ properties: {
438
+ file_path: { type: 'string', description: 'Path to DLL file' },
439
+ search_terms: { type: 'array', items: { type: 'string' }, description: 'Filter by these terms (case-insensitive)' },
440
+ max_classes: { type: 'number', description: 'Max classes to return (default 200)' },
441
+ },
442
+ required: ['file_path'],
443
+ }, async (args) => {
444
+ const filePath = args.file_path;
445
+ const searchTerms = args.search_terms || [];
446
+ const maxClasses = args.max_classes || 200;
447
+ const buffer = readFileSync(filePath);
448
+ // Extract strings that look like class names (PascalCase, contain namespace dots)
449
+ const classPattern = /[A-Z][a-zA-Z0-9]*(?:\.[A-Z][a-zA-Z0-9]*)*/g;
450
+ const content = buffer.toString('utf-8', 0, Math.min(buffer.length, 10_000_000));
451
+ const matches = new Set();
452
+ let match;
453
+ while ((match = classPattern.exec(content)) !== null && matches.size < maxClasses * 5) {
454
+ const name = match[0];
455
+ if (name.length >= 3 && name.includes('.')) {
456
+ if (searchTerms.length === 0 || searchTerms.some(t => name.toLowerCase().includes(t.toLowerCase()))) {
457
+ matches.add(name);
458
+ }
459
+ }
460
+ }
461
+ const sorted = Array.from(matches).sort().slice(0, maxClasses);
462
+ return JSON.stringify({
463
+ file: basename(filePath),
464
+ classCount: sorted.length,
465
+ classes: sorted,
466
+ searchTerms: searchTerms.length > 0 ? searchTerms : 'none',
467
+ }, null, 2);
468
+ }),
469
+ nativeTool('search_binary_pattern', 'Search for text patterns in a binary file using stream-based approach.', {
470
+ type: 'object',
471
+ properties: {
472
+ file_path: { type: 'string', description: 'Path to binary file' },
473
+ patterns: { type: 'array', items: { type: 'string' }, description: 'Text patterns to search for' },
474
+ max_results: { type: 'number', description: 'Max results per pattern (default 50)' },
475
+ },
476
+ required: ['file_path', 'patterns'],
477
+ }, async (args) => {
478
+ const filePath = args.file_path;
479
+ const patterns = args.patterns;
480
+ const maxResults = args.max_results || 50;
481
+ const buffer = readFileSync(filePath);
482
+ const results = {};
483
+ for (const pattern of patterns) {
484
+ results[pattern] = [];
485
+ const searchBuf = Buffer.from(pattern, 'utf-8');
486
+ for (let i = 0; i <= buffer.length - searchBuf.length && results[pattern].length < maxResults; i++) {
487
+ if (buffer.subarray(i, i + searchBuf.length).equals(searchBuf)) {
488
+ const ctxStart = Math.max(0, i - 20);
489
+ const ctxEnd = Math.min(buffer.length, i + searchBuf.length + 20);
490
+ const ctx = buffer.subarray(ctxStart, ctxEnd).toString('utf-8').replace(/[^\x20-\x7e]/g, '.');
491
+ results[pattern].push({ offset: `0x${i.toString(16)}`, context: ctx });
492
+ }
493
+ }
494
+ }
495
+ return JSON.stringify({ file: basename(filePath), results }, null, 2);
496
+ }),
497
+ nativeTool('extract_strings', 'Simple string extraction from a binary file.', {
498
+ type: 'object',
499
+ properties: {
500
+ file_path: { type: 'string', description: 'Path to binary file' },
501
+ filter: { type: 'string', description: 'Optional filter' },
502
+ min_length: { type: 'number', description: 'Minimum string length (default 4)' },
503
+ },
504
+ required: ['file_path'],
505
+ }, async (args) => {
506
+ // Simplified version - delegates to extract_strings_advanced
507
+ const filePath = args.file_path;
508
+ const filter = args.filter;
509
+ const minLength = args.min_length || 4;
510
+ const buffer = readFileSync(filePath);
511
+ const strings = [];
512
+ let current = '';
513
+ for (let i = 0; i < buffer.length && strings.length < 1000; i++) {
514
+ const b = buffer[i];
515
+ if (b >= 0x20 && b <= 0x7e) {
516
+ current += String.fromCharCode(b);
517
+ }
518
+ else {
519
+ if (current.length >= minLength) {
520
+ if (!filter || current.toLowerCase().includes(filter.toLowerCase())) {
521
+ strings.push(current);
522
+ }
523
+ }
524
+ current = '';
525
+ }
526
+ }
527
+ return JSON.stringify({ file: basename(filePath), count: strings.length, strings }, null, 2);
528
+ }),
529
+ nativeTool('analyze_dll_structure', 'Analyze the overall structure of a DLL: sections, imports, exports summary.', {
530
+ type: 'object',
531
+ properties: {
532
+ file_path: { type: 'string', description: 'Path to DLL file' },
533
+ },
534
+ required: ['file_path'],
535
+ }, async (args) => {
536
+ const filePath = args.file_path;
537
+ const buffer = readFileSync(filePath);
538
+ const pe = readPEHeaders(buffer);
539
+ return JSON.stringify({ file: basename(filePath), size: buffer.length, ...pe }, null, 2);
540
+ }),
541
+ // === Checksum Tools ===
542
+ nativeTool('calculate_checksums', 'Calculate MD5, SHA1, SHA256 checksums for a file.', {
543
+ type: 'object',
544
+ properties: {
545
+ file_path: { type: 'string', description: 'Path to the file' },
546
+ },
547
+ required: ['file_path'],
548
+ }, async (args) => {
549
+ const { createHash } = await import('crypto');
550
+ const filePath = args.file_path;
551
+ const buffer = readFileSync(filePath);
552
+ return JSON.stringify({
553
+ file: basename(filePath),
554
+ size: buffer.length,
555
+ md5: createHash('md5').update(buffer).digest('hex'),
556
+ sha1: createHash('sha1').update(buffer).digest('hex'),
557
+ sha256: createHash('sha256').update(buffer).digest('hex'),
558
+ }, null, 2);
559
+ }),
560
+ nativeTool('compare_binaries_detailed', 'Compare two binary files and show differences.', {
561
+ type: 'object',
562
+ properties: {
563
+ file_path_a: { type: 'string', description: 'Path to first file' },
564
+ file_path_b: { type: 'string', description: 'Path to second file' },
565
+ max_diffs: { type: 'number', description: 'Max differences to show (default 50)' },
566
+ },
567
+ required: ['file_path_a', 'file_path_b'],
568
+ }, async (args) => {
569
+ const a = readFileSync(args.file_path_a);
570
+ const b = readFileSync(args.file_path_b);
571
+ const maxDiffs = args.max_diffs || 50;
572
+ const diffs = [];
573
+ const minLen = Math.min(a.length, b.length);
574
+ for (let i = 0; i < minLen && diffs.length < maxDiffs; i++) {
575
+ if (a[i] !== b[i]) {
576
+ diffs.push({
577
+ offset: `0x${i.toString(16)}`,
578
+ fileA: a[i].toString(16).padStart(2, '0'),
579
+ fileB: b[i].toString(16).padStart(2, '0'),
580
+ });
581
+ }
582
+ }
583
+ return JSON.stringify({
584
+ fileA: { name: basename(args.file_path_a), size: a.length },
585
+ fileB: { name: basename(args.file_path_b), size: b.length },
586
+ identical: a.equals(b),
587
+ sizeDifference: b.length - a.length,
588
+ diffCount: diffs.length,
589
+ diffs,
590
+ }, null, 2);
591
+ }),
592
+ // === RVA/Offset conversion ===
593
+ nativeTool('rva_to_offset', 'Convert a Relative Virtual Address (RVA) to a file offset in a PE file.', {
594
+ type: 'object',
595
+ properties: {
596
+ file_path: { type: 'string', description: 'Path to PE file' },
597
+ rva: { type: 'number', description: 'RVA to convert' },
598
+ },
599
+ required: ['file_path', 'rva'],
600
+ }, async (args) => {
601
+ const buffer = readFileSync(args.file_path);
602
+ const rva = args.rva;
603
+ const pe = readPEHeaders(buffer);
604
+ const sections = pe.sections;
605
+ for (const section of sections || []) {
606
+ const va = section.virtualAddress;
607
+ const vs = section.virtualSize;
608
+ if (rva >= va && rva < va + vs) {
609
+ const offset = rva - va + section.rawDataPointer;
610
+ return JSON.stringify({ rva: `0x${rva.toString(16)}`, fileOffset: `0x${offset.toString(16)}`, section: section.name }, null, 2);
611
+ }
612
+ }
613
+ return `Could not resolve RVA 0x${rva.toString(16)} to any section`;
614
+ }),
615
+ nativeTool('offset_to_rva', 'Convert a file offset to a Relative Virtual Address (RVA) in a PE file.', {
616
+ type: 'object',
617
+ properties: {
618
+ file_path: { type: 'string', description: 'Path to PE file' },
619
+ offset: { type: 'number', description: 'File offset to convert' },
620
+ },
621
+ required: ['file_path', 'offset'],
622
+ }, async (args) => {
623
+ const buffer = readFileSync(args.file_path);
624
+ const offset = args.offset;
625
+ const pe = readPEHeaders(buffer);
626
+ const sections = pe.sections;
627
+ for (const section of sections || []) {
628
+ const rawStart = section.rawDataPointer;
629
+ const rawEnd = rawStart + section.rawDataSize;
630
+ if (offset >= rawStart && offset < rawEnd) {
631
+ const rva = offset - rawStart + section.virtualAddress;
632
+ return JSON.stringify({ fileOffset: `0x${offset.toString(16)}`, rva: `0x${rva.toString(16)}`, section: section.name }, null, 2);
633
+ }
634
+ }
635
+ return `Could not resolve offset 0x${offset.toString(16)} to any section`;
636
+ }),
637
+ // === Godot PCK (basic) ===
638
+ nativeTool('analyze_godot_pck', 'Analyze a Godot .pck archive and list its contents.', {
639
+ type: 'object',
640
+ properties: {
641
+ file_path: { type: 'string', description: 'Path to .pck file' },
642
+ filter: { type: 'string', description: 'Optional filename filter' },
643
+ },
644
+ required: ['file_path'],
645
+ }, async (args) => {
646
+ const filePath = args.file_path;
647
+ const filter = args.filter;
648
+ const buffer = readFileSync(filePath);
649
+ // PCK magic: "GDPC" at offset 0
650
+ const magic = buffer.toString('ascii', 0, 4);
651
+ if (magic !== 'GDPC') {
652
+ return JSON.stringify({ error: 'Not a valid Godot PCK file (missing GDPC magic)' }, null, 2);
653
+ }
654
+ const version = buffer.readUInt32LE(4);
655
+ const verMajor = buffer.readUInt32LE(8);
656
+ const verMinor = buffer.readUInt32LE(12);
657
+ const verPatch = buffer.readUInt32LE(16);
658
+ // Skip flags (4 bytes at offset 20) + reserved (64 bytes)
659
+ const fileCount = buffer.readUInt32LE(88);
660
+ const files = [];
661
+ let offset = 92;
662
+ for (let i = 0; i < fileCount && offset < buffer.length; i++) {
663
+ const pathLen = buffer.readUInt32LE(offset);
664
+ offset += 4;
665
+ const path = buffer.toString('utf-8', offset, offset + pathLen).replace(/\0+$/, '');
666
+ offset += pathLen;
667
+ const fileOffset = Number(buffer.readBigUInt64LE(offset));
668
+ offset += 8;
669
+ const fileSize = Number(buffer.readBigUInt64LE(offset));
670
+ offset += 8;
671
+ const md5 = buffer.subarray(offset, offset + 16).toString('hex');
672
+ offset += 16;
673
+ // Skip flags (4 bytes) if version >= 2
674
+ if (version >= 2)
675
+ offset += 4;
676
+ if (!filter || path.toLowerCase().includes(filter.toLowerCase())) {
677
+ files.push({ path, offset: fileOffset, size: fileSize });
678
+ }
679
+ }
680
+ return JSON.stringify({
681
+ file: basename(filePath),
682
+ godotVersion: `${verMajor}.${verMinor}.${verPatch}`,
683
+ pckVersion: version,
684
+ totalFiles: fileCount,
685
+ matchedFiles: files.length,
686
+ files: files.slice(0, 200),
687
+ }, null, 2);
688
+ }),
689
+ // ============================================================
690
+ // GoreBox / Lua Mod Tools
691
+ // ============================================================
692
+ nativeTool('gorebox_generate_discovery_mod', 'Generate a GoreBox Lua mod that discovers all available API functions, globals, tables, and callbacks. Writes results to a dump file in the mod folder. Use this when you need to learn what Lua API is available in a game that uses Lua scripting.', {
693
+ type: 'object',
694
+ properties: {
695
+ mods_dir: { type: 'string', description: 'Path to the game Mods directory (e.g. C:/Users/.../GoreBox/Mods)' },
696
+ mod_name: { type: 'string', description: 'Name for the discovery mod (default: APIDisco)' },
697
+ },
698
+ required: ['mods_dir'],
699
+ }, async (args) => {
700
+ const modsDir = args.mods_dir;
701
+ const modName = args.mod_name || 'APIDisco';
702
+ const { mkdirSync } = await import('fs');
703
+ const { join } = await import('path');
704
+ const modDir = join(modsDir, modName);
705
+ if (!existsSync(modDir))
706
+ mkdirSync(modDir, { recursive: true });
707
+ // Write info.json
708
+ const infoJson = {
709
+ name: modName,
710
+ description: 'API Discovery - dumps all globals, functions, tables to file',
711
+ mainScript: 'main.lua',
712
+ version: '1.0.0',
713
+ active: true,
714
+ unloadOnError: false,
715
+ safeMode: false,
716
+ isPlugin: true,
717
+ };
718
+ writeFileSync(join(modDir, 'info.json'), JSON.stringify(infoJson, null, 2));
719
+ // The discovery Lua script - designed to be maximally resilient
720
+ // It tries multiple output methods and probes everything
721
+ const luaScript = `-- API Discovery Mod - Auto-generated
722
+ -- Probes the entire Lua environment and writes results to dump.txt
723
+
724
+ local _results = {}
725
+ local _errors = {}
726
+
727
+ local function safeStr(v)
728
+ local ok, s = pcall(tostring, v)
729
+ return ok and s or "<?>"
730
+ end
731
+
732
+ local function log(msg)
733
+ _results[#_results + 1] = msg
734
+ end
735
+
736
+ local function logError(msg)
737
+ _errors[#_errors + 1] = msg
738
+ end
739
+
740
+ -- Probe a table recursively (max depth 3)
741
+ local _seen = {}
742
+ local function probeTable(tbl, prefix, depth)
743
+ if depth > 3 then return end
744
+ if _seen[tbl] then return end
745
+ _seen[tbl] = true
746
+
747
+ local keys = {}
748
+ local ok, err = pcall(function()
749
+ for k, _ in pairs(tbl) do
750
+ keys[#keys + 1] = k
751
+ end
752
+ end)
753
+ if not ok then
754
+ logError("Cannot iterate: " .. prefix .. " (" .. safeStr(err) .. ")")
755
+ return
756
+ end
757
+
758
+ -- Sort keys
759
+ pcall(function()
760
+ table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
761
+ end)
762
+
763
+ for _, k in ipairs(keys) do
764
+ local fullKey = prefix .. "." .. tostring(k)
765
+ local vOk, v = pcall(function() return tbl[k] end)
766
+ if not vOk then
767
+ logError("Cannot read: " .. fullKey)
768
+ else
769
+ local t = type(v)
770
+ if t == "function" then
771
+ log("FUNC|" .. fullKey)
772
+ elseif t == "table" then
773
+ log("TABLE|" .. fullKey)
774
+ probeTable(v, fullKey, depth + 1)
775
+ elseif t == "userdata" then
776
+ log("USERDATA|" .. fullKey .. "|" .. safeStr(v))
777
+ -- Probe metatable
778
+ local mt = getmetatable(v)
779
+ if mt and type(mt) == "table" then
780
+ log(" METATABLE|" .. fullKey)
781
+ probeTable(mt, fullKey .. ".__mt", depth + 1)
782
+ end
783
+ elseif t == "string" then
784
+ local short = #v > 100 and v:sub(1, 100) .. "..." or v
785
+ log("STRING|" .. fullKey .. "|" .. short)
786
+ elseif t == "number" or t == "boolean" then
787
+ log(t:upper() .. "|" .. fullKey .. "|" .. tostring(v))
788
+ else
789
+ log(t:upper() .. "|" .. fullKey)
790
+ end
791
+ end
792
+ end
793
+ end
794
+
795
+ -- Standard Lua libs to skip (we know what these are)
796
+ local skipSet = {}
797
+ for _, name in ipairs({"_G", "_VERSION", "arg", "coroutine", "debug", "io", "math", "os", "package", "string", "table", "utf8", "bit", "bit32", "jit"}) do
798
+ skipSet[name] = true
799
+ end
800
+
801
+ log("=== API DISCOVERY START ===")
802
+ log("TIME|" .. (os.date and os.date() or "unknown"))
803
+
804
+ -- Enumerate all globals
805
+ local globalKeys = {}
806
+ pcall(function()
807
+ for k, _ in pairs(_G) do
808
+ globalKeys[#globalKeys + 1] = k
809
+ end
810
+ end)
811
+ pcall(function() table.sort(globalKeys) end)
812
+
813
+ log("GLOBAL_COUNT|" .. #globalKeys)
814
+
815
+ for _, k in ipairs(globalKeys) do
816
+ if not skipSet[k] then
817
+ local ok2, v = pcall(function() return _G[k] end)
818
+ if ok2 and v ~= nil then
819
+ local t = type(v)
820
+ if t == "function" then
821
+ log("GFUNC|" .. k)
822
+ elseif t == "table" then
823
+ log("GTABLE|" .. k)
824
+ probeTable(v, k, 0)
825
+ elseif t == "userdata" then
826
+ log("GUSERDATA|" .. k .. "|" .. safeStr(v))
827
+ local mt = getmetatable(v)
828
+ if mt and type(mt) == "table" then
829
+ probeTable(mt, k .. ".__mt", 0)
830
+ end
831
+ elseif t == "string" then
832
+ log("GSTRING|" .. k .. "|" .. (v:sub(1, 200)))
833
+ else
834
+ log("GVALUE|" .. k .. "|" .. t .. "|" .. safeStr(v))
835
+ end
836
+ end
837
+ end
838
+ end
839
+
840
+ -- Probe specific names that game engines commonly expose
841
+ local probeNames = {
842
+ "Debug", "GB", "GoreBox", "Game", "API", "ModAPI", "Mod", "Entity", "Entities",
843
+ "NPC", "Player", "World", "Spawn", "SpawnEntity", "Health", "Damage",
844
+ "Dismember", "Decapitate", "Immortal", "Invincible", "Gore", "Blood", "Bleed",
845
+ "Physics", "Chat", "Console", "Event", "Events", "Hook", "Hooks", "Callback",
846
+ "Register", "UI", "GUI", "Input", "Time", "Camera", "Sound", "Audio",
847
+ "Net", "Network", "Map", "Inventory", "CS", "UnityEngine", "Unity",
848
+ "GameObject", "Component", "Transform", "Application", "System",
849
+ "require", "import", "dofile", "loadfile", "json", "JSON",
850
+ "File", "Path", "Directory",
851
+ "OnSpawn", "OnDamage", "OnDeath", "OnHit", "OnUpdate", "OnStart",
852
+ "AddForce", "SetPosition", "GetPosition", "SetRotation", "FindObject",
853
+ "Instantiate", "Destroy", "GetComponent", "AddComponent",
854
+ "SendMessage", "BroadcastMessage", "Invoke", "InvokeRepeating",
855
+ "Raycast", "OverlapSphere", "SphereCast",
856
+ "Color", "Vector3", "Vector2", "Quaternion", "Mathf",
857
+ "PlayerPrefs", "Resources", "AssetBundle", "SceneManager",
858
+ "RPC", "Photon", "PhotonNetwork",
859
+ }
860
+
861
+ log("")
862
+ log("=== PROBE SPECIFIC NAMES ===")
863
+ for _, name in ipairs(probeNames) do
864
+ local ok3, val = pcall(function() return _G[name] end)
865
+ if ok3 and val ~= nil then
866
+ log("PROBE_HIT|" .. name .. "|" .. type(val))
867
+ end
868
+ end
869
+
870
+ -- Try to discover metatables on common globals
871
+ log("")
872
+ log("=== METATABLE PROBES ===")
873
+ for _, name in ipairs({"Debug", "GB", "Game", "Player", "Entity", "World", "API", "ModAPI", "Mod"}) do
874
+ local ok4, val = pcall(function() return _G[name] end)
875
+ if ok4 and val ~= nil then
876
+ local mt = getmetatable(val)
877
+ if mt then
878
+ log("MT_FOUND|" .. name)
879
+ probeTable(mt, name .. ".__mt", 0)
880
+ end
881
+ end
882
+ end
883
+
884
+ -- Record errors
885
+ if #_errors > 0 then
886
+ log("")
887
+ log("=== ERRORS ===")
888
+ for _, e in ipairs(_errors) do
889
+ log("ERR|" .. e)
890
+ end
891
+ end
892
+
893
+ log("")
894
+ log("=== API DISCOVERY COMPLETE ===")
895
+ log("TOTAL_ENTRIES|" .. #_results)
896
+
897
+ -- Build output string
898
+ local output = table.concat(_results, "\\n")
899
+
900
+ -- Try EVERY possible way to write the output
901
+ local written = false
902
+
903
+ -- Method 1: io.open (standard Lua)
904
+ pcall(function()
905
+ local f = io.open("${modDir.replace(/\\/g, '/')}/dump.txt", "w")
906
+ if f then f:write(output) f:close() written = true end
907
+ end)
908
+
909
+ -- Method 2: Relative path
910
+ if not written then
911
+ pcall(function()
912
+ local f = io.open("dump.txt", "w")
913
+ if f then f:write(output) f:close() written = true end
914
+ end)
915
+ end
916
+
917
+ -- Method 3: os.execute
918
+ if not written then
919
+ pcall(function()
920
+ local f = io.open("${modsDir.replace(/\\/g, '/')}/api_dump.txt", "w")
921
+ if f then f:write(output) f:close() written = true end
922
+ end)
923
+ end
924
+
925
+ -- Method 4: If there's a File API
926
+ if not written then
927
+ pcall(function()
928
+ if File and File.WriteAllText then
929
+ File.WriteAllText("${modDir.replace(/\\/g, '/')}/dump.txt", output)
930
+ written = true
931
+ end
932
+ end)
933
+ end
934
+
935
+ -- Method 5: If there's a WriteFile function
936
+ if not written then
937
+ pcall(function()
938
+ if WriteFile then WriteFile("dump.txt", output) written = true end
939
+ end)
940
+ end
941
+
942
+ -- Always try Debug.Log as fallback - output in chunks
943
+ pcall(function()
944
+ Debug.Log("[APIDisco] Discovery complete: " .. #_results .. " entries, written=" .. tostring(written))
945
+ -- Log first 50 entries to console
946
+ for i = 1, math.min(50, #_results) do
947
+ Debug.Log("[APIDisco] " .. _results[i])
948
+ end
949
+ if #_results > 50 then
950
+ Debug.Log("[APIDisco] ... and " .. (#_results - 50) .. " more entries in dump.txt")
951
+ end
952
+ end)
953
+
954
+ -- Also try print
955
+ pcall(function()
956
+ print("[APIDisco] Discovery complete: " .. #_results .. " entries")
957
+ end)
958
+ `;
959
+ writeFileSync(join(modDir, 'main.lua'), luaScript);
960
+ return JSON.stringify({
961
+ success: true,
962
+ modDir,
963
+ files: ['info.json', 'main.lua'],
964
+ instructions: [
965
+ 'Mod created at: ' + modDir,
966
+ 'Enable it in game: Addons → Mods → ' + modName,
967
+ 'Load into a map/sandbox',
968
+ 'Check for dump.txt in: ' + modDir,
969
+ 'Also check the game console for [APIDisco] messages',
970
+ 'Use gorebox_read_api_dump to parse the results',
971
+ ],
972
+ }, null, 2);
973
+ }),
974
+ nativeTool('gorebox_read_api_dump', 'Read and parse an API discovery dump file generated by gorebox_generate_discovery_mod. Returns structured information about all discovered globals, functions, tables, and callbacks.', {
975
+ type: 'object',
976
+ properties: {
977
+ dump_path: { type: 'string', description: 'Path to the dump.txt file' },
978
+ filter: { type: 'string', description: 'Optional filter string to match entries (case-insensitive)' },
979
+ },
980
+ required: ['dump_path'],
981
+ }, async (args) => {
982
+ const dumpPath = args.dump_path;
983
+ const filter = (args.filter || '').toLowerCase();
984
+ if (!existsSync(dumpPath)) {
985
+ // Search common locations
986
+ const { readdirSync } = await import('fs');
987
+ const { dirname, join } = await import('path');
988
+ const searchDirs = [
989
+ dirname(dumpPath),
990
+ join(dirname(dumpPath), '..'),
991
+ ];
992
+ const found = [];
993
+ for (const dir of searchDirs) {
994
+ try {
995
+ for (const f of readdirSync(dir, { recursive: true })) {
996
+ const fname = String(f);
997
+ if (fname.endsWith('dump.txt') || fname.endsWith('api_dump.txt')) {
998
+ found.push(join(dir, fname));
999
+ }
1000
+ }
1001
+ }
1002
+ catch { /* ignore */ }
1003
+ }
1004
+ return JSON.stringify({
1005
+ error: 'dump.txt not found at: ' + dumpPath,
1006
+ searchedAlso: searchDirs,
1007
+ possibleFiles: found,
1008
+ hint: 'The mod may not have run yet, or file I/O is sandboxed. Check game console for [APIDisco] Debug.Log output instead.',
1009
+ }, null, 2);
1010
+ }
1011
+ const content = readFileSync(dumpPath, 'utf-8');
1012
+ const lines = content.split('\n').filter(l => l.trim());
1013
+ const globalFunctions = [];
1014
+ const globalTables = [];
1015
+ const tableMethods = {};
1016
+ const globalValues = {};
1017
+ const userdataObjects = [];
1018
+ const metatables = {};
1019
+ const probeHits = {};
1020
+ const errors = [];
1021
+ for (const line of lines) {
1022
+ if (filter && !line.toLowerCase().includes(filter))
1023
+ continue;
1024
+ const parts = line.split('|');
1025
+ const tag = parts[0];
1026
+ const name = parts[1] || '';
1027
+ switch (tag) {
1028
+ case 'GFUNC':
1029
+ globalFunctions.push(name);
1030
+ break;
1031
+ case 'GTABLE':
1032
+ globalTables.push(name);
1033
+ break;
1034
+ case 'FUNC': {
1035
+ const dotIdx = name.indexOf('.');
1036
+ if (dotIdx > 0) {
1037
+ const tbl = name.substring(0, dotIdx);
1038
+ if (!tableMethods[tbl])
1039
+ tableMethods[tbl] = [];
1040
+ tableMethods[tbl].push(name.substring(dotIdx + 1));
1041
+ }
1042
+ else {
1043
+ globalFunctions.push(name);
1044
+ }
1045
+ break;
1046
+ }
1047
+ case 'TABLE': break; // already captured by GTABLE
1048
+ case 'STRING':
1049
+ case 'NUMBER':
1050
+ case 'BOOLEAN':
1051
+ case 'GSTRING':
1052
+ case 'GVALUE':
1053
+ globalValues[name] = parts.slice(2).join('|');
1054
+ break;
1055
+ case 'GUSERDATA':
1056
+ case 'USERDATA':
1057
+ userdataObjects.push(name + (parts[2] ? ' = ' + parts[2] : ''));
1058
+ break;
1059
+ case 'MT_FOUND':
1060
+ case ' METATABLE':
1061
+ if (!metatables[name])
1062
+ metatables[name] = [];
1063
+ break;
1064
+ case 'PROBE_HIT':
1065
+ probeHits[name] = parts[2] || 'unknown';
1066
+ break;
1067
+ case 'ERR':
1068
+ errors.push(name);
1069
+ break;
1070
+ }
1071
+ }
1072
+ return JSON.stringify({
1073
+ totalLines: lines.length,
1074
+ filter: filter || '(none)',
1075
+ globalFunctions,
1076
+ globalTables,
1077
+ tableMethods,
1078
+ globalValues,
1079
+ userdataObjects,
1080
+ metatables,
1081
+ probeHits,
1082
+ errors,
1083
+ summary: {
1084
+ functions: globalFunctions.length,
1085
+ tables: globalTables.length,
1086
+ tableMethodGroups: Object.keys(tableMethods).length,
1087
+ values: Object.keys(globalValues).length,
1088
+ userdata: userdataObjects.length,
1089
+ probeHits: Object.keys(probeHits).length,
1090
+ errors: errors.length,
1091
+ },
1092
+ }, null, 2);
1093
+ }),
1094
+ nativeTool('gorebox_generate_mod', 'Generate a GoreBox-compatible Lua mod with proper info.json and main.lua. Creates a ready-to-use mod in the Mods directory.', {
1095
+ type: 'object',
1096
+ properties: {
1097
+ mods_dir: { type: 'string', description: 'Path to the game Mods directory' },
1098
+ mod_name: { type: 'string', description: 'Name for the mod folder' },
1099
+ display_name: { type: 'string', description: 'Display name in the mod browser' },
1100
+ description: { type: 'string', description: 'Mod description' },
1101
+ lua_code: { type: 'string', description: 'The Lua script code for main.lua' },
1102
+ is_plugin: { type: 'boolean', description: 'Whether this is a plugin mod (default true)' },
1103
+ safe_mode: { type: 'boolean', description: 'Whether to run in safe mode (default false)' },
1104
+ version: { type: 'string', description: 'Version string (default 1.0.0)' },
1105
+ },
1106
+ required: ['mods_dir', 'mod_name', 'lua_code'],
1107
+ }, async (args) => {
1108
+ const modsDir = args.mods_dir;
1109
+ const modName = args.mod_name;
1110
+ const displayName = args.display_name || modName;
1111
+ const description = args.description || 'Generated by Universal Game Modder';
1112
+ const luaCode = args.lua_code;
1113
+ const isPlugin = args.is_plugin !== false;
1114
+ const safeMode = args.safe_mode === true;
1115
+ const version = args.version || '1.0.0';
1116
+ const { mkdirSync } = await import('fs');
1117
+ const { join } = await import('path');
1118
+ const modDir = join(modsDir, modName);
1119
+ if (!existsSync(modDir))
1120
+ mkdirSync(modDir, { recursive: true });
1121
+ const infoJson = {
1122
+ name: displayName,
1123
+ description,
1124
+ mainScript: 'main.lua',
1125
+ version,
1126
+ active: true,
1127
+ unloadOnError: true,
1128
+ safeMode,
1129
+ isPlugin,
1130
+ };
1131
+ writeFileSync(join(modDir, 'info.json'), JSON.stringify(infoJson, null, 2));
1132
+ writeFileSync(join(modDir, 'main.lua'), luaCode);
1133
+ return JSON.stringify({
1134
+ success: true,
1135
+ modDir,
1136
+ modName: displayName,
1137
+ files: {
1138
+ 'info.json': JSON.stringify(infoJson, null, 2),
1139
+ 'main.lua': luaCode.length + ' bytes',
1140
+ },
1141
+ instructions: [
1142
+ 'Mod deployed to: ' + modDir,
1143
+ 'Enable in game: Addons → Mods → ' + displayName,
1144
+ 'Refresh mods with the cycle button or restart the map',
1145
+ ],
1146
+ }, null, 2);
1147
+ }),
1148
+ nativeTool('gorebox_list_mods', 'List all installed GoreBox mods with their info.json contents and file structure.', {
1149
+ type: 'object',
1150
+ properties: {
1151
+ mods_dir: { type: 'string', description: 'Path to the game Mods directory' },
1152
+ },
1153
+ required: ['mods_dir'],
1154
+ }, async (args) => {
1155
+ const modsDir = args.mods_dir;
1156
+ const { readdirSync } = await import('fs');
1157
+ const { join } = await import('path');
1158
+ if (!existsSync(modsDir)) {
1159
+ return JSON.stringify({ error: 'Mods directory not found: ' + modsDir });
1160
+ }
1161
+ const mods = [];
1162
+ const entries = readdirSync(modsDir, { withFileTypes: true });
1163
+ for (const entry of entries) {
1164
+ if (!entry.isDirectory())
1165
+ continue;
1166
+ const modDir = join(modsDir, entry.name);
1167
+ const infoPath = join(modDir, 'info.json');
1168
+ const mainPath = join(modDir, 'main.lua');
1169
+ const mod = {
1170
+ folder: entry.name,
1171
+ hasInfoJson: existsSync(infoPath),
1172
+ hasMainLua: existsSync(mainPath),
1173
+ };
1174
+ if (mod.hasInfoJson) {
1175
+ try {
1176
+ mod.info = JSON.parse(readFileSync(infoPath, 'utf-8'));
1177
+ }
1178
+ catch (e) {
1179
+ mod.infoError = 'Failed to parse info.json';
1180
+ }
1181
+ }
1182
+ // List all files in mod folder
1183
+ try {
1184
+ const files = readdirSync(modDir);
1185
+ mod.files = files;
1186
+ }
1187
+ catch { /* ignore */ }
1188
+ // Check for dump files (discovery output)
1189
+ const dumpPath = join(modDir, 'dump.txt');
1190
+ if (existsSync(dumpPath)) {
1191
+ const stat = statSync(dumpPath);
1192
+ mod.hasDump = true;
1193
+ mod.dumpSize = stat.size;
1194
+ }
1195
+ mods.push(mod);
1196
+ }
1197
+ return JSON.stringify({ modsDir, totalMods: mods.length, mods }, null, 2);
1198
+ }),
1199
+ ];
1200
+ }
1201
+ //# sourceMappingURL=index.js.map