269 lines
11 KiB
C#
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);
|
|
}
|
|
}
|