Skip to content

Commit 66c414e

Browse files
committed
Fix parsing of mod metadata, and auto-fill tags
1 parent 8866c12 commit 66c414e

1 file changed

Lines changed: 202 additions & 30 deletions

File tree

electron/main/mod-parser.ts

Lines changed: 202 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,141 @@ function parsePackageJson(data: string): ModPackageInfo | null {
153153
}
154154
}
155155

156+
/**
157+
* Detect tags from ModAPI function calls present in mod.js content
158+
*/
159+
function detectTagsFromModJs(data: string): string[] {
160+
const tagMap: Array<{ pattern: RegExp; tag: string }> = [
161+
// Items
162+
{ pattern: /\baddItem\s*\(/, tag: 'Items' },
163+
{ pattern: /\baddItemToShop\s*\(/, tag: 'Items' },
164+
{ pattern: /\baddItemToGuild\s*\(/, tag: 'Items' },
165+
{ pattern: /\baddItemToAuction\s*\(/, tag: 'Items' },
166+
{ pattern: /\baddItemToFallenStar\s*\(/, tag: 'Items' },
167+
{ pattern: /\baddEnchantment\s*\(/, tag: 'Items' },
168+
{ pattern: /\baddUncutStone\s*\(/, tag: 'Items' },
169+
{ pattern: /\baddManual\s*\(/, tag: 'Items' },
170+
// Crafting
171+
{ pattern: /\baddRecipeToLibrary\s*\(/, tag: 'Crafting' },
172+
{ pattern: /\baddRecipeToResearch\s*\(/, tag: 'Crafting' },
173+
{ pattern: /\baddResearchableRecipe\s*\(/, tag: 'Crafting' },
174+
{ pattern: /\baddCraftingTechnique\s*\(/, tag: 'Crafting' },
175+
{ pattern: /\baddHarmonyType\s*\(/, tag: 'Crafting' },
176+
{ pattern: /\baddCraftingMissionsToLocation\s*\(/, tag: 'Crafting' },
177+
// Characters
178+
{ pattern: /\baddCharacter\s*\(/, tag: 'Characters' },
179+
// Locations
180+
{ pattern: /\baddLocation\s*\(/, tag: 'Locations' },
181+
{ pattern: /\blinkLocations\s*\(/, tag: 'Locations' },
182+
{ pattern: /\bregisterRootLocation\s*\(/, tag: 'Locations' },
183+
{ pattern: /\baddBuildingsToLocation\s*\(/, tag: 'Locations' },
184+
// Combat
185+
{ pattern: /\baddEnemiesToLocation\s*\(/, tag: 'Combat' },
186+
{ pattern: /\baddFallenStar\s*\(/, tag: 'Combat' },
187+
{ pattern: /\baddPuppetType\s*\(/, tag: 'Combat' },
188+
// Techniques
189+
{ pattern: /\baddTechnique\s*\(/, tag: 'Techniques' },
190+
// Cultivation
191+
{ pattern: /\baddBreakthrough\s*\(/, tag: 'Cultivation' },
192+
{ pattern: /\baddDestiny\s*\(/, tag: 'Cultivation' },
193+
// Events
194+
{ pattern: /\baddTriggeredEvent\s*\(/, tag: 'Events' },
195+
{ pattern: /\baddCalendarEvent\s*\(/, tag: 'Events' },
196+
{ pattern: /\baddEventsToLocation\s*\(/, tag: 'Events' },
197+
{ pattern: /\baddExplorationEventsToLocation\s*\(/, tag: 'Events' },
198+
{ pattern: /\baddMapEventsToLocation\s*\(/, tag: 'Events' },
199+
// Quests
200+
{ pattern: /\baddQuest\s*\(/, tag: 'Quests' },
201+
{ pattern: /\baddMissionsToLocation\s*\(/, tag: 'Quests' },
202+
// Audio
203+
{ pattern: /\baddMusic\s*\(/, tag: 'Audio' },
204+
{ pattern: /\baddSfx\s*\(/, tag: 'Audio' },
205+
// Housing
206+
{ pattern: /\baddRoom\s*\(/, tag: 'Housing' },
207+
// Guilds
208+
{ pattern: /\baddGuild\s*\(/, tag: 'Guilds' },
209+
// Relationships
210+
{ pattern: /\baddDualCultivationTechnique\s*\(/, tag: 'Relationships' },
211+
// Cosmetics
212+
{ pattern: /\baddPlayerSprite\s*\(/, tag: 'Cosmetics' },
213+
// UI
214+
{ pattern: /\baddScreen\s*\(/, tag: 'UI' },
215+
{ pattern: /\baddCustomFont\s*\(/, tag: 'UI' },
216+
{ pattern: /\bsetCustomFontFamily\s*\(/, tag: 'UI' },
217+
{ pattern: /\baddThemeOverride\s*\(/, tag: 'UI' },
218+
// Character Creation
219+
{ pattern: /\baddBirthBackground\s*\(/, tag: 'Character Creation' },
220+
{ pattern: /\baddChildBackground\s*\(/, tag: 'Character Creation' },
221+
{ pattern: /\baddTeenBackground\s*\(/, tag: 'Character Creation' },
222+
{ pattern: /\baddAlternativeStart\s*\(/, tag: 'Alternative Start' },
223+
// Translation
224+
{ pattern: /\baddTranslation\s*\(/, tag: 'Translation' },
225+
// Exploration
226+
{ pattern: /\baddMineChamber\s*\(/, tag: 'Exploration' },
227+
{ pattern: /\baddMysticalRegionBlessing\s*\(/, tag: 'Exploration' },
228+
// Farming
229+
{ pattern: /\baddCrop\s*\(/, tag: 'Farming' },
230+
];
231+
232+
const detectedTags = new Set<string>();
233+
for (const { pattern, tag } of tagMap) {
234+
if (pattern.test(data)) {
235+
detectedTags.add(tag);
236+
}
237+
}
238+
239+
const tags = Array.from(detectedTags);
240+
if (tags.length > 0) {
241+
console.log('Auto-detected tags from API usage:', tags);
242+
}
243+
return tags;
244+
}
245+
156246
/**
157247
* Parse mod.js content to extract metadata
158248
*/
159249
function parseModJsContent(data: string): ModPackageInfo | null {
160250
console.log('Parsing mod.js content...');
161251

252+
const detectedTags = detectTagsFromModJs(data);
253+
254+
const mergeDetectedTags = (result: ModPackageInfo | null): ModPackageInfo | null => {
255+
if (detectedTags.length === 0) return result;
256+
// If metadata parsing failed but we have tags, return a minimal result with tags
257+
const base = result ?? {};
258+
const existing = base.tags ?? [];
259+
const merged = Array.from(new Set([...existing, ...detectedTags]));
260+
return { ...base, tags: merged };
261+
};
262+
162263
try {
163-
// Primary method: Look for getMetadata function and extract its return value
164-
const metadataMatch = data.match(
264+
// Try all getMetadata syntax variants
265+
const metadataPatterns = [
266+
// Traditional: getMetadata: function() { return {...} }
165267
/getMetadata\s*:\s*function\s*\(\s*\)\s*\{\s*return\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})/,
166-
);
167-
168-
if (metadataMatch) {
169-
const result = parseMetadataObject(data, metadataMatch);
170-
if (result) return result;
268+
// Method shorthand: getMetadata() { return {...} }
269+
/getMetadata\s*\(\s*\)\s*\{\s*return\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})/,
270+
// Arrow with body: getMetadata: () => { return {...} }
271+
/getMetadata\s*:\s*\(\s*\)\s*=>\s*\{\s*return\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})/,
272+
// Arrow with parenthesized object: getMetadata: () => ({...})
273+
/getMetadata\s*:\s*\(\s*\)\s*=>\s*\(\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})\s*\)/,
274+
// Arrow without parens returning object directly: getMetadata:()=>({...})
275+
/getMetadata\s*:\s*\(\s*\)\s*=>\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})/,
276+
];
277+
278+
for (const pattern of metadataPatterns) {
279+
const metadataMatch = data.match(pattern);
280+
if (metadataMatch) {
281+
const result = parseMetadataObject(data, metadataMatch);
282+
if (result) return mergeDetectedTags(result);
283+
}
171284
}
172285

173286
// Fallback: Try enhanced regex extraction
174-
return extractWithFallbackMethods(data);
287+
return mergeDetectedTags(extractWithFallbackMethods(data));
175288
} catch (parseError) {
176289
console.error('Error parsing mod.js metadata:', parseError);
177-
return extractWithFallbackMethods(data);
290+
return mergeDetectedTags(extractWithFallbackMethods(data));
178291
}
179292
}
180293

@@ -237,35 +350,94 @@ function parseMetadataObject(
237350
*/
238351
function extractWithFallbackMethods(data: string): ModPackageInfo | null {
239352
try {
353+
// Method 0: Webpack compiled inline JSON — find the JSON object literal
354+
// after getMetadata and parse it directly (handles nested objects with brace counting)
355+
const getMetadataPos = data.indexOf('getMetadata');
356+
if (getMetadataPos !== -1) {
357+
const searchArea = data.slice(getMetadataPos, getMetadataPos + 3000);
358+
const jsonObjectStart = searchArea.search(/\{"name"\s*:/);
359+
if (jsonObjectStart !== -1) {
360+
const absoluteStart = getMetadataPos + jsonObjectStart;
361+
let braceCount = 0;
362+
let jsonStr = '';
363+
for (
364+
let i = absoluteStart;
365+
i < Math.min(absoluteStart + 5000, data.length);
366+
i++
367+
) {
368+
const char = data[i];
369+
jsonStr += char;
370+
if (char === '{') braceCount++;
371+
else if (char === '}') {
372+
braceCount--;
373+
if (braceCount === 0) break;
374+
}
375+
}
376+
console.log('DEBUG Method 0: extracted JSON string:', jsonStr.slice(0, 200));
377+
try {
378+
const parsed = JSON.parse(jsonStr);
379+
if (parsed.name) {
380+
console.log('Successfully parsed metadata via inline JSON extraction');
381+
return {
382+
name: parsed.name,
383+
title: formatModName(parsed.title || parsed.name),
384+
description: parsed.description,
385+
version: parsed.version,
386+
author:
387+
typeof parsed.author === 'string'
388+
? parsed.author
389+
: parsed.author?.name,
390+
tags: parsed.tags || parsed.keywords,
391+
};
392+
}
393+
} catch (jsonError) {
394+
console.error('Failed to parse inline JSON object:', jsonError);
395+
}
396+
} else {
397+
console.log('DEBUG Method 0: no {"name": pattern found near getMetadata');
398+
}
399+
}
400+
240401
// Method 1: Handle webpack inline JSON pattern
241402
// Pattern: name: {"name":"value",...}.name
242-
const webpackJsonMatch = data.match(
403+
const webpackJsonPatterns = [
243404
/getMetadata\s*:\s*function\s*\(\s*\)\s*\{\s*return\s*\(\s*\{[^]*?name\s*:\s*(\{[^}]+\})\.name/,
244-
);
245-
246-
if (webpackJsonMatch) {
247-
console.log('Found webpack inline JSON pattern, extracting...');
248-
try {
249-
const inlineJson = JSON.parse(webpackJsonMatch[1]);
250-
return {
251-
name: inlineJson.name,
252-
title: formatModName(inlineJson.title || inlineJson.name),
253-
description: inlineJson.description,
254-
version: inlineJson.version,
255-
author:
256-
typeof inlineJson.author === 'string'
257-
? inlineJson.author
258-
: inlineJson.author?.name,
259-
tags: inlineJson.tags || inlineJson.keywords,
260-
};
261-
} catch (jsonError) {
262-
console.error('Failed to parse webpack inline JSON:', jsonError);
405+
/getMetadata\s*\(\s*\)\s*\{\s*return\s*\(\s*\{[^]*?name\s*:\s*(\{[^}]+\})\.name/,
406+
/getMetadata\s*:\s*\(\s*\)\s*=>\s*\(?\s*\{[^]*?name\s*:\s*(\{[^}]+\})\.name/,
407+
];
408+
409+
for (const pattern of webpackJsonPatterns) {
410+
const webpackJsonMatch = data.match(pattern);
411+
if (webpackJsonMatch) {
412+
console.log('Found webpack inline JSON pattern, extracting...');
413+
try {
414+
const inlineJson = JSON.parse(webpackJsonMatch[1]);
415+
return {
416+
name: inlineJson.name,
417+
title: formatModName(inlineJson.title || inlineJson.name),
418+
description: inlineJson.description,
419+
version: inlineJson.version,
420+
author:
421+
typeof inlineJson.author === 'string'
422+
? inlineJson.author
423+
: inlineJson.author?.name,
424+
tags: inlineJson.tags || inlineJson.keywords,
425+
};
426+
} catch (jsonError) {
427+
console.error('Failed to parse webpack inline JSON:', jsonError);
428+
}
263429
}
264430
}
265431

266432
// Method 2: Look for the exact pattern with individual property extraction
267-
const fullMatch = data.match(
433+
const fullMatchPatterns = [
268434
/getMetadata\s*:\s*function\s*\(\s*\)\s*\{\s*return\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)}/,
435+
/getMetadata\s*\(\s*\)\s*\{\s*return\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)}/,
436+
/getMetadata\s*:\s*\(\s*\)\s*=>\s*\(?\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}?\s*\)?/,
437+
];
438+
const fullMatch = fullMatchPatterns.reduce<RegExpMatchArray | null>(
439+
(found, pattern) => found ?? data.match(pattern),
440+
null,
269441
);
270442

271443
if (fullMatch) {

0 commit comments

Comments
 (0)