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.
- package/ACCEPTABLE_USE.md +49 -0
- package/CHANGELOG.md +119 -0
- package/LICENSE +21 -0
- package/LICENSE-EULA.md +62 -0
- package/README.md +162 -0
- package/VERIFICATION.md +132 -0
- package/dist/backends/child-pool.js +91 -0
- package/dist/backends/mcp-child.js +102 -0
- package/dist/backends/mcp-child.js.map +1 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.js +48 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/engine-detector.d.ts +13 -0
- package/dist/core/engine-detector.js +247 -0
- package/dist/core/engine-detector.js.map +1 -0
- package/dist/core/event-bus.js +18 -0
- package/dist/core/file-classifier.js +394 -0
- package/dist/core/model-db.js +517 -0
- package/dist/core/model-db.js.map +1 -0
- package/dist/core/router.d.ts +6 -0
- package/dist/core/router.js +143 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/session.js +114 -0
- package/dist/decoders/audio-export.js +57 -0
- package/dist/decoders/mesh-export.d.ts +20 -0
- package/dist/decoders/mesh-export.js +174 -0
- package/dist/decoders/mesh-export.js.map +1 -0
- package/dist/decoders/native/disassembler.js +85 -0
- package/dist/decoders/native/disassembler.js.map +1 -0
- package/dist/decoders/png-encode.d.ts +17 -0
- package/dist/decoders/png-encode.js +33 -0
- package/dist/decoders/texture-decode.js +278 -0
- package/dist/decoders/texture-decode.js.map +1 -0
- package/dist/decoders/unity/audioclip.d.ts +55 -0
- package/dist/decoders/unity/audioclip.js +87 -0
- package/dist/decoders/unity/audioclip.js.map +1 -0
- package/dist/decoders/unity/binary-reader.d.ts +45 -0
- package/dist/decoders/unity/binary-reader.js +141 -0
- package/dist/decoders/unity/binary-reader.js.map +1 -0
- package/dist/decoders/unity/mesh.js +198 -0
- package/dist/decoders/unity/mesh.js.map +1 -0
- package/dist/decoders/unity/ress-reader.d.ts +33 -0
- package/dist/decoders/unity/ress-reader.js +82 -0
- package/dist/decoders/unity/ress-reader.js.map +1 -0
- package/dist/decoders/unity/serialized-file.d.ts +65 -0
- package/dist/decoders/unity/serialized-file.js +144 -0
- package/dist/decoders/unity/texture2d.js +185 -0
- package/dist/decoders/unity/vertex-data.d.ts +54 -0
- package/dist/decoders/unity/vertex-data.js +174 -0
- package/dist/decoders/unity/vertex-data.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +23 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +66 -0
- package/dist/tools/decode-tools.d.ts +29 -0
- package/dist/tools/decode-tools.js +441 -0
- package/dist/tools/delegates/jar-editor.d.ts +2 -0
- package/dist/tools/delegates/jar-editor.js +276 -0
- package/dist/tools/delegates/unity-decompiler.d.ts +2 -0
- package/dist/tools/delegates/unity-decompiler.js +291 -0
- package/dist/tools/delegates/unreal-assets.d.ts +2 -0
- package/dist/tools/delegates/unreal-assets.js +122 -0
- package/dist/tools/detection-tools.d.ts +2 -0
- package/dist/tools/detection-tools.js +164 -0
- package/dist/tools/detection-tools.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/native/index.d.ts +2 -0
- package/dist/tools/native/index.js +1201 -0
- package/dist/tools/native-comprehension-tools.js +298 -0
- package/dist/tools/native-comprehension-tools.js.map +1 -0
- package/dist/tools/unpack-tools.d.ts +2 -0
- package/dist/tools/unpack-tools.js +382 -0
- package/dist/tools/workflow-tools.d.ts +2 -0
- package/dist/tools/workflow-tools.js +434 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/path-utils.d.ts +15 -0
- package/dist/utils/path-utils.js +24 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/steam-finder.d.ts +8 -0
- package/dist/utils/steam-finder.js +32 -0
- package/dist/web/web-server.d.ts +1 -0
- package/dist/web/web-server.js +369 -0
- package/licenses/THIRD-PARTY-NOTICES.md +35 -0
- package/llms-install.md +34 -0
- package/package.json +69 -0
- package/server.json +20 -0
- package/setup.ps1 +34 -0
- 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
|