Files
gregExtractor/ModProjectAnalyzer.cs

269 lines
11 KiB
C#

using System.Text.RegularExpressions;
namespace gregExtractor;
public sealed class ModProjectAnalyzer
{
private static readonly Regex HarmonyPatchRegex = new(@"\[(HarmonyPatch|HarmonyPrefix|HarmonyPostfix|HarmonyTranspiler)", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex MelonModInheritanceRegex = new(@":\s*MelonMod\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex GregPluginInheritanceRegex = new(@":\s*gregPluginBase\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex GregEventSubscriptionRegex = new(@"gregEventDispatcher\.On\s*\(\s*\""(?<hook>[^\""\r\n]+)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex GregApiReferenceRegex = new(@"\bgreg\.[A-Za-z0-9_.]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex IdentifierTokenRegex = new(@"[A-Za-z][A-Za-z0-9_]{3,}", RegexOptions.Compiled | RegexOptions.CultureInvariant);
public ModProjectAnalysisResult Analyze(string projectRoot, IReadOnlyList<HookCatalogRow> gregCoreRows)
{
if (string.IsNullOrWhiteSpace(projectRoot) || !Directory.Exists(projectRoot))
throw new DirectoryNotFoundException($"Project path not found: {projectRoot}");
string[] files = Directory.EnumerateFiles(projectRoot, "*.cs", SearchOption.AllDirectories)
.Where(path => !IsIgnoredPath(path))
.ToArray();
int harmonyPatchCount = 0;
int melonModCount = 0;
int gregPluginCount = 0;
int gregSubscriptionCount = 0;
int gregApiRefCount = 0;
var usedHooks = new HashSet<string>(StringComparer.Ordinal);
var keywordTokens = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var fileInsights = new List<ModProjectFileInsightRow>();
foreach (string file in files)
{
string text;
try
{
text = File.ReadAllText(file);
}
catch
{
continue;
}
int fileHarmony = HarmonyPatchRegex.Matches(text).Count;
int fileMelon = MelonModInheritanceRegex.Matches(text).Count;
int fileGregPlugin = GregPluginInheritanceRegex.Matches(text).Count;
int fileGregApi = GregApiReferenceRegex.Matches(text).Count;
harmonyPatchCount += fileHarmony;
melonModCount += fileMelon;
gregPluginCount += fileGregPlugin;
gregApiRefCount += fileGregApi;
MatchCollection subscriptionMatches = GregEventSubscriptionRegex.Matches(text);
int fileSubs = subscriptionMatches.Count;
gregSubscriptionCount += fileSubs;
foreach (Match match in subscriptionMatches)
{
string hook = match.Groups["hook"].Value.Trim();
if (!string.IsNullOrWhiteSpace(hook))
usedHooks.Add(hook);
}
foreach (Match tokenMatch in IdentifierTokenRegex.Matches(text))
{
string token = tokenMatch.Value;
if (!string.IsNullOrWhiteSpace(token) && token.Length >= 4)
keywordTokens.Add(token);
}
bool needsMigration = (fileHarmony > 0 && fileSubs == 0)
|| (fileMelon > 0 && fileGregPlugin == 0)
|| (fileHarmony > fileSubs);
string recommendation = BuildFileRecommendation(fileHarmony, fileMelon, fileGregPlugin, fileSubs, fileGregApi, needsMigration);
fileInsights.Add(new ModProjectFileInsightRow
{
FilePath = file,
HarmonyPatches = fileHarmony,
GregSubscriptions = fileSubs,
GregApiReferences = fileGregApi,
NeedsMigration = needsMigration,
Recommendation = recommendation,
});
}
int totalIntegrationPoints = harmonyPatchCount + melonModCount + gregPluginCount + gregSubscriptionCount;
int migratedPoints = gregPluginCount + gregSubscriptionCount;
int remainingPoints = Math.Max(0, totalIntegrationPoints - migratedPoints);
double migrationPercent = totalIntegrationPoints == 0
? (migratedPoints > 0 ? 100d : 0d)
: Math.Min(100d, 100d * migratedPoints / totalIntegrationPoints);
string[] distinctGregCoreHooks = gregCoreRows
.Select(r => r.GregApiCall)
.Where(h => !string.IsNullOrWhiteSpace(h))
.Distinct(StringComparer.Ordinal)
.ToArray();
int usedGregCoreHooks = usedHooks.Count(h => distinctGregCoreHooks.Contains(h, StringComparer.Ordinal));
int missingGregCoreHooks = Math.Max(0, distinctGregCoreHooks.Length - usedGregCoreHooks);
List<string> suggestions = BuildSuggestedHooks(distinctGregCoreHooks, usedHooks, keywordTokens);
List<MigrationOpportunityRow> opportunities = BuildOpportunities(
harmonyPatchCount,
melonModCount,
gregPluginCount,
gregSubscriptionCount,
usedHooks,
suggestions);
return new ModProjectAnalysisResult
{
ProjectRoot = projectRoot,
CSharpFileCount = files.Length,
HarmonyPatchCount = harmonyPatchCount,
MelonModInheritanceCount = melonModCount,
GregPluginInheritanceCount = gregPluginCount,
GregEventSubscriptionCount = gregSubscriptionCount,
GregApiReferenceCount = gregApiRefCount,
IntegrationPointsTotal = totalIntegrationPoints,
IntegrationPointsMigrated = migratedPoints,
IntegrationPointsRemaining = remainingPoints,
MigrationPercent = migrationPercent,
KnownGregCoreHooks = distinctGregCoreHooks.Length,
UsedGregCoreHooks = usedGregCoreHooks,
MissingGregCoreHooks = missingGregCoreHooks,
UsedHooks = usedHooks.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(),
SuggestedHooks = suggestions,
Opportunities = opportunities,
FileInsights = fileInsights
.OrderByDescending(x => x.NeedsMigration)
.ThenByDescending(x => x.HarmonyPatches)
.ThenBy(x => x.FilePath, StringComparer.OrdinalIgnoreCase)
.ToList(),
};
}
private static string BuildFileRecommendation(int harmonyCount, int melonCount, int gregPluginCount, int subCount, int gregApiCount, bool needsMigration)
{
if (!needsMigration)
return "Looks aligned with greg usage; keep validating hooks after updates.";
if (melonCount > 0 && gregPluginCount == 0)
return "Add gregPluginBase entry and move lifecycle logic out of MelonMod base.";
if (harmonyCount > 0 && subCount == 0)
return "Replace Harmony patches with gregEventDispatcher.On subscriptions where equivalent hooks exist.";
if (harmonyCount > subCount)
return "Prioritize remaining Harmony-heavy paths and migrate to greg hooks incrementally.";
if (gregApiCount == 0)
return "Introduce greg API usage and map game-specific calls to framework events.";
return "Review this file for additional greg hook opportunities.";
}
private static List<string> BuildSuggestedHooks(
IReadOnlyList<string> gregCoreHooks,
IReadOnlySet<string> usedHooks,
IReadOnlySet<string> keywordTokens)
{
var candidates = new List<(string Hook, int Score)>();
foreach (string hook in gregCoreHooks)
{
if (usedHooks.Contains(hook))
continue;
int score = 0;
foreach (string token in keywordTokens)
{
if (hook.Contains(token, StringComparison.OrdinalIgnoreCase))
score += Math.Min(3, token.Length / 4);
}
if (score > 0)
candidates.Add((hook, score));
}
return candidates
.OrderByDescending(x => x.Score)
.ThenBy(x => x.Hook, StringComparer.OrdinalIgnoreCase)
.Select(x => x.Hook)
.Distinct(StringComparer.Ordinal)
.Take(20)
.ToList();
}
private static List<MigrationOpportunityRow> BuildOpportunities(
int harmonyPatchCount,
int melonModCount,
int gregPluginCount,
int gregSubscriptionCount,
IReadOnlySet<string> usedHooks,
IReadOnlyList<string> suggestedHooks)
{
var rows = new List<MigrationOpportunityRow>();
if (melonModCount > 0 && gregPluginCount == 0)
{
rows.Add(new MigrationOpportunityRow
{
Type = "Base Class",
CurrentPattern = "MelonMod inheritance",
SuggestedGregHook = "gregPluginBase",
Suggestion = "Create a greg plugin entry and move shared lifecycle logic into gregPluginBase.",
});
}
if (harmonyPatchCount > 0)
{
rows.Add(new MigrationOpportunityRow
{
Type = "Patching",
CurrentPattern = $"Harmony patches detected: {harmonyPatchCount}",
SuggestedGregHook = "Use matching greg.* events",
Suggestion = "Replace direct Harmony patches with greg hooks where possible to keep compatibility across updates.",
});
}
if (gregSubscriptionCount == 0)
{
rows.Add(new MigrationOpportunityRow
{
Type = "Events",
CurrentPattern = "No gregEventDispatcher.On subscriptions",
SuggestedGregHook = "gregEventDispatcher.On",
Suggestion = "Subscribe to available greg events to reduce game-specific coupling.",
});
}
foreach (string suggested in suggestedHooks.Take(8))
{
rows.Add(new MigrationOpportunityRow
{
Type = "Candidate Hook",
CurrentPattern = "Not yet used",
SuggestedGregHook = suggested,
Suggestion = "Evaluate this hook for replacing direct game calls in your mod code.",
});
}
if (usedHooks.Count > 0)
{
rows.Add(new MigrationOpportunityRow
{
Type = "Validation",
CurrentPattern = $"Used hooks: {usedHooks.Count}",
SuggestedGregHook = "Run plugin tests against updates",
Suggestion = "Validate hook payload handling after each game update and regenerate as needed.",
});
}
return rows;
}
private static bool IsIgnoredPath(string path)
{
string normalized = path.Replace('/', '\\');
return normalized.Contains("\\bin\\", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("\\obj\\", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("\\.git\\", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("\\generated-template\\", StringComparison.OrdinalIgnoreCase);
}
}