From 4f8f826381e4de0f317c8d2b39f55d9b14e1288b Mon Sep 17 00:00:00 2001
From: ifBars
Date: Sat, 24 Jan 2026 03:04:54 -0800
Subject: [PATCH 01/28] feat: BepInEx 5.* Support
---
Abstractions/IConfigManager.cs | 42 ++
Abstractions/IPlatformEnvironment.cs | 46 ++
.../BepInExAssemblyResolverProvider.cs | 45 ++
BepInEx/Adapters/BepInExScanLogger.cs | 25 +
BepInEx/BepInExConfigManager.cs | 142 ++++
BepInEx/BepInExEnvironment.cs | 69 ++
BepInEx/BepInExPluginDisabler.cs | 30 +
BepInEx/BepInExPluginScanner.cs | 57 ++
BepInEx/BepInExReportGenerator.cs | 214 ++++++
BepInEx/MLVScanPatcher.cs | 120 ++++
Core.cs | 19 +-
MLVScan.csproj | 679 ++++--------------
.../Adapters}/GameAssemblyResolverProvider.cs | 0
.../Adapters}/MelonScanLogger.cs | 0
.../MelonConfigManager.cs | 18 +-
MelonLoader/MelonEnvironment.cs | 88 +++
MelonLoader/MelonPluginDisabler.cs | 31 +
MelonLoader/MelonPluginScanner.cs | 107 +++
Models/DisabledModInfo.cs | 16 -
PlatformConstants.cs | 17 +-
Services/DeveloperReportGenerator.cs | 61 +-
Services/HashUtility.cs | 65 ++
Services/IlDumpService.cs | 64 +-
Services/ModDisabler.cs | 68 --
Services/ModScanner.cs | 169 -----
Services/PluginDisablerBase.cs | 139 ++++
Services/PluginScannerBase.cs | 139 ++++
Services/PromptGeneratorService.cs | 5 +-
Services/ServiceFactory.cs | 43 +-
29 files changed, 1639 insertions(+), 879 deletions(-)
create mode 100644 Abstractions/IConfigManager.cs
create mode 100644 Abstractions/IPlatformEnvironment.cs
create mode 100644 BepInEx/Adapters/BepInExAssemblyResolverProvider.cs
create mode 100644 BepInEx/Adapters/BepInExScanLogger.cs
create mode 100644 BepInEx/BepInExConfigManager.cs
create mode 100644 BepInEx/BepInExEnvironment.cs
create mode 100644 BepInEx/BepInExPluginDisabler.cs
create mode 100644 BepInEx/BepInExPluginScanner.cs
create mode 100644 BepInEx/BepInExReportGenerator.cs
create mode 100644 BepInEx/MLVScanPatcher.cs
rename {Adapters => MelonLoader/Adapters}/GameAssemblyResolverProvider.cs (100%)
rename {Adapters => MelonLoader/Adapters}/MelonScanLogger.cs (100%)
rename Services/ConfigManager.cs => MelonLoader/MelonConfigManager.cs (94%)
create mode 100644 MelonLoader/MelonEnvironment.cs
create mode 100644 MelonLoader/MelonPluginDisabler.cs
create mode 100644 MelonLoader/MelonPluginScanner.cs
delete mode 100644 Models/DisabledModInfo.cs
create mode 100644 Services/HashUtility.cs
delete mode 100644 Services/ModDisabler.cs
delete mode 100644 Services/ModScanner.cs
create mode 100644 Services/PluginDisablerBase.cs
create mode 100644 Services/PluginScannerBase.cs
diff --git a/Abstractions/IConfigManager.cs b/Abstractions/IConfigManager.cs
new file mode 100644
index 0000000..15d55ce
--- /dev/null
+++ b/Abstractions/IConfigManager.cs
@@ -0,0 +1,42 @@
+using MLVScan.Models;
+
+namespace MLVScan.Abstractions
+{
+ ///
+ /// Abstraction for configuration management across different mod platforms.
+ /// MelonLoader uses MelonPreferences (INI-based), BepInEx uses JSON files.
+ ///
+ public interface IConfigManager
+ {
+ ///
+ /// Gets the current configuration.
+ ///
+ ScanConfig Config { get; }
+
+ ///
+ /// Loads configuration from persistent storage.
+ /// Creates default configuration if none exists.
+ ///
+ ScanConfig LoadConfig();
+
+ ///
+ /// Saves configuration to persistent storage.
+ ///
+ void SaveConfig(ScanConfig config);
+
+ ///
+ /// Checks if a file hash is in the whitelist.
+ ///
+ bool IsHashWhitelisted(string hash);
+
+ ///
+ /// Gets all whitelisted hashes.
+ ///
+ string[] GetWhitelistedHashes();
+
+ ///
+ /// Sets the whitelisted hashes (normalizes and deduplicates).
+ ///
+ void SetWhitelistedHashes(string[] hashes);
+ }
+}
diff --git a/Abstractions/IPlatformEnvironment.cs b/Abstractions/IPlatformEnvironment.cs
new file mode 100644
index 0000000..bf86c04
--- /dev/null
+++ b/Abstractions/IPlatformEnvironment.cs
@@ -0,0 +1,46 @@
+namespace MLVScan.Abstractions
+{
+ ///
+ /// Abstraction for platform-specific paths and environment info.
+ /// MelonLoader uses MelonEnvironment, BepInEx uses BepInEx.Paths.
+ ///
+ public interface IPlatformEnvironment
+ {
+ ///
+ /// Gets the game's root directory.
+ ///
+ string GameRootDirectory { get; }
+
+ ///
+ /// Gets the directory where plugins/mods are stored.
+ /// MelonLoader: Mods/ and Plugins/
+ /// BepInEx: BepInEx/plugins/
+ ///
+ string[] PluginDirectories { get; }
+
+ ///
+ /// Gets the directory for MLVScan's own data (reports, disabled info, etc.).
+ ///
+ string DataDirectory { get; }
+
+ ///
+ /// Gets the directory for scan reports.
+ ///
+ string ReportsDirectory { get; }
+
+ ///
+ /// Gets the managed assemblies directory (Unity DLLs, game code).
+ ///
+ string ManagedDirectory { get; }
+
+ ///
+ /// Gets the path to the MLVScan assembly itself (for self-exclusion).
+ ///
+ string SelfAssemblyPath { get; }
+
+ ///
+ /// Gets the platform name for display/logging.
+ ///
+ string PlatformName { get; }
+ }
+}
diff --git a/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs b/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs
new file mode 100644
index 0000000..a686764
--- /dev/null
+++ b/BepInEx/Adapters/BepInExAssemblyResolverProvider.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+using Mono.Cecil;
+
+namespace MLVScan.BepInEx.Adapters
+{
+ ///
+ /// Provides assembly resolution for scanning in BepInEx context.
+ /// Adds all relevant BepInEx and game directories to search paths.
+ ///
+ public class BepInExAssemblyResolverProvider : IAssemblyResolverProvider
+ {
+ public IAssemblyResolver CreateResolver()
+ {
+ var resolver = new DefaultAssemblyResolver();
+
+ try
+ {
+ // Game's managed assemblies (Unity DLLs, game code)
+ if (Directory.Exists(Paths.ManagedPath))
+ resolver.AddSearchDirectory(Paths.ManagedPath);
+
+ // BepInEx core assemblies
+ if (Directory.Exists(Paths.BepInExAssemblyDirectory))
+ resolver.AddSearchDirectory(Paths.BepInExAssemblyDirectory);
+
+ // Plugin directory (for plugin-to-plugin references)
+ if (Directory.Exists(Paths.PluginPath))
+ resolver.AddSearchDirectory(Paths.PluginPath);
+
+ // Patcher directory (where we are running from)
+ if (Directory.Exists(Paths.PatcherPluginPath))
+ resolver.AddSearchDirectory(Paths.PatcherPluginPath);
+ }
+ catch (Exception)
+ {
+ // If path resolution fails, use default resolver behavior
+ }
+
+ return resolver;
+ }
+ }
+}
diff --git a/BepInEx/Adapters/BepInExScanLogger.cs b/BepInEx/Adapters/BepInExScanLogger.cs
new file mode 100644
index 0000000..0e21bfd
--- /dev/null
+++ b/BepInEx/Adapters/BepInExScanLogger.cs
@@ -0,0 +1,25 @@
+using System;
+using BepInEx.Logging;
+using MLVScan.Abstractions;
+
+namespace MLVScan.BepInEx.Adapters
+{
+ ///
+ /// Adapter that wraps BepInEx's logging system to implement IScanLogger.
+ ///
+ public class BepInExScanLogger : IScanLogger
+ {
+ private readonly ManualLogSource _logger;
+
+ public BepInExScanLogger(ManualLogSource logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void Debug(string message) => _logger.LogDebug(message);
+ public void Info(string message) => _logger.LogInfo(message);
+ public void Warning(string message) => _logger.LogWarning(message);
+ public void Error(string message) => _logger.LogError(message);
+ public void Error(string message, Exception exception) => _logger.LogError($"{message}: {exception}");
+ }
+}
diff --git a/BepInEx/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs
new file mode 100644
index 0000000..913aedf
--- /dev/null
+++ b/BepInEx/BepInExConfigManager.cs
@@ -0,0 +1,142 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using BepInEx;
+using BepInEx.Logging;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of IConfigManager using JSON file storage.
+ /// Required because BepInEx's ConfigFile isn't available at preload time.
+ ///
+ public class BepInExConfigManager : IConfigManager
+ {
+ private readonly ManualLogSource _logger;
+ private readonly string[] _defaultWhitelistedHashes;
+ private readonly string _configPath;
+ private ScanConfig _config;
+
+ // JSON serialization options
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _defaultWhitelistedHashes = defaultWhitelistedHashes ?? Array.Empty();
+
+ // Config stored alongside other BepInEx configs
+ _configPath = Path.Combine(Paths.ConfigPath, "MLVScan.json");
+ _config = new ScanConfig();
+ }
+
+ public ScanConfig Config => _config;
+
+ public ScanConfig LoadConfig()
+ {
+ try
+ {
+ if (File.Exists(_configPath))
+ {
+ var json = File.ReadAllText(_configPath);
+ var loaded = JsonSerializer.Deserialize(json, JsonOptions);
+
+ if (loaded != null)
+ {
+ _config = loaded;
+ _logger.LogInfo("Configuration loaded from MLVScan.json");
+ return _config;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Failed to load config, using defaults: {ex.Message}");
+ }
+
+ // Create default config
+ _config = CreateDefaultConfig();
+ SaveConfig(_config);
+ _logger.LogInfo("Created default MLVScan.json configuration");
+
+ return _config;
+ }
+
+ private ScanConfig CreateDefaultConfig()
+ {
+ return new ScanConfig
+ {
+ EnableAutoScan = true,
+ EnableAutoDisable = true,
+ MinSeverityForDisable = Severity.Medium,
+ ScanDirectories = new[] { "plugins" },
+ SuspiciousThreshold = 1,
+ WhitelistedHashes = _defaultWhitelistedHashes,
+ DumpFullIlReports = false,
+ DeveloperMode = false
+ };
+ }
+
+ public void SaveConfig(ScanConfig config)
+ {
+ try
+ {
+ // Ensure config directory exists
+ var configDir = Path.GetDirectoryName(_configPath);
+ if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+
+ var json = JsonSerializer.Serialize(config, JsonOptions);
+
+ File.WriteAllText(_configPath, json);
+ _config = config;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to save config: {ex.Message}");
+ }
+ }
+
+ public bool IsHashWhitelisted(string hash)
+ {
+ if (string.IsNullOrWhiteSpace(hash))
+ return false;
+
+ return _config.WhitelistedHashes.Contains(
+ hash.ToLowerInvariant(),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ public string[] GetWhitelistedHashes()
+ {
+ return _config.WhitelistedHashes;
+ }
+
+ public void SetWhitelistedHashes(string[] hashes)
+ {
+ if (hashes == null)
+ return;
+
+ var normalizedHashes = hashes
+ .Where(h => !string.IsNullOrWhiteSpace(h))
+ .Select(h => h.ToLowerInvariant())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ _config.WhitelistedHashes = normalizedHashes;
+ SaveConfig(_config);
+ _logger.LogInfo($"Updated whitelist with {normalizedHashes.Length} hash(es)");
+ }
+ }
+}
diff --git a/BepInEx/BepInExEnvironment.cs b/BepInEx/BepInExEnvironment.cs
new file mode 100644
index 0000000..c91b6f4
--- /dev/null
+++ b/BepInEx/BepInExEnvironment.cs
@@ -0,0 +1,69 @@
+using System;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of IPlatformEnvironment.
+ /// Uses BepInEx.Paths for path resolution.
+ ///
+ public class BepInExPlatformEnvironment : IPlatformEnvironment
+ {
+ private readonly string _dataDir;
+ private readonly string _reportsDir;
+
+ public BepInExPlatformEnvironment()
+ {
+ _dataDir = Path.Combine(Paths.BepInExRootPath, "MLVScan");
+ _reportsDir = Path.Combine(_dataDir, "Reports");
+ }
+
+ public string GameRootDirectory => Paths.GameRootPath;
+
+ public string[] PluginDirectories => new[]
+ {
+ Paths.PluginPath
+ };
+
+ public string DataDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_dataDir))
+ Directory.CreateDirectory(_dataDir);
+ return _dataDir;
+ }
+ }
+
+ public string ReportsDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_reportsDir))
+ Directory.CreateDirectory(_reportsDir);
+ return _reportsDir;
+ }
+ }
+
+ public string ManagedDirectory => Paths.ManagedPath;
+
+ public string SelfAssemblyPath
+ {
+ get
+ {
+ try
+ {
+ return typeof(BepInExPlatformEnvironment).Assembly.Location;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+ }
+
+ public string PlatformName => "BepInEx";
+ }
+}
diff --git a/BepInEx/BepInExPluginDisabler.cs b/BepInEx/BepInExPluginDisabler.cs
new file mode 100644
index 0000000..0539ad9
--- /dev/null
+++ b/BepInEx/BepInExPluginDisabler.cs
@@ -0,0 +1,30 @@
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of plugin disabler.
+ /// Uses ".blocked" extension (BepInEx convention).
+ ///
+ public class BepInExPluginDisabler : PluginDisablerBase
+ {
+ private const string BepInExBlockedExtension = ".blocked";
+
+ public BepInExPluginDisabler(IScanLogger logger, ScanConfig config)
+ : base(logger, config)
+ {
+ }
+
+ protected override string DisabledExtension => BepInExBlockedExtension;
+
+ ///
+ /// BepInEx uses append style (plugin.dll -> plugin.dll.blocked).
+ ///
+ protected override string GetDisabledPath(string originalPath)
+ {
+ return originalPath + BepInExBlockedExtension;
+ }
+ }
+}
diff --git a/BepInEx/BepInExPluginScanner.cs b/BepInEx/BepInExPluginScanner.cs
new file mode 100644
index 0000000..63e4739
--- /dev/null
+++ b/BepInEx/BepInExPluginScanner.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using BepInEx;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx implementation of plugin scanner.
+ /// Scans BepInEx/plugins/ directory.
+ ///
+ public class BepInExPluginScanner : PluginScannerBase
+ {
+ private readonly BepInExPlatformEnvironment _environment;
+
+ public BepInExPluginScanner(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager,
+ BepInExPlatformEnvironment environment)
+ : base(logger, resolverProvider, config, configManager)
+ {
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
+ }
+
+ protected override IEnumerable GetScanDirectories()
+ {
+ // BepInEx plugins directory
+ if (Directory.Exists(Paths.PluginPath))
+ {
+ yield return Paths.PluginPath;
+ }
+ }
+
+ protected override bool IsSelfAssembly(string filePath)
+ {
+ try
+ {
+ var selfPath = _environment.SelfAssemblyPath;
+ if (string.IsNullOrEmpty(selfPath))
+ return false;
+
+ return Path.GetFullPath(filePath).Equals(
+ Path.GetFullPath(selfPath),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/BepInEx/BepInExReportGenerator.cs b/BepInEx/BepInExReportGenerator.cs
new file mode 100644
index 0000000..da77b07
--- /dev/null
+++ b/BepInEx/BepInExReportGenerator.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BepInEx;
+using BepInEx.Logging;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// Generates detailed reports for blocked plugins.
+ ///
+ public class BepInExReportGenerator
+ {
+ private readonly ManualLogSource _logger;
+ private readonly ScanConfig _config;
+ private readonly string _reportDirectory;
+
+ public BepInExReportGenerator(ManualLogSource logger, ScanConfig config)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+
+ // Reports go to BepInEx/MLVScan/Reports/
+ _reportDirectory = Path.Combine(Paths.BepInExRootPath, "MLVScan", "Reports");
+ }
+
+ public void GenerateReports(
+ List disabledPlugins,
+ Dictionary> scanResults)
+ {
+ EnsureReportDirectoryExists();
+
+ foreach (var pluginInfo in disabledPlugins)
+ {
+ if (!scanResults.TryGetValue(pluginInfo.OriginalPath, out var findings))
+ continue;
+
+ var pluginName = Path.GetFileName(pluginInfo.OriginalPath);
+
+ // Log to console
+ LogConsoleReport(pluginName, pluginInfo.FileHash, findings);
+
+ // Generate file report
+ GenerateFileReport(pluginName, pluginInfo, findings);
+ }
+ }
+
+ private void LogConsoleReport(string pluginName, string hash, List findings)
+ {
+ _logger.LogWarning(new string('=', 50));
+ _logger.LogWarning($"BLOCKED PLUGIN: {pluginName}");
+ _logger.LogInfo($"SHA256: {hash}");
+ _logger.LogInfo($"Suspicious patterns: {findings.Count}");
+
+ var grouped = findings
+ .GroupBy(f => f.Severity)
+ .OrderByDescending(g => (int)g.Key);
+
+ foreach (var group in grouped)
+ {
+ _logger.LogInfo($" {group.Key}: {group.Count()} issue(s)");
+ }
+
+ // Show top 3 findings
+ var topFindings = findings
+ .OrderByDescending(f => f.Severity)
+ .Take(3);
+
+ foreach (var finding in topFindings)
+ {
+ _logger.LogWarning($"[{finding.Severity}] {finding.Description}");
+ _logger.LogInfo($" Location: {finding.Location}");
+ }
+
+ if (findings.Count > 3)
+ {
+ _logger.LogInfo($" ... and {findings.Count - 3} more findings");
+ }
+
+ DisplaySecurityNotice(pluginName);
+ }
+
+ private void DisplaySecurityNotice(string pluginName)
+ {
+ _logger.LogWarning("--- SECURITY NOTICE ---");
+ _logger.LogInfo($"MLVScan blocked {pluginName} before it could execute.");
+ _logger.LogInfo("If this is your first time with this plugin, you are likely safe.");
+ _logger.LogInfo("If you've used it before, consider running a malware scan.");
+ _logger.LogInfo("");
+ _logger.LogInfo("To whitelist a false positive:");
+ _logger.LogInfo(" Add the SHA256 hash to BepInEx/config/MLVScan.json");
+ _logger.LogInfo("");
+ _logger.LogInfo("Resources:");
+ _logger.LogInfo(" Malwarebytes: https://www.malwarebytes.com/");
+ _logger.LogInfo(" Community: https://discord.gg/UD4K4chKak");
+ }
+
+ private void GenerateFileReport(
+ string pluginName,
+ DisabledPluginInfo pluginInfo,
+ List findings)
+ {
+ try
+ {
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var reportPath = Path.Combine(_reportDirectory, $"{pluginName}_{timestamp}.report.txt");
+
+ var sb = new StringBuilder();
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("MLVScan Security Report (BepInEx)");
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine($"Generated: {DateTime.Now}");
+ sb.AppendLine($"Plugin: {pluginName}");
+ sb.AppendLine($"SHA256: {pluginInfo.FileHash}");
+ sb.AppendLine($"Original Path: {pluginInfo.OriginalPath}");
+ sb.AppendLine($"Blocked Path: {pluginInfo.DisabledPath}");
+ sb.AppendLine($"Total Findings: {findings.Count}");
+ sb.AppendLine();
+
+ // Severity breakdown
+ sb.AppendLine("Severity Breakdown:");
+ foreach (var group in findings.GroupBy(f => f.Severity).OrderByDescending(g => (int)g.Key))
+ {
+ sb.AppendLine($" {group.Key}: {group.Count()}");
+ }
+ sb.AppendLine();
+
+ // Detailed findings
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("DETAILED FINDINGS");
+ sb.AppendLine(new string('=', 60));
+
+ var groupedByDescription = findings.GroupBy(f => f.Description);
+ foreach (var group in groupedByDescription)
+ {
+ var first = group.First();
+ sb.AppendLine();
+ sb.AppendLine($"[{first.Severity}] {first.Description}");
+ sb.AppendLine($"Occurrences: {group.Count()}");
+
+ if (_config.DeveloperMode && first.DeveloperGuidance != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("Developer Guidance:");
+ sb.AppendLine($" {first.DeveloperGuidance.Remediation}");
+
+ if (first.DeveloperGuidance.AlternativeApis?.Length > 0)
+ {
+ sb.AppendLine($" Alternatives: {string.Join(", ", first.DeveloperGuidance.AlternativeApis)}");
+ }
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("Locations:");
+ foreach (var finding in group.Take(10))
+ {
+ sb.AppendLine($" - {finding.Location}");
+ if (!string.IsNullOrEmpty(finding.CodeSnippet))
+ {
+ foreach (var line in finding.CodeSnippet.Split('\n').Take(5))
+ {
+ sb.AppendLine($" {line.Trim()}");
+ }
+ }
+ }
+
+ if (group.Count() > 10)
+ {
+ sb.AppendLine($" ... and {group.Count() - 10} more");
+ }
+ }
+
+ // Security notice
+ sb.AppendLine();
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("SECURITY RECOMMENDATIONS");
+ sb.AppendLine(new string('=', 60));
+ sb.AppendLine("1. Verify with the modding community if this is a known mod");
+ sb.AppendLine("2. Run a full system scan with Malwarebytes or similar");
+ sb.AppendLine("3. Check the Discord for guidance: https://discord.gg/UD4K4chKak");
+ sb.AppendLine();
+ sb.AppendLine("To whitelist (if false positive):");
+ sb.AppendLine($" Add this hash to BepInEx/config/MLVScan.json:");
+ sb.AppendLine($" \"{pluginInfo.FileHash}\"");
+
+ File.WriteAllText(reportPath, sb.ToString());
+ _logger.LogInfo($"Report saved: {reportPath}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to generate report: {ex.Message}");
+ }
+ }
+
+ private void EnsureReportDirectoryExists()
+ {
+ try
+ {
+ if (!Directory.Exists(_reportDirectory))
+ {
+ Directory.CreateDirectory(_reportDirectory);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to create report directory: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/BepInEx/MLVScanPatcher.cs b/BepInEx/MLVScanPatcher.cs
new file mode 100644
index 0000000..e5933d7
--- /dev/null
+++ b/BepInEx/MLVScanPatcher.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using BepInEx;
+using BepInEx.Logging;
+using Mono.Cecil;
+using MLVScan.BepInEx.Adapters;
+
+namespace MLVScan.BepInEx
+{
+ ///
+ /// BepInEx preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ public static class MLVScanPatcher
+ {
+ private static ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Required: Declares which assemblies to patch.
+ /// Empty = we don't patch game assemblies, just use Initialize() as entry point.
+ ///
+ public static IEnumerable TargetDLLs { get; } = Array.Empty();
+
+ ///
+ /// Required: Patching method (no-op - we don't modify game code).
+ ///
+ public static void Patch(AssemblyDefinition assembly) { }
+
+ ///
+ /// Called before patching - our main entry point.
+ /// Runs BEFORE the chainloader loads any plugins.
+ ///
+ public static void Initialize()
+ {
+ _logger = Logger.CreateLogSource("MLVScan");
+
+ try
+ {
+ _logger.LogInfo("MLVScan preloader patcher initializing...");
+ _logger.LogInfo($"Plugin directory: {Paths.PluginPath}");
+
+ // Create platform environment
+ var environment = new BepInExPlatformEnvironment();
+
+ // Load or create configuration
+ var configManager = new BepInExConfigManager(_logger, DefaultWhitelistedHashes);
+ var config = configManager.LoadConfig();
+
+ // Create adapters
+ var scanLogger = new BepInExScanLogger(_logger);
+ var resolverProvider = new BepInExAssemblyResolverProvider();
+
+ // Create scanner and disabler
+ var pluginScanner = new BepInExPluginScanner(
+ scanLogger,
+ resolverProvider,
+ config,
+ configManager,
+ environment);
+
+ var pluginDisabler = new BepInExPluginDisabler(scanLogger, config);
+ var reportGenerator = new BepInExReportGenerator(_logger, config);
+
+ // Scan all plugins
+ var scanResults = pluginScanner.ScanAllPlugins();
+
+ if (scanResults.Count > 0)
+ {
+ // Disable suspicious plugins
+ var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+
+ // Generate reports for disabled plugins
+ if (disabledPlugins.Count > 0)
+ {
+ reportGenerator.GenerateReports(disabledPlugins, scanResults);
+
+ _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
+ _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+ }
+ }
+ else
+ {
+ _logger.LogInfo("No suspicious plugins detected.");
+ }
+
+ _logger.LogInfo("MLVScan preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called after all patching and assembly loading is complete.
+ ///
+ public static void Finish()
+ {
+ // Optional: cleanup, final summary logging
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/Core.cs b/Core.cs
index 2f8a125..e497758 100644
--- a/Core.cs
+++ b/Core.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using MelonLoader;
+using MLVScan.MelonLoader;
using MLVScan.Models;
using MLVScan.Services;
@@ -15,9 +16,10 @@ namespace MLVScan
public class Core : MelonPlugin
{
private ServiceFactory _serviceFactory;
- private ConfigManager _configManager;
- private ModScanner _modScanner;
- private ModDisabler _modDisabler;
+ private MelonConfigManager _configManager;
+ private MelonPlatformEnvironment _environment;
+ private MelonPluginScanner _pluginScanner;
+ private MelonPluginDisabler _pluginDisabler;
private IlDumpService _ilDumpService;
private DeveloperReportGenerator _developerReportGenerator;
private bool _initialized = false;
@@ -40,11 +42,12 @@ public override void OnEarlyInitializeMelon()
_serviceFactory = new ServiceFactory(LoggerInstance);
_configManager = _serviceFactory.CreateConfigManager();
+ _environment = _serviceFactory.CreateEnvironment();
InitializeDefaultWhitelist();
- _modScanner = _serviceFactory.CreateModScanner();
- _modDisabler = _serviceFactory.CreateModDisabler();
+ _pluginScanner = _serviceFactory.CreatePluginScanner();
+ _pluginDisabler = _serviceFactory.CreatePluginDisabler();
_ilDumpService = _serviceFactory.CreateIlDumpService();
_developerReportGenerator = _serviceFactory.CreateDeveloperReportGenerator();
@@ -103,7 +106,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
}
LoggerInstance.Msg("Scanning for suspicious mods...");
- var scanResults = _modScanner.ScanAllMods(force);
+ var scanResults = _pluginScanner.ScanAllPlugins(force);
var filteredResults = scanResults
.Where(kv => kv.Value.Count > 0 && kv.Value.Any(f => f.Location != "Assembly scanning"))
@@ -111,7 +114,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
if (filteredResults.Count > 0)
{
- var disabledMods = _modDisabler.DisableSuspiciousMods(filteredResults, force);
+ var disabledMods = _pluginDisabler.DisableSuspiciousPlugins(filteredResults, force);
var disabledCount = disabledMods.Count;
LoggerInstance.Msg($"Disabled {disabledCount} suspicious mods");
@@ -135,7 +138,7 @@ public Dictionary> ScanAndDisableMods(bool force = fal
}
}
- private void GenerateDetailedReports(List disabledMods, Dictionary> scanResults)
+ private void GenerateDetailedReports(List disabledMods, Dictionary> scanResults)
{
var isDeveloperMode = _configManager?.Config?.DeveloperMode ?? false;
diff --git a/MLVScan.csproj b/MLVScan.csproj
index 651e68c..fb6aefc 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -1,565 +1,178 @@
+
+
+
netstandard2.1
+ latest
enable
disable
MLVScan
- default
false
1.6.1
1.6.1
en-US
+ True
+ false
+ MelonLoader;BepInEx
+
+
+
+
+
+
+ MELONLOADER
MLVScan.MelonLoader
- latest
- enable
+
+
+
+
+
+ BEPINEX
+ MLVScan.BepInEx
+
+
+
+
+
+
+
+
+
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Accessibility.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\AstarPathfindingProject.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Autodesk.Fbx.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\com.rlabrecque.steamworks.net.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\HSVPicker.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\LokoSolo.PinchableScrollRect.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.Data.Sqlite.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.Posix.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Mono.WebBrowser.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Newtonsoft.Json.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\RuntimePreviewGenerator.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\StylizedWaterForURP.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.AI.Navigation.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Burst.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Burst.Unsafe.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Collections.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Collections.LowLevel.ILSupport.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Formats.Fbx.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.ForUI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.InputSystem.RebindingUI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Mathematics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.MemoryProfiler.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Postprocessing.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.ProGrids.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipeline.Universal.ShaderLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Core.ShaderLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Config.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Runtime.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.RenderPipelines.Universal.Shaders.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Analytics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.CloudDiagnostics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Analytics.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Configuration.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Device.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Environments.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Environments.Internal.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Internal.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Networking.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Registration.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Scheduler.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Telemetry.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Services.Core.Threading.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.TerrainTools.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.TextMeshPro.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\Unity.Timeline.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AccessibilityModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AndroidJNIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AnimationModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ARModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AssetBundleModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.AudioModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClothModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClusterInputModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ClusterRendererModule.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MelonLoaderPath)\MelonLoader.dll
false
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ContentLoadModule.dll
+
+ $(MelonLoaderPath)\0Harmony.dll
false
+
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.CoreModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.CrashReportingModule.dll
+ $(GameManagedPath)\UnityEngine.CoreModule.dll
false
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.DirectorModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.DSPGraphModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GameCenterModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.GridModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.HotReloadModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ImageConversionModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.IMGUIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.InputLegacyModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.InputModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.JSONSerializeModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.LocalizationModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.NVIDIAModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ParticleSystemModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PerformanceReportingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.Physics2DModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PhysicsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ProfilerModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.PropertiesModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.ScreenCaptureModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SharedInternalsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SpriteMaskModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SpriteShapeModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.StreamingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SubstanceModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.SubsystemsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TerrainModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TerrainPhysicsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextCoreFontEngineModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextCoreTextEngineModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TextRenderingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TilemapModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.TLSModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UI.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UIElementsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UIModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UmbraModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityAnalyticsCommonModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityAnalyticsModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityConnectModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityCurlModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityTestProtocolModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestAssetBundleModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestTextureModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VehiclesModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VFXModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VideoModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VirtualTexturingModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.VRModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.WindModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\UnityEngine.XRModule.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.Nature.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.Packages.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\Schedule I_Data\Managed\VisualDesignCafe.ShaderX.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\MelonLoader.dll
- false
-
-
- D:\SteamLibrary\steamapps\common\Schedule I_alternate\MelonLoader\net35\0Harmony.dll
- false
+
+
+
+
+
+
+
+
+
+
+ $(BepInExCorePath)\BepInEx.dll
+
+
+ $(BepInExCorePath)\0Harmony.dll
-
- True
- false
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/Adapters/GameAssemblyResolverProvider.cs b/MelonLoader/Adapters/GameAssemblyResolverProvider.cs
similarity index 100%
rename from Adapters/GameAssemblyResolverProvider.cs
rename to MelonLoader/Adapters/GameAssemblyResolverProvider.cs
diff --git a/Adapters/MelonScanLogger.cs b/MelonLoader/Adapters/MelonScanLogger.cs
similarity index 100%
rename from Adapters/MelonScanLogger.cs
rename to MelonLoader/Adapters/MelonScanLogger.cs
diff --git a/Services/ConfigManager.cs b/MelonLoader/MelonConfigManager.cs
similarity index 94%
rename from Services/ConfigManager.cs
rename to MelonLoader/MelonConfigManager.cs
index c4887f1..cbb864d 100644
--- a/Services/ConfigManager.cs
+++ b/MelonLoader/MelonConfigManager.cs
@@ -1,9 +1,15 @@
+using System;
+using System.Linq;
using MelonLoader;
+using MLVScan.Abstractions;
using MLVScan.Models;
-namespace MLVScan.Services
+namespace MLVScan.MelonLoader
{
- public class ConfigManager
+ ///
+ /// MelonLoader implementation of IConfigManager using MelonPreferences.
+ ///
+ public class MelonConfigManager : IConfigManager
{
private readonly MelonLogger.Instance _logger;
private readonly MelonPreferences_Category _category;
@@ -17,7 +23,7 @@ public class ConfigManager
private readonly MelonPreferences_Entry _dumpFullIlReports;
private readonly MelonPreferences_Entry _developerMode;
- public ConfigManager(MelonLogger.Instance logger)
+ public MelonConfigManager(MelonLogger.Instance logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -72,6 +78,12 @@ public ConfigManager(MelonLogger.Instance logger)
public ScanConfig Config { get; private set; }
+ public ScanConfig LoadConfig()
+ {
+ UpdateConfigFromPreferences();
+ return Config;
+ }
+
private void OnConfigChanged(T oldValue, T newValue)
{
UpdateConfigFromPreferences();
diff --git a/MelonLoader/MelonEnvironment.cs b/MelonLoader/MelonEnvironment.cs
new file mode 100644
index 0000000..34e4bdc
--- /dev/null
+++ b/MelonLoader/MelonEnvironment.cs
@@ -0,0 +1,88 @@
+using System;
+using System.IO;
+using MelonLoader.Utils;
+using MLVScan.Abstractions;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of IPlatformEnvironment.
+ /// Uses MelonEnvironment for path resolution.
+ ///
+ public class MelonPlatformEnvironment : IPlatformEnvironment
+ {
+ private readonly string _gameRoot;
+ private readonly string _dataDir;
+ private readonly string _reportsDir;
+
+ public MelonPlatformEnvironment()
+ {
+ _gameRoot = MelonEnvironment.GameRootDirectory;
+ _dataDir = Path.Combine(_gameRoot, "MLVScan");
+ _reportsDir = Path.Combine(_dataDir, "Reports");
+ }
+
+ public string GameRootDirectory => _gameRoot;
+
+ public string[] PluginDirectories => new[]
+ {
+ Path.Combine(_gameRoot, "Mods"),
+ Path.Combine(_gameRoot, "Plugins")
+ };
+
+ public string DataDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_dataDir))
+ Directory.CreateDirectory(_dataDir);
+ return _dataDir;
+ }
+ }
+
+ public string ReportsDirectory
+ {
+ get
+ {
+ if (!Directory.Exists(_reportsDir))
+ Directory.CreateDirectory(_reportsDir);
+ return _reportsDir;
+ }
+ }
+
+ public string ManagedDirectory
+ {
+ get
+ {
+ // Unity managed assemblies location
+ var managedPath = Path.Combine(_gameRoot, "Schedule I_Data", "Managed");
+ if (Directory.Exists(managedPath))
+ return managedPath;
+
+ // Fallback for Il2Cpp games
+ var il2cppPath = Path.Combine(_gameRoot, "MelonLoader", "Managed");
+ if (Directory.Exists(il2cppPath))
+ return il2cppPath;
+
+ return string.Empty;
+ }
+ }
+
+ public string SelfAssemblyPath
+ {
+ get
+ {
+ try
+ {
+ return typeof(MelonPlatformEnvironment).Assembly.Location;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+ }
+
+ public string PlatformName => "MelonLoader";
+ }
+}
diff --git a/MelonLoader/MelonPluginDisabler.cs b/MelonLoader/MelonPluginDisabler.cs
new file mode 100644
index 0000000..0ad3498
--- /dev/null
+++ b/MelonLoader/MelonPluginDisabler.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of plugin disabler.
+ /// Uses ".di" extension (MelonLoader convention).
+ ///
+ public class MelonPluginDisabler : PluginDisablerBase
+ {
+ private const string MelonDisabledExtension = ".di";
+
+ public MelonPluginDisabler(IScanLogger logger, ScanConfig config)
+ : base(logger, config)
+ {
+ }
+
+ protected override string DisabledExtension => MelonDisabledExtension;
+
+ ///
+ /// MelonLoader uses extension replacement style (plugin.dll -> plugin.di).
+ ///
+ protected override string GetDisabledPath(string originalPath)
+ {
+ return Path.ChangeExtension(originalPath, MelonDisabledExtension);
+ }
+ }
+}
diff --git a/MelonLoader/MelonPluginScanner.cs b/MelonLoader/MelonPluginScanner.cs
new file mode 100644
index 0000000..cad1ce2
--- /dev/null
+++ b/MelonLoader/MelonPluginScanner.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MelonLoader.Utils;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+using MLVScan.Services;
+
+namespace MLVScan.MelonLoader
+{
+ ///
+ /// MelonLoader implementation of plugin scanner.
+ /// Scans Mods/, Plugins/, and Thunderstore directories.
+ ///
+ public class MelonPluginScanner : PluginScannerBase
+ {
+ private readonly MelonPlatformEnvironment _environment;
+
+ public MelonPluginScanner(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager,
+ MelonPlatformEnvironment environment)
+ : base(logger, resolverProvider, config, configManager)
+ {
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
+ }
+
+ protected override IEnumerable GetScanDirectories()
+ {
+ // Configured directories relative to game root
+ foreach (var scanDir in Config.ScanDirectories)
+ {
+ yield return Path.Combine(MelonEnvironment.GameRootDirectory, scanDir);
+ }
+ }
+
+ protected override bool IsSelfAssembly(string filePath)
+ {
+ try
+ {
+ var selfPath = _environment.SelfAssemblyPath;
+ if (string.IsNullOrEmpty(selfPath))
+ return false;
+
+ return Path.GetFullPath(filePath).Equals(
+ Path.GetFullPath(selfPath),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ protected override void OnScanComplete(Dictionary> results)
+ {
+ // Also scan Thunderstore Mod Manager directories
+ ScanThunderstoreModManager(results);
+ }
+
+ private void ScanThunderstoreModManager(Dictionary> results)
+ {
+ try
+ {
+ string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ string thunderstoreBasePath = Path.Combine(appDataPath, "Thunderstore Mod Manager", "DataFolder");
+
+ if (!Directory.Exists(thunderstoreBasePath))
+ return;
+
+ // Find game folders
+ foreach (var gameFolder in Directory.GetDirectories(thunderstoreBasePath))
+ {
+ // Scan profiles
+ string profilesPath = Path.Combine(gameFolder, "profiles");
+ if (!Directory.Exists(profilesPath))
+ continue;
+
+ foreach (var profileFolder in Directory.GetDirectories(profilesPath))
+ {
+ // Scan Mods directory
+ string modsPath = Path.Combine(profileFolder, "Mods");
+ if (Directory.Exists(modsPath))
+ {
+ Logger.Info($"Scanning Thunderstore profile mods: {modsPath}");
+ ScanDirectory(modsPath, results);
+ }
+
+ // Scan Plugins directory
+ string pluginsPath = Path.Combine(profileFolder, "Plugins");
+ if (Directory.Exists(pluginsPath))
+ {
+ Logger.Info($"Scanning Thunderstore profile plugins: {pluginsPath}");
+ ScanDirectory(pluginsPath, results);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error scanning Thunderstore Mod Manager directories: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/Models/DisabledModInfo.cs b/Models/DisabledModInfo.cs
deleted file mode 100644
index 70c5923..0000000
--- a/Models/DisabledModInfo.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace MLVScan.Models
-{
- public class DisabledModInfo
- {
- public string OriginalPath { get; }
- public string DisabledPath { get; }
- public string FileHash { get; }
-
- public DisabledModInfo(string originalPath, string disabledPath, string fileHash)
- {
- OriginalPath = originalPath;
- DisabledPath = disabledPath;
- FileHash = fileHash;
- }
- }
-}
diff --git a/PlatformConstants.cs b/PlatformConstants.cs
index 9873b2c..a974ade 100644
--- a/PlatformConstants.cs
+++ b/PlatformConstants.cs
@@ -1,20 +1,33 @@
namespace MLVScan
{
///
- /// Version and build constants for MLVScan.MelonLoader platform.
+ /// Version and build constants for MLVScan platform.
/// Update this file when releasing new versions.
+ /// Uses conditional compilation for platform-specific values.
///
public static class PlatformConstants
{
///
- /// Platform-specific version (MelonLoader implementation).
+ /// Platform-specific version.
///
public const string PlatformVersion = "1.6.1";
+#if MELONLOADER
///
/// Platform name identifier.
///
public const string PlatformName = "MLVScan.MelonLoader";
+#elif BEPINEX
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx";
+#else
+ ///
+ /// Platform name identifier (fallback for IDE).
+ ///
+ public const string PlatformName = "MLVScan";
+#endif
///
/// Gets the full platform version string.
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index e54f15c..1bba7be 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -1,5 +1,8 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
-using MelonLoader;
+using MLVScan.Abstractions;
using MLVScan.Models;
namespace MLVScan.Services
@@ -10,9 +13,9 @@ namespace MLVScan.Services
///
public class DeveloperReportGenerator
{
- private readonly MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
- public DeveloperReportGenerator(MelonLogger.Instance logger)
+ public DeveloperReportGenerator(IScanLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -25,12 +28,12 @@ public void GenerateConsoleReport(string modName, List findings)
if (findings == null || findings.Count == 0)
return;
- _logger.Msg("======= DEVELOPER SCAN REPORT =======");
- _logger.Msg(PlatformConstants.GetFullVersionInfo());
- _logger.Msg($"Mod: {modName}");
- _logger.Msg("--------------------------------------");
- _logger.Msg($"Total findings: {findings.Count}");
- _logger.Msg("");
+ _logger.Info("======= DEVELOPER SCAN REPORT =======");
+ _logger.Info(PlatformConstants.GetFullVersionInfo());
+ _logger.Info($"Mod: {modName}");
+ _logger.Info("--------------------------------------");
+ _logger.Info($"Total findings: {findings.Count}");
+ _logger.Info("");
var groupedByRule = findings
.Where(f => f.RuleId != null)
@@ -42,57 +45,57 @@ public void GenerateConsoleReport(string modName, List findings)
var firstFinding = ruleGroup.First();
var count = ruleGroup.Count();
- _logger.Msg($"[{firstFinding.Severity}] {firstFinding.Description}");
- _logger.Msg($" Rule: {firstFinding.RuleId}");
- _logger.Msg($" Occurrences: {count}");
+ _logger.Info($"[{firstFinding.Severity}] {firstFinding.Description}");
+ _logger.Info($" Rule: {firstFinding.RuleId}");
+ _logger.Info($" Occurrences: {count}");
// Show developer guidance if available
if (firstFinding.DeveloperGuidance != null)
{
- _logger.Msg("");
- _logger.Msg(" Developer Guidance:");
- _logger.Msg($" {WrapText(firstFinding.DeveloperGuidance.Remediation, 2)}");
+ _logger.Info("");
+ _logger.Info(" Developer Guidance:");
+ _logger.Info($" {WrapText(firstFinding.DeveloperGuidance.Remediation, 2)}");
if (!string.IsNullOrEmpty(firstFinding.DeveloperGuidance.DocumentationUrl))
{
- _logger.Msg($" Documentation: {firstFinding.DeveloperGuidance.DocumentationUrl}");
+ _logger.Info($" Documentation: {firstFinding.DeveloperGuidance.DocumentationUrl}");
}
if (firstFinding.DeveloperGuidance.AlternativeApis != null &&
firstFinding.DeveloperGuidance.AlternativeApis.Length > 0)
{
- _logger.Msg($" Suggested APIs: {string.Join(", ", firstFinding.DeveloperGuidance.AlternativeApis)}");
+ _logger.Info($" Suggested APIs: {string.Join(", ", firstFinding.DeveloperGuidance.AlternativeApis)}");
}
if (!firstFinding.DeveloperGuidance.IsRemediable)
{
- _logger.Warning(" ⚠ No safe alternative - this pattern should not be used in MelonLoader mods.");
+ _logger.Warning(" No safe alternative - this pattern should not be used in mods.");
}
}
else
{
- _logger.Msg(" (No developer guidance available for this rule)");
+ _logger.Info(" (No developer guidance available for this rule)");
}
// Show sample locations
- _logger.Msg("");
- _logger.Msg(" Sample locations:");
+ _logger.Info("");
+ _logger.Info(" Sample locations:");
foreach (var finding in ruleGroup.Take(3))
{
- _logger.Msg($" - {finding.Location}");
+ _logger.Info($" - {finding.Location}");
}
if (count > 3)
{
- _logger.Msg($" ... and {count - 3} more");
+ _logger.Info($" ... and {count - 3} more");
}
- _logger.Msg("");
- _logger.Msg("--------------------------------------");
+ _logger.Info("");
+ _logger.Info("--------------------------------------");
}
- _logger.Msg("");
- _logger.Msg("For more information, visit: https://discord.gg/UD4K4chKak");
- _logger.Msg("=====================================");
+ _logger.Info("");
+ _logger.Info("For more information, visit: https://discord.gg/UD4K4chKak");
+ _logger.Info("=====================================");
}
///
@@ -151,7 +154,7 @@ public string GenerateFileReport(string modName, string hash, List
if (!firstFinding.DeveloperGuidance.IsRemediable)
{
sb.AppendLine("");
- sb.AppendLine("WARNING: This pattern has no safe alternative and should not be used in MelonLoader mods.");
+ sb.AppendLine("WARNING: This pattern has no safe alternative and should not be used in mods.");
}
sb.AppendLine("");
diff --git a/Services/HashUtility.cs b/Services/HashUtility.cs
new file mode 100644
index 0000000..f559752
--- /dev/null
+++ b/Services/HashUtility.cs
@@ -0,0 +1,65 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Utility class for computing file hashes.
+ /// Used for whitelisting and tracking disabled mods.
+ ///
+ public static class HashUtility
+ {
+ ///
+ /// Calculates the SHA256 hash of a file.
+ ///
+ /// Path to the file to hash.
+ /// Lowercase hex string of the hash, or error message on failure.
+ public static string CalculateFileHash(string filePath)
+ {
+ try
+ {
+ if (!File.Exists(filePath))
+ return $"File not found: {filePath}";
+
+ using var sha256 = SHA256.Create();
+ using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var hash = sha256.ComputeHash(stream);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return "Access denied";
+ }
+ catch (IOException ex)
+ {
+ return $"IO Error: {ex.Message}";
+ }
+ catch (Exception ex)
+ {
+ return $"Error: {ex.Message}";
+ }
+ }
+
+ ///
+ /// Validates that a string looks like a valid SHA256 hash.
+ ///
+ public static bool IsValidHash(string hash)
+ {
+ if (string.IsNullOrWhiteSpace(hash))
+ return false;
+
+ // SHA256 produces 64 hex characters
+ if (hash.Length != 64)
+ return false;
+
+ foreach (char c in hash)
+ {
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Services/IlDumpService.cs b/Services/IlDumpService.cs
index 6590344..919423d 100644
--- a/Services/IlDumpService.cs
+++ b/Services/IlDumpService.cs
@@ -1,21 +1,26 @@
using System;
using System.IO;
using System.Linq;
-using MelonLoader;
-using MelonLoader.Utils;
+using MLVScan.Abstractions;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace MLVScan.Services
{
+ ///
+ /// Service for dumping IL code from assemblies.
+ /// Uses platform environment for assembly resolution.
+ ///
public class IlDumpService
{
- private readonly MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
+ private readonly IPlatformEnvironment _environment;
private readonly DefaultAssemblyResolver _assemblyResolver;
- public IlDumpService(MelonLogger.Instance logger)
+ public IlDumpService(IScanLogger logger, IPlatformEnvironment environment)
{
- _logger = logger;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _environment = environment ?? throw new ArgumentNullException(nameof(environment));
_assemblyResolver = BuildResolver();
}
@@ -42,7 +47,8 @@ public bool TryDumpAssembly(string assemblyPath, string outputPath)
using var writer = new StreamWriter(outputPath);
writer.WriteLine($"; Full IL dump for {Path.GetFileName(assemblyPath)}");
- writer.WriteLine($"; Generated: {System.DateTime.Now}");
+ writer.WriteLine($"; Generated: {DateTime.Now}");
+ writer.WriteLine($"; Platform: {_environment.PlatformName}");
writer.WriteLine();
foreach (var module in assembly.Modules)
@@ -56,12 +62,12 @@ public bool TryDumpAssembly(string assemblyPath, string outputPath)
}
}
- _logger?.Msg($"Saved IL dump to: {outputPath}");
+ _logger.Info($"Saved IL dump to: {outputPath}");
return true;
}
catch (Exception ex)
{
- _logger?.Error($"Failed to dump IL for {Path.GetFileName(assemblyPath)}: {ex.Message}");
+ _logger.Error($"Failed to dump IL for {Path.GetFileName(assemblyPath)}: {ex.Message}");
return false;
}
}
@@ -70,37 +76,27 @@ private DefaultAssemblyResolver BuildResolver()
{
var resolver = new DefaultAssemblyResolver();
- var gameDir = MelonEnvironment.GameRootDirectory;
- var melonDir = Path.Combine(gameDir, "MelonLoader");
-
- resolver.AddSearchDirectory(gameDir);
-
- if (Directory.Exists(melonDir))
+ // Add game root
+ var gameDir = _environment.GameRootDirectory;
+ if (Directory.Exists(gameDir))
{
- resolver.AddSearchDirectory(melonDir);
-
- var managedDir = Path.Combine(melonDir, "Managed");
- if (Directory.Exists(managedDir))
- {
- resolver.AddSearchDirectory(managedDir);
- }
-
- var dependenciesDir = Path.Combine(melonDir, "Dependencies");
- if (Directory.Exists(dependenciesDir))
- {
- resolver.AddSearchDirectory(dependenciesDir);
+ resolver.AddSearchDirectory(gameDir);
+ }
- foreach (var dir in Directory.GetDirectories(dependenciesDir, "*", SearchOption.AllDirectories))
- {
- resolver.AddSearchDirectory(dir);
- }
- }
+ // Add managed assemblies directory
+ var managedDir = _environment.ManagedDirectory;
+ if (!string.IsNullOrEmpty(managedDir) && Directory.Exists(managedDir))
+ {
+ resolver.AddSearchDirectory(managedDir);
}
- var gameManagedDir = Path.Combine(gameDir, "Schedule I_Data", "Managed");
- if (Directory.Exists(gameManagedDir))
+ // Add plugin directories
+ foreach (var pluginDir in _environment.PluginDirectories)
{
- resolver.AddSearchDirectory(gameManagedDir);
+ if (Directory.Exists(pluginDir))
+ {
+ resolver.AddSearchDirectory(pluginDir);
+ }
}
return resolver;
diff --git a/Services/ModDisabler.cs b/Services/ModDisabler.cs
deleted file mode 100644
index da1b895..0000000
--- a/Services/ModDisabler.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MelonLoader;
-using MLVScan.Models;
-
-namespace MLVScan.Services
-{
- public class ModDisabler(MelonLogger.Instance logger, ScanConfig config)
- {
- private readonly MelonLogger.Instance _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- private readonly ScanConfig _config = config ?? throw new ArgumentNullException(nameof(config));
- private const string DisabledExtension = ".di";
-
- public List DisableSuspiciousMods(Dictionary> scanResults, bool forceDisable = false)
- {
- if (!forceDisable && !_config.EnableAutoDisable)
- {
- _logger.Msg("Automatic disabling is turned off in configuration");
- return [];
- }
-
- var disabledMods = new List();
-
- foreach (var (modFilePath, findings) in scanResults)
- {
- var severeFindings = findings.Where(f =>
- (int)f.Severity >= (int)_config.MinSeverityForDisable)
- .ToList();
-
- // Skip if no findings meet the minimum severity threshold
- if (severeFindings.Count == 0)
- {
- _logger.Msg($"Mod {Path.GetFileName(modFilePath)} has findings but none meet minimum severity threshold ({_config.MinSeverityForDisable} - If this is set to Medium, the mod is likely not malicious).");
- continue;
- }
-
- if (!forceDisable && severeFindings.Count < _config.SuspiciousThreshold)
- {
- _logger.Msg($"Mod {Path.GetFileName(modFilePath)} has suspicious patterns but below threshold");
- continue;
- }
-
- try
- {
- var fileHash = ModScanner.CalculateFileHash(modFilePath);
- var newFilePath = Path.ChangeExtension(modFilePath, DisabledExtension);
-
- if (File.Exists(newFilePath))
- {
- File.Delete(newFilePath);
- }
-
- File.Move(modFilePath, newFilePath);
- _logger.Warning($"Disabled potentially malicious mod: {Path.GetFileName(modFilePath)}");
- disabledMods.Add(new DisabledModInfo(modFilePath, newFilePath, fileHash));
- }
- catch (Exception ex)
- {
- _logger.Error($"Failed to disable mod {Path.GetFileName(modFilePath)}: {ex.Message}");
- }
- }
-
- return disabledMods;
- }
- }
-}
diff --git a/Services/ModScanner.cs b/Services/ModScanner.cs
deleted file mode 100644
index de67508..0000000
--- a/Services/ModScanner.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using MelonLoader;
-using MelonLoader.Utils;
-using MLVScan.Models;
-
-namespace MLVScan.Services
-{
- public class ModScanner(
- AssemblyScanner assemblyScanner,
- MelonLogger.Instance logger,
- ScanConfig config,
- ConfigManager configManager)
- {
- private readonly AssemblyScanner _assemblyScanner = assemblyScanner ?? throw new ArgumentNullException(nameof(assemblyScanner));
- private readonly MelonLogger.Instance _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- private readonly ScanConfig _config = config ?? throw new ArgumentNullException(nameof(config));
- private readonly ConfigManager _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
-
- public Dictionary> ScanAllMods(bool forceScanning = false)
- {
- var results = new Dictionary>();
-
- if (!forceScanning && !_config.EnableAutoScan)
- {
- _logger.Msg("Automatic scanning is disabled in configuration");
- return results;
- }
-
- // Scan configured directories
- foreach (var scanDir in _config.ScanDirectories)
- {
- var directoryPath = Path.Combine(MelonEnvironment.GameRootDirectory, scanDir);
-
- if (!Directory.Exists(directoryPath))
- {
- _logger.Warning($"Directory not found: {directoryPath}");
- continue;
- }
-
- ScanDirectory(directoryPath, results);
- }
-
- // Scan Thunderstore Mod Manager directories
- ScanThunderstoreModManager(results);
-
- return results;
- }
-
- private void ScanDirectory(string directoryPath, Dictionary> results)
- {
- var modFiles = Directory.GetFiles(directoryPath, "*.dll", SearchOption.AllDirectories);
- _logger.Msg($"Found {modFiles.Length} potential mod files in {directoryPath}");
-
- foreach (var modFile in modFiles)
- {
- try
- {
- var modFileName = Path.GetFileName(modFile);
- var hash = CalculateFileHash(modFile);
-
- if (Path.GetFullPath(modFile).Equals(Path.GetFullPath(typeof(Core).Assembly.Location), StringComparison.OrdinalIgnoreCase))
- {
- _logger.Msg($"Skipping self: {modFileName}");
- continue;
- }
-
- if (_configManager.IsHashWhitelisted(hash))
- {
- _logger.Msg($"Skipping whitelisted mod: {modFileName} [Hash: {hash}]");
- continue;
- }
-
- var findings = _assemblyScanner.Scan(modFile).ToList();
- if (findings.Count < _config.SuspiciousThreshold)
- continue;
- results.Add(modFile, findings);
- _logger.Warning($"Found {findings.Count} suspicious patterns in {Path.GetFileName(modFile)}");
- }
- catch (Exception ex)
- {
- _logger.Error($"Error scanning {Path.GetFileName(modFile)}: {ex.Message}");
- }
- }
- }
-
- private void ScanThunderstoreModManager(Dictionary> results)
- {
- try
- {
- string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- string thunderstoreBasePath = Path.Combine(appDataPath, "Thunderstore Mod Manager", "DataFolder");
-
- if (!Directory.Exists(thunderstoreBasePath))
- {
- return;
- }
-
- // Find game folders (like ScheduleI in the example)
- foreach (var gameFolder in Directory.GetDirectories(thunderstoreBasePath))
- {
- // Scan profiles
- string profilesPath = Path.Combine(gameFolder, "profiles");
- if (Directory.Exists(profilesPath))
- {
- foreach (var profileFolder in Directory.GetDirectories(profilesPath))
- {
- // Scan Mods directory
- string modsPath = Path.Combine(profileFolder, "Mods");
- if (Directory.Exists(modsPath))
- {
- _logger.Msg($"Scanning Thunderstore profile mods: {modsPath}");
- ScanDirectory(modsPath, results);
- }
-
- // Scan Plugins directory
- string pluginsPath = Path.Combine(profileFolder, "Plugins");
- if (Directory.Exists(pluginsPath))
- {
- _logger.Msg($"Scanning Thunderstore profile plugins: {pluginsPath}");
- ScanDirectory(pluginsPath, results);
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- _logger.Error($"Error scanning Thunderstore Mod Manager directories: {ex.Message}");
- }
- }
-
- public static string CalculateFileHash(string filePath)
- {
- try
- {
- // Check if file exists first
- if (!File.Exists(filePath))
- {
- return $"File not found: {filePath}";
- }
-
- using (var sha256 = SHA256.Create())
- {
- using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- var hash = sha256.ComputeHash(stream);
- return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
- }
- }
- }
- catch (UnauthorizedAccessException)
- {
- return "Access denied";
- }
- catch (IOException ex)
- {
- return $"IO Error: {ex.Message}";
- }
- catch (Exception ex)
- {
- return $"Error: {ex.Message}";
- }
- }
- }
-}
diff --git a/Services/PluginDisablerBase.cs b/Services/PluginDisablerBase.cs
new file mode 100644
index 0000000..4196020
--- /dev/null
+++ b/Services/PluginDisablerBase.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Result info for a disabled plugin.
+ ///
+ public class DisabledPluginInfo
+ {
+ public string OriginalPath { get; }
+ public string DisabledPath { get; }
+ public string FileHash { get; }
+
+ public DisabledPluginInfo(string originalPath, string disabledPath, string fileHash)
+ {
+ OriginalPath = originalPath;
+ DisabledPath = disabledPath;
+ FileHash = fileHash;
+ }
+ }
+
+ ///
+ /// Abstract base class for plugin/mod disabling across platforms.
+ /// Contains shared disabling logic, with platform-specific details
+ /// delegated to derived classes.
+ ///
+ public abstract class PluginDisablerBase
+ {
+ protected readonly IScanLogger Logger;
+ protected readonly ScanConfig Config;
+
+ ///
+ /// Extension used to disable plugins (platform-specific).
+ /// MelonLoader uses ".di", BepInEx uses ".blocked".
+ ///
+ protected abstract string DisabledExtension { get; }
+
+ protected PluginDisablerBase(IScanLogger logger, ScanConfig config)
+ {
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ Config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ ///
+ /// Gets the disabled file path for a given plugin.
+ ///
+ protected virtual string GetDisabledPath(string originalPath)
+ {
+ // BepInEx style: append extension (plugin.dll.blocked)
+ // MelonLoader style: replace extension (plugin.di)
+ // Default to append style
+ return originalPath + DisabledExtension;
+ }
+
+ ///
+ /// Called after a plugin is disabled successfully.
+ ///
+ protected virtual void OnPluginDisabled(string originalPath, string disabledPath, string hash) { }
+
+ ///
+ /// Disables plugins that meet severity and threshold criteria.
+ ///
+ /// Dictionary of file paths to their findings.
+ /// If true, disables even if auto-disable is off.
+ public List DisableSuspiciousPlugins(
+ Dictionary> scanResults,
+ bool forceDisable = false)
+ {
+ if (!forceDisable && !Config.EnableAutoDisable)
+ {
+ Logger.Info("Automatic disabling is turned off in configuration");
+ return new List();
+ }
+
+ var disabledPlugins = new List();
+
+ foreach (var (pluginPath, findings) in scanResults)
+ {
+ var severeFindings = findings
+ .Where(f => (int)f.Severity >= (int)Config.MinSeverityForDisable)
+ .ToList();
+
+ if (severeFindings.Count == 0)
+ {
+ Logger.Info($"Plugin {Path.GetFileName(pluginPath)} has findings but none meet severity threshold ({Config.MinSeverityForDisable})");
+ continue;
+ }
+
+ if (!forceDisable && severeFindings.Count < Config.SuspiciousThreshold)
+ {
+ Logger.Info($"Plugin {Path.GetFileName(pluginPath)} below suspicious threshold");
+ continue;
+ }
+
+ try
+ {
+ var info = DisablePlugin(pluginPath);
+ if (info != null)
+ {
+ disabledPlugins.Add(info);
+ OnPluginDisabled(info.OriginalPath, info.DisabledPath, info.FileHash);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Failed to disable {Path.GetFileName(pluginPath)}: {ex.Message}");
+ }
+ }
+
+ return disabledPlugins;
+ }
+
+ ///
+ /// Disables a single plugin by renaming it.
+ ///
+ protected virtual DisabledPluginInfo DisablePlugin(string pluginPath)
+ {
+ var fileHash = HashUtility.CalculateFileHash(pluginPath);
+ var disabledPath = GetDisabledPath(pluginPath);
+
+ // Remove existing disabled file if present
+ if (File.Exists(disabledPath))
+ {
+ File.Delete(disabledPath);
+ }
+
+ // Rename to disable
+ File.Move(pluginPath, disabledPath);
+
+ Logger.Warning($"BLOCKED: {Path.GetFileName(pluginPath)}");
+ return new DisabledPluginInfo(pluginPath, disabledPath, fileHash);
+ }
+ }
+}
diff --git a/Services/PluginScannerBase.cs b/Services/PluginScannerBase.cs
new file mode 100644
index 0000000..f778f4f
--- /dev/null
+++ b/Services/PluginScannerBase.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MLVScan.Abstractions;
+using MLVScan.Models;
+
+namespace MLVScan.Services
+{
+ ///
+ /// Abstract base class for plugin/mod scanning across platforms.
+ /// Contains shared scanning logic, with platform-specific details
+ /// delegated to derived classes.
+ ///
+ public abstract class PluginScannerBase
+ {
+ protected readonly IScanLogger Logger;
+ protected readonly IAssemblyResolverProvider ResolverProvider;
+ protected readonly ScanConfig Config;
+ protected readonly IConfigManager ConfigManager;
+ protected readonly AssemblyScanner AssemblyScanner;
+
+ protected PluginScannerBase(
+ IScanLogger logger,
+ IAssemblyResolverProvider resolverProvider,
+ ScanConfig config,
+ IConfigManager configManager)
+ {
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ ResolverProvider = resolverProvider ?? throw new ArgumentNullException(nameof(resolverProvider));
+ Config = config ?? throw new ArgumentNullException(nameof(config));
+ ConfigManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
+
+ var rules = RuleFactory.CreateDefaultRules();
+ AssemblyScanner = new AssemblyScanner(rules, Config, ResolverProvider);
+ }
+
+ ///
+ /// Gets the directories to scan for plugins.
+ ///
+ protected abstract IEnumerable GetScanDirectories();
+
+ ///
+ /// Checks if a file path is this scanner's own assembly.
+ ///
+ protected abstract bool IsSelfAssembly(string filePath);
+
+ ///
+ /// Performs any platform-specific post-scan processing.
+ ///
+ protected virtual void OnScanComplete(Dictionary> results) { }
+
+ ///
+ /// Scans all plugins in configured directories.
+ ///
+ /// If true, scans even if auto-scan is disabled.
+ public Dictionary> ScanAllPlugins(bool forceScanning = false)
+ {
+ var results = new Dictionary>();
+
+ if (!forceScanning && !Config.EnableAutoScan)
+ {
+ Logger.Info("Automatic scanning is disabled in configuration");
+ return results;
+ }
+
+ foreach (var directory in GetScanDirectories())
+ {
+ if (!Directory.Exists(directory))
+ {
+ Logger.Warning($"Directory not found: {directory}");
+ continue;
+ }
+
+ ScanDirectory(directory, results);
+ }
+
+ OnScanComplete(results);
+ return results;
+ }
+
+ ///
+ /// Scans a single directory for malicious plugins.
+ ///
+ protected virtual void ScanDirectory(string directoryPath, Dictionary> results)
+ {
+ var pluginFiles = Directory.GetFiles(directoryPath, "*.dll", SearchOption.AllDirectories);
+ Logger.Info($"Found {pluginFiles.Length} plugin files in {directoryPath}");
+
+ foreach (var pluginFile in pluginFiles)
+ {
+ try
+ {
+ ScanSingleFile(pluginFile, results);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error scanning {Path.GetFileName(pluginFile)}: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Scans a single file and adds results if suspicious.
+ ///
+ protected virtual void ScanSingleFile(string filePath, Dictionary> results)
+ {
+ var fileName = Path.GetFileName(filePath);
+ var hash = HashUtility.CalculateFileHash(filePath);
+
+ // Skip ourselves
+ if (IsSelfAssembly(filePath))
+ {
+ Logger.Debug($"Skipping self: {fileName}");
+ return;
+ }
+
+ // Skip whitelisted plugins
+ if (ConfigManager.IsHashWhitelisted(hash))
+ {
+ Logger.Debug($"Skipping whitelisted: {fileName}");
+ return;
+ }
+
+ var findings = AssemblyScanner.Scan(filePath).ToList();
+
+ // Filter out placeholder findings
+ var actualFindings = findings
+ .Where(f => f.Location != "Assembly scanning")
+ .ToList();
+
+ if (actualFindings.Count >= Config.SuspiciousThreshold)
+ {
+ results.Add(filePath, actualFindings);
+ Logger.Warning($"Found {actualFindings.Count} suspicious patterns in {fileName}");
+ }
+ }
+ }
+}
diff --git a/Services/PromptGeneratorService.cs b/Services/PromptGeneratorService.cs
index ea4cd08..944bfee 100644
--- a/Services/PromptGeneratorService.cs
+++ b/Services/PromptGeneratorService.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Text;
+using MLVScan.Abstractions;
using MLVScan.Models;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -13,9 +14,9 @@ namespace MLVScan.Services
public class PromptGeneratorService
{
private readonly ScanConfig _config;
- private readonly MelonLoader.MelonLogger.Instance _logger;
+ private readonly IScanLogger _logger;
- public PromptGeneratorService(ScanConfig config, MelonLoader.MelonLogger.Instance logger)
+ public PromptGeneratorService(ScanConfig config, IScanLogger logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
diff --git a/Services/ServiceFactory.cs b/Services/ServiceFactory.cs
index 26a78ad..7e03b1c 100644
--- a/Services/ServiceFactory.cs
+++ b/Services/ServiceFactory.cs
@@ -1,6 +1,8 @@
+using System;
using MelonLoader;
using MLVScan.Abstractions;
using MLVScan.Adapters;
+using MLVScan.MelonLoader;
using MLVScan.Models;
using MLVScan.Services;
@@ -11,35 +13,42 @@ namespace MLVScan
///
public class ServiceFactory
{
- private readonly MelonLogger.Instance _logger;
+ private readonly MelonLogger.Instance _melonLogger;
private readonly IScanLogger _scanLogger;
private readonly IAssemblyResolverProvider _resolverProvider;
- private readonly ConfigManager _configManager;
+ private readonly MelonConfigManager _configManager;
+ private readonly MelonPlatformEnvironment _environment;
private readonly ScanConfig _fallbackConfig;
public ServiceFactory(MelonLogger.Instance logger)
{
- _logger = logger;
+ _melonLogger = logger ?? throw new ArgumentNullException(nameof(logger));
_scanLogger = new MelonScanLogger(logger);
_resolverProvider = new GameAssemblyResolverProvider();
+ _environment = new MelonPlatformEnvironment();
_fallbackConfig = new ScanConfig();
try
{
- _configManager = new ConfigManager(logger);
+ _configManager = new MelonConfigManager(logger);
}
catch (Exception ex)
{
- _logger.Error($"Failed to create ConfigManager: {ex.Message}");
- _logger.Msg("Using default configuration values");
+ _melonLogger.Error($"Failed to create ConfigManager: {ex.Message}");
+ _melonLogger.Msg("Using default configuration values");
}
}
- public ConfigManager CreateConfigManager()
+ public MelonConfigManager CreateConfigManager()
{
return _configManager;
}
+ public MelonPlatformEnvironment CreateEnvironment()
+ {
+ return _environment;
+ }
+
public AssemblyScanner CreateAssemblyScanner()
{
var config = _configManager?.Config ?? _fallbackConfig;
@@ -48,33 +57,37 @@ public AssemblyScanner CreateAssemblyScanner()
return new AssemblyScanner(rules, config, _resolverProvider);
}
- public ModScanner CreateModScanner()
+ public MelonPluginScanner CreatePluginScanner()
{
- var assemblyScanner = CreateAssemblyScanner();
var config = _configManager?.Config ?? _fallbackConfig;
- return new ModScanner(assemblyScanner, _logger, config, _configManager);
+ return new MelonPluginScanner(
+ _scanLogger,
+ _resolverProvider,
+ config,
+ _configManager,
+ _environment);
}
- public ModDisabler CreateModDisabler()
+ public MelonPluginDisabler CreatePluginDisabler()
{
var config = _configManager?.Config ?? _fallbackConfig;
- return new ModDisabler(_logger, config);
+ return new MelonPluginDisabler(_scanLogger, config);
}
public PromptGeneratorService CreatePromptGenerator()
{
var config = _configManager?.Config ?? _fallbackConfig;
- return new PromptGeneratorService(config, _logger);
+ return new PromptGeneratorService(config, _scanLogger);
}
public IlDumpService CreateIlDumpService()
{
- return new IlDumpService(_logger);
+ return new IlDumpService(_scanLogger, _environment);
}
public DeveloperReportGenerator CreateDeveloperReportGenerator()
{
- return new DeveloperReportGenerator(_logger);
+ return new DeveloperReportGenerator(_scanLogger);
}
}
}
From 78b8f2130087af01cc1cb34d1f5d5cd12d31146a Mon Sep 17 00:00:00 2001
From: ifBars
Date: Sat, 24 Jan 2026 12:42:18 -0800
Subject: [PATCH 02/28] fix: Migrate from System.Text.Json to Newtonsoft.Json
---
BepInEx/BepInExConfigManager.cs | 17 ++++++++---------
MLVScan.csproj | 2 +-
2 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/BepInEx/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs
index 913aedf..bc65bb3 100644
--- a/BepInEx/BepInExConfigManager.cs
+++ b/BepInEx/BepInExConfigManager.cs
@@ -1,12 +1,12 @@
using System;
using System.IO;
using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using BepInEx;
using BepInEx.Logging;
using MLVScan.Abstractions;
using MLVScan.Models;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
namespace MLVScan.BepInEx
{
@@ -21,12 +21,11 @@ public class BepInExConfigManager : IConfigManager
private readonly string _configPath;
private ScanConfig _config;
- // JSON serialization options
- private static readonly JsonSerializerOptions JsonOptions = new()
+ // JSON serialization settings
+ private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
{
- WriteIndented = true,
- PropertyNameCaseInsensitive = true,
- Converters = { new JsonStringEnumConverter() }
+ Formatting = Formatting.Indented,
+ Converters = { new StringEnumConverter() }
};
public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null)
@@ -48,7 +47,7 @@ public ScanConfig LoadConfig()
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
- var loaded = JsonSerializer.Deserialize(json, JsonOptions);
+ var loaded = JsonConvert.DeserializeObject(json, JsonSettings);
if (loaded != null)
{
@@ -97,7 +96,7 @@ public void SaveConfig(ScanConfig config)
Directory.CreateDirectory(configDir);
}
- var json = JsonSerializer.Serialize(config, JsonOptions);
+ var json = JsonConvert.SerializeObject(config, JsonSettings);
File.WriteAllText(_configPath, json);
_config = config;
diff --git a/MLVScan.csproj b/MLVScan.csproj
index fb6aefc..85659ac 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -133,7 +133,7 @@
-
+
From 83ab1773b0565c3491984c9f875448bad3f898db Mon Sep 17 00:00:00 2001
From: ifBars
Date: Sat, 24 Jan 2026 13:00:17 -0800
Subject: [PATCH 03/28] Update README.md
---
README.md | 26 +++++++++++++++++++++++---
1 file changed, 23 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index dc64b97..063fee5 100644
--- a/README.md
+++ b/README.md
@@ -4,26 +4,46 @@
-**MLVScan** is a security-focused MelonLoader plugin that protects your game by scanning mods for malicious patterns *before* they execute.
+**MLVScan** is a security-focused mod loader plugin that protects your game by scanning mods for malicious patterns *before* they execute.
+
+Supports **MelonLoader** and **BepInEx 5.x**.

## ⚡ Quick Start
-1. **Download** the latest `MLVScan.dll` from [Releases](../../releases).
+### For MelonLoader
+1. **Download** the latest `MLVScan.MelonLoader.dll` from [Releases](../../releases).
2. **Install** by dropping it into your game's `Plugins` folder.
3. **Play!** MLVScan automatically scans mods on startup.
+### For BepInEx 5.x
+1. **Download** the latest `MLVScan.BepInEx.dll` from [Releases](../../releases).
+2. **Install** by dropping it into your game's `BepInEx/patchers` folder.
+3. **Play!** MLVScan automatically scans plugins before they load.
+
## 📚 Documentation
Detailed documentation is available in the **[MLVScan Wiki](https://github.com/ifBars/MLVScan/wiki)**:
-* **[Getting Started](https://github.com/ifBars/MLVScan/wiki/Getting-Started)** - Full installation and setup guide.
+* **[Getting Started](https://github.com/ifBars/MLVScan/wiki/Getting-Started)** - Full installation and setup guide for both MelonLoader and BepInEx.
* **[Whitelisting](https://github.com/ifBars/MLVScan/wiki/Whitelisting)** - How to use the SHA256 security whitelist.
* **[Understanding Reports](https://github.com/ifBars/MLVScan/wiki/Scan-Reports)** - Interpret warnings and security levels.
* **[Architecture](https://github.com/ifBars/MLVScan/wiki/Architecture)** - How the ecosystem works.
* **[FAQ](https://github.com/ifBars/MLVScan/wiki/FAQ)** - Common questions and troubleshooting.
+### Key Differences
+
+**MelonLoader:**
+- Runs as a plugin during the mod loading phase
+- Configuration stored in `MelonPreferences.cfg`
+- Reports saved to `UserData/MLVScan/Reports/`
+
+**BepInEx:**
+- Runs as a preloader patcher (scans before chainloader)
+- Configuration stored in `BepInEx/config/MLVScan.json`
+- Reports saved to `BepInEx/MLVScan/Reports/`
+
## 🛡️ Powered by MLVScan.Core
MLVScan is built on **[MLVScan.Core](https://github.com/ifBars/MLVScan.Core)**, a cross-platform malware detection engine.
From 065534d92ef25b9c3f8ad753fcab6dacd500bd6a Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 18:39:49 -0800
Subject: [PATCH 04/28] feat: BepInEx 6.* Support
---
.../BepInEx5Patcher.cs} | 7 +-
BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs | 109 ++++++++++++++++++
BepInEx/6/Mono/BepInEx6MonoPatcher.cs | 109 ++++++++++++++++++
BepInEx/BepInExConfigManager.cs | 15 +--
BepInEx/BepInExReportGenerator.cs | 62 ++++++++++
MLVScan.csproj | 109 +++++++++++++++---
MLVScan.sln | 20 ++--
Core.cs => MelonLoader/MelonLoaderPlugin.cs | 91 +++++++++++++--
.../MelonLoaderServiceFactory.cs | 7 +-
PlatformConstants.cs | 10 ++
Services/DeveloperReportGenerator.cs | 97 +++++++++++++++-
11 files changed, 586 insertions(+), 50 deletions(-)
rename BepInEx/{MLVScanPatcher.cs => 5/BepInEx5Patcher.cs} (96%)
create mode 100644 BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
create mode 100644 BepInEx/6/Mono/BepInEx6MonoPatcher.cs
rename Core.cs => MelonLoader/MelonLoaderPlugin.cs (79%)
rename Services/ServiceFactory.cs => MelonLoader/MelonLoaderServiceFactory.cs (95%)
diff --git a/BepInEx/MLVScanPatcher.cs b/BepInEx/5/BepInEx5Patcher.cs
similarity index 96%
rename from BepInEx/MLVScanPatcher.cs
rename to BepInEx/5/BepInEx5Patcher.cs
index e5933d7..2d83ff0 100644
--- a/BepInEx/MLVScanPatcher.cs
+++ b/BepInEx/5/BepInEx5Patcher.cs
@@ -3,15 +3,16 @@
using BepInEx;
using BepInEx.Logging;
using Mono.Cecil;
+using MLVScan.BepInEx;
using MLVScan.BepInEx.Adapters;
-namespace MLVScan.BepInEx
+namespace MLVScan.BepInEx5
{
///
- /// BepInEx preloader patcher that scans plugins for malicious patterns
+ /// BepInEx 5.x preloader patcher that scans plugins for malicious patterns
/// before the chainloader initializes them.
///
- public static class MLVScanPatcher
+ public static class BepInEx5Patcher
{
private static ManualLogSource _logger;
diff --git a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
new file mode 100644
index 0000000..b789fdf
--- /dev/null
+++ b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
@@ -0,0 +1,109 @@
+using System;
+using BepInEx;
+using BepInEx.Logging;
+using BepInEx.Preloader.Core.Patching;
+using MLVScan.BepInEx;
+using MLVScan.BepInEx.Adapters;
+
+namespace MLVScan.BepInEx6.IL2CPP
+{
+ ///
+ /// BepInEx 6.x (IL2CPP) preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ [PatcherPluginInfo("com.bars.mlvscan", "MLVScan", "1.6.1")]
+ public class BepInEx6IL2CppPatcher : BasePatcher
+ {
+ private ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Called when the patcher is initialized.
+ /// This is the main entry point for BepInEx 6.x patchers.
+ ///
+ public override void Initialize()
+ {
+ _logger = Logger.CreateLogSource("MLVScan");
+
+ try
+ {
+ _logger.LogInfo("MLVScan BepInEx 6 (IL2CPP) patcher initializing...");
+ _logger.LogInfo($"Plugin directory: {Paths.PluginPath}");
+
+ // Create platform environment
+ var environment = new BepInExPlatformEnvironment();
+
+ // Load or create configuration
+ var configManager = new BepInExConfigManager(_logger, DefaultWhitelistedHashes);
+ var config = configManager.LoadConfig();
+
+ // Create adapters
+ var scanLogger = new BepInExScanLogger(_logger);
+ var resolverProvider = new BepInExAssemblyResolverProvider();
+
+ // Create scanner and disabler
+ var pluginScanner = new BepInExPluginScanner(
+ scanLogger,
+ resolverProvider,
+ config,
+ configManager,
+ environment);
+
+ var pluginDisabler = new BepInExPluginDisabler(scanLogger, config);
+ var reportGenerator = new BepInExReportGenerator(_logger, config);
+
+ // Scan all plugins
+ var scanResults = pluginScanner.ScanAllPlugins();
+
+ if (scanResults.Count > 0)
+ {
+ // Disable suspicious plugins
+ var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+
+ // Generate reports for disabled plugins
+ if (disabledPlugins.Count > 0)
+ {
+ reportGenerator.GenerateReports(disabledPlugins, scanResults);
+
+ _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
+ _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+ }
+ }
+ else
+ {
+ _logger.LogInfo("No suspicious plugins detected.");
+ }
+
+ _logger.LogInfo("MLVScan BepInEx 6 (IL2CPP) preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called when all patching is complete.
+ ///
+ public override void Finalizer()
+ {
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
new file mode 100644
index 0000000..6378a5d
--- /dev/null
+++ b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
@@ -0,0 +1,109 @@
+using System;
+using BepInEx;
+using BepInEx.Logging;
+using BepInEx.Preloader.Core.Patching;
+using MLVScan.BepInEx;
+using MLVScan.BepInEx.Adapters;
+
+namespace MLVScan.BepInEx6.Mono
+{
+ ///
+ /// BepInEx 6.x (Mono) preloader patcher that scans plugins for malicious patterns
+ /// before the chainloader initializes them.
+ ///
+ [PatcherPluginInfo("com.bars.mlvscan", "MLVScan", "1.6.1")]
+ public class BepInEx6MonoPatcher : BasePatcher
+ {
+ private ManualLogSource _logger;
+
+ ///
+ /// Default whitelist for known-safe BepInEx ecosystem plugins.
+ ///
+ private static readonly string[] DefaultWhitelistedHashes =
+ [
+ // BepInEx ecosystem - known safe plugins
+ "8c0735f521d0fa785bf81b2e627a93042362b736ebc2c4c7ac425276b49fa692",
+ "9f86b196ffc845bdbc85192054e2876388ce1294b5a880459c93cbed7de2ae9d",
+ "bc67dab59532d0daca129e574c87d43b24a0b63ccb7312ccd25e0d7c4887784c",
+ "f1f3ff967bdb8f63a4bfd878255890f6393af37d3cc357babb6b504d9473ee06",
+ "d034d0e941deb47ea6b5ee8ca288bdb1d0bb25475dfba02cb61f6eadf0fa448e",
+ "e28b71abefdb5c2e90ea2d9e3c79bdff95f8173d08022732f62f35d2c328895d",
+ "bd5ec0343880b528ef190afe91778d172a239a625929dc176492eddc5c66cc31",
+ "503f851721ffacc7839e42d7c6c8a7c39fa2cea6e70a480b8bad822064d65aa0",
+ "184386c0f5f5bae6b63c96b73e312d3f39eba0d0ca81de3e3bd574ef389d1e29"
+ ];
+
+ ///
+ /// Called when the patcher is initialized.
+ /// This is the main entry point for BepInEx 6.x patchers.
+ ///
+ public override void Initialize()
+ {
+ _logger = Logger.CreateLogSource("MLVScan");
+
+ try
+ {
+ _logger.LogInfo("MLVScan BepInEx 6 (Mono) patcher initializing...");
+ _logger.LogInfo($"Plugin directory: {Paths.PluginPath}");
+
+ // Create platform environment
+ var environment = new BepInExPlatformEnvironment();
+
+ // Load or create configuration
+ var configManager = new BepInExConfigManager(_logger, DefaultWhitelistedHashes);
+ var config = configManager.LoadConfig();
+
+ // Create adapters
+ var scanLogger = new BepInExScanLogger(_logger);
+ var resolverProvider = new BepInExAssemblyResolverProvider();
+
+ // Create scanner and disabler
+ var pluginScanner = new BepInExPluginScanner(
+ scanLogger,
+ resolverProvider,
+ config,
+ configManager,
+ environment);
+
+ var pluginDisabler = new BepInExPluginDisabler(scanLogger, config);
+ var reportGenerator = new BepInExReportGenerator(_logger, config);
+
+ // Scan all plugins
+ var scanResults = pluginScanner.ScanAllPlugins();
+
+ if (scanResults.Count > 0)
+ {
+ // Disable suspicious plugins
+ var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+
+ // Generate reports for disabled plugins
+ if (disabledPlugins.Count > 0)
+ {
+ reportGenerator.GenerateReports(disabledPlugins, scanResults);
+
+ _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
+ _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+ }
+ }
+ else
+ {
+ _logger.LogInfo("No suspicious plugins detected.");
+ }
+
+ _logger.LogInfo("MLVScan BepInEx 6 (Mono) preloader scan complete.");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError($"MLVScan initialization failed: {ex}");
+ }
+ }
+
+ ///
+ /// Called when all patching is complete.
+ ///
+ public override void Finalizer()
+ {
+ _logger?.LogDebug("MLVScan patcher finished.");
+ }
+ }
+}
diff --git a/BepInEx/BepInExConfigManager.cs b/BepInEx/BepInExConfigManager.cs
index bc65bb3..2040e58 100644
--- a/BepInEx/BepInExConfigManager.cs
+++ b/BepInEx/BepInExConfigManager.cs
@@ -5,8 +5,8 @@
using BepInEx.Logging;
using MLVScan.Abstractions;
using MLVScan.Models;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace MLVScan.BepInEx
{
@@ -22,10 +22,11 @@ public class BepInExConfigManager : IConfigManager
private ScanConfig _config;
// JSON serialization settings
- private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
+ private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
- Formatting = Formatting.Indented,
- Converters = { new StringEnumConverter() }
+ WriteIndented = true,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new JsonStringEnumConverter() }
};
public BepInExConfigManager(ManualLogSource logger, string[] defaultWhitelistedHashes = null)
@@ -47,7 +48,7 @@ public ScanConfig LoadConfig()
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
- var loaded = JsonConvert.DeserializeObject(json, JsonSettings);
+ var loaded = JsonSerializer.Deserialize(json, JsonOptions);
if (loaded != null)
{
@@ -96,7 +97,7 @@ public void SaveConfig(ScanConfig config)
Directory.CreateDirectory(configDir);
}
- var json = JsonConvert.SerializeObject(config, JsonSettings);
+ var json = JsonSerializer.Serialize(config, JsonOptions);
File.WriteAllText(_configPath, json);
_config = config;
diff --git a/BepInEx/BepInExReportGenerator.cs b/BepInEx/BepInExReportGenerator.cs
index da77b07..3a52399 100644
--- a/BepInEx/BepInExReportGenerator.cs
+++ b/BepInEx/BepInExReportGenerator.cs
@@ -6,6 +6,7 @@
using BepInEx;
using BepInEx.Logging;
using MLVScan.Models;
+using MLVScan.Models.Rules;
using MLVScan.Services;
namespace MLVScan.BepInEx
@@ -74,6 +75,35 @@ private void LogConsoleReport(string pluginName, string hash, List
{
_logger.LogWarning($"[{finding.Severity}] {finding.Description}");
_logger.LogInfo($" Location: {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ _logger.LogInfo(" Call Chain:");
+ foreach (var node in finding.CallChain.Nodes.Take(5))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ _logger.LogInfo($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 5)
+ {
+ _logger.LogInfo($" ... and {finding.CallChain.Nodes.Count - 5} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ _logger.LogInfo($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}% confidence)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ _logger.LogInfo($" Cross-method: {finding.DataFlowChain.InvolvedMethods.Count} methods involved");
+ }
+ }
}
if (findings.Count > 3)
@@ -159,6 +189,38 @@ private void GenerateFileReport(
foreach (var finding in group.Take(10))
{
sb.AppendLine($" - {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ sb.AppendLine(" Call Chain Analysis:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ sb.AppendLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ sb.AppendLine($" {node.Description}");
+ }
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ sb.AppendLine(" Data Flow Analysis:");
+ sb.AppendLine($" Pattern: {finding.DataFlowChain.Pattern}");
+ sb.AppendLine($" Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ sb.AppendLine($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
foreach (var line in finding.CodeSnippet.Split('\n').Take(5))
diff --git a/MLVScan.csproj b/MLVScan.csproj
index 85659ac..dd42e2a 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -14,7 +14,7 @@
en-US
True
false
- MelonLoader;BepInEx
+ MelonLoader;BepInEx;BepInEx6Mono;BepInEx6IL2CPP
@@ -23,6 +23,7 @@
MELONLOADER
MLVScan.MelonLoader
+ $(NoWarn);MSB3277
@@ -33,6 +34,23 @@
MLVScan.BepInEx
+
+
+
+
+ BEPINEX;BEPINEX6;BEPINEX6_MONO
+ MLVScan.BepInEx6.Mono
+
+
+
+
+
+
+ net6.0
+ BEPINEX;BEPINEX6;BEPINEX6_IL2CPP
+ MLVScan.BepInEx6.IL2CPP
+
+
@@ -62,21 +80,49 @@
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -110,7 +156,7 @@
-
@@ -121,19 +167,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
@@ -142,20 +207,30 @@
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+ "$(PkgILRepack)\tools\ILRepack.exe" /parallel /union
+ /lib:"@(ILRepackLib, '" /lib:"')"
+ $(ILRepackArgs) $(ILRepackLibArgs)
+ $(ILRepackArgs) /out:"$(OutputPath)$(AssemblyName).merged.dll"
+ $(ILRepackArgs) "@(InputAssemblies, '" "')"
+
+
+
diff --git a/MLVScan.sln b/MLVScan.sln
index efa2737..03c91d8 100644
--- a/MLVScan.sln
+++ b/MLVScan.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13
@@ -7,14 +7,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan", "MLVScan.csproj",
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
+ MelonLoader|Any CPU = MelonLoader|Any CPU
+ BepInEx|Any CPU = BepInEx|Any CPU
+ BepInEx6Mono|Any CPU = BepInEx6Mono|Any CPU
+ BepInEx6IL2CPP|Any CPU = BepInEx6IL2CPP|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.Build.0 = BepInEx6IL2CPP|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Core.cs b/MelonLoader/MelonLoaderPlugin.cs
similarity index 79%
rename from Core.cs
rename to MelonLoader/MelonLoaderPlugin.cs
index e497758..9db8d3b 100644
--- a/Core.cs
+++ b/MelonLoader/MelonLoaderPlugin.cs
@@ -3,19 +3,24 @@
using System.IO;
using System.Linq;
using MelonLoader;
-using MLVScan.MelonLoader;
+using MelonLoader.Utils;
using MLVScan.Models;
+using MLVScan.Models.Rules;
using MLVScan.Services;
-[assembly: MelonInfo(typeof(MLVScan.Core), "MLVScan", MLVScan.PlatformConstants.PlatformVersion, "Bars")]
+[assembly: MelonInfo(typeof(MLVScan.MelonLoader.MelonLoaderPlugin), "MLVScan", MLVScan.PlatformConstants.PlatformVersion, "Bars")]
[assembly: MelonPriority(Int32.MinValue)]
[assembly: MelonColor(255, 139, 0, 0)]
-namespace MLVScan
+namespace MLVScan.MelonLoader
{
- public class Core : MelonPlugin
+ ///
+ /// MelonLoader plugin entry point for MLVScan.
+ /// Sets up services, initializes the default whitelist, and orchestrates scanning, disabling, and reporting.
+ ///
+ public class MelonLoaderPlugin : MelonPlugin
{
- private ServiceFactory _serviceFactory;
+ private MelonLoaderServiceFactory _serviceFactory;
private MelonConfigManager _configManager;
private MelonPlatformEnvironment _environment;
private MelonPluginScanner _pluginScanner;
@@ -40,7 +45,7 @@ public override void OnEarlyInitializeMelon()
{
LoggerInstance.Msg("Pre-scanning for malicious mods...");
- _serviceFactory = new ServiceFactory(LoggerInstance);
+ _serviceFactory = new MelonLoaderServiceFactory(LoggerInstance);
_configManager = _serviceFactory.CreateConfigManager();
_environment = _serviceFactory.CreateEnvironment();
@@ -201,7 +206,7 @@ private void GenerateDetailedReports(List disabledMods, Dict
{
_developerReportGenerator.GenerateConsoleReport(modName, actualFindings);
}
- else
+ else
{
// Standard reporting
LoggerInstance.Warning("Suspicious patterns found:");
@@ -217,9 +222,39 @@ private void GenerateDetailedReports(List disabledMods, Dict
{
var finding = categoryFindings[i];
LoggerInstance.Msg($" * At: {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ LoggerInstance.Msg(" Call Chain Analysis:");
+ foreach (var node in finding.CallChain.Nodes.Take(4))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ LoggerInstance.Msg($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 4)
+ {
+ LoggerInstance.Msg($" ... and {finding.CallChain.Nodes.Count - 4} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ LoggerInstance.Msg($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}% confidence)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ LoggerInstance.Msg($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
- LoggerInstance.Msg($" Code Snippet (IL):");
+ LoggerInstance.Msg(" Code Snippet (IL):");
foreach (var line in finding.CodeSnippet.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
LoggerInstance.Msg($" {line}");
@@ -242,7 +277,7 @@ private void GenerateDetailedReports(List disabledMods, Dict
try
{
// Generate report and prompt files
- var reportDirectory = Path.Combine(MelonLoader.Utils.MelonEnvironment.UserDataDirectory, "MLVScan", "Reports");
+ var reportDirectory = Path.Combine(MelonEnvironment.UserDataDirectory, "MLVScan", "Reports");
if (!Directory.Exists(reportDirectory))
{
Directory.CreateDirectory(reportDirectory);
@@ -305,10 +340,45 @@ private void GenerateDetailedReports(List disabledMods, Dict
writer.WriteLine($"\n== {group.Key} ==");
writer.WriteLine($"Severity: {group.Value[0].Severity}");
writer.WriteLine($"Instances: {group.Value.Count}");
- writer.WriteLine("\nLocations & Snippets:");
+ writer.WriteLine("\nLocations & Analysis:");
foreach (var finding in group.Value)
{
writer.WriteLine($"- {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ writer.WriteLine(" Call Chain Analysis:");
+ writer.WriteLine($" {finding.CallChain.Summary}");
+ writer.WriteLine(" Attack Path:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ writer.WriteLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ writer.WriteLine($" {node.Description}");
+ }
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ writer.WriteLine(" Data Flow Analysis:");
+ writer.WriteLine($" Pattern: {finding.DataFlowChain.Pattern}");
+ writer.WriteLine($" Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ writer.WriteLine($" {finding.DataFlowChain.Summary}");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ writer.WriteLine($" Cross-method flow through {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
writer.WriteLine(" Code Snippet (IL):");
@@ -409,6 +479,7 @@ private static void WriteSecurityNoticeToReport(StreamWriter writer)
writer.WriteLine("\nDETAILED MALWARE REMOVAL GUIDES:");
writer.WriteLine("- Malwarebytes Guide: https://www.malwarebytes.com/cybersecurity/basics/how-to-remove-virus-from-computer");
writer.WriteLine("- Microsoft Safety Scanner: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download");
+ writer.WriteLine("- XWorm (Common Modding Malware) Removal Guide: https://www.pcrisk.com/removal-guides/27436-xworm-rat");
writer.WriteLine("\n=============================================");
}
}
diff --git a/Services/ServiceFactory.cs b/MelonLoader/MelonLoaderServiceFactory.cs
similarity index 95%
rename from Services/ServiceFactory.cs
rename to MelonLoader/MelonLoaderServiceFactory.cs
index 7e03b1c..99ed812 100644
--- a/Services/ServiceFactory.cs
+++ b/MelonLoader/MelonLoaderServiceFactory.cs
@@ -2,16 +2,15 @@
using MelonLoader;
using MLVScan.Abstractions;
using MLVScan.Adapters;
-using MLVScan.MelonLoader;
using MLVScan.Models;
using MLVScan.Services;
-namespace MLVScan
+namespace MLVScan.MelonLoader
{
///
/// Factory for creating MLVScan services in the MelonLoader context.
///
- public class ServiceFactory
+ public class MelonLoaderServiceFactory
{
private readonly MelonLogger.Instance _melonLogger;
private readonly IScanLogger _scanLogger;
@@ -20,7 +19,7 @@ public class ServiceFactory
private readonly MelonPlatformEnvironment _environment;
private readonly ScanConfig _fallbackConfig;
- public ServiceFactory(MelonLogger.Instance logger)
+ public MelonLoaderServiceFactory(MelonLogger.Instance logger)
{
_melonLogger = logger ?? throw new ArgumentNullException(nameof(logger));
_scanLogger = new MelonScanLogger(logger);
diff --git a/PlatformConstants.cs b/PlatformConstants.cs
index a974ade..468ff41 100644
--- a/PlatformConstants.cs
+++ b/PlatformConstants.cs
@@ -17,6 +17,16 @@ public static class PlatformConstants
/// Platform name identifier.
///
public const string PlatformName = "MLVScan.MelonLoader";
+#elif BEPINEX6_IL2CPP
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx6.IL2CPP";
+#elif BEPINEX6_MONO
+ ///
+ /// Platform name identifier.
+ ///
+ public const string PlatformName = "MLVScan.BepInEx6.Mono";
#elif BEPINEX
///
/// Platform name identifier.
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index 1bba7be..d766b75 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -4,6 +4,7 @@
using System.Text;
using MLVScan.Abstractions;
using MLVScan.Models;
+using MLVScan.Models.Rules;
namespace MLVScan.Services
{
@@ -83,13 +84,41 @@ public void GenerateConsoleReport(string modName, List findings)
foreach (var finding in ruleGroup.Take(3))
{
_logger.Info($" - {finding.Location}");
+
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ _logger.Info(" Call Chain:");
+ foreach (var node in finding.CallChain.Nodes.Take(3))
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ _logger.Info($" {prefix} {node.Location}");
+ }
+ if (finding.CallChain.Nodes.Count > 3)
+ {
+ _logger.Info($" ... and {finding.CallChain.Nodes.Count - 3} more");
+ }
+ }
+
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ _logger.Info($" Data Flow: {finding.DataFlowChain.Pattern} ({finding.DataFlowChain.Confidence * 100:F0}%)");
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ _logger.Info($" Cross-method: {finding.DataFlowChain.InvolvedMethods.Count} methods");
+ }
+ }
}
if (count > 3)
{
_logger.Info($" ... and {count - 3} more");
}
- _logger.Info("");
_logger.Info("--------------------------------------");
}
@@ -170,12 +199,76 @@ public string GenerateFileReport(string modName, string hash, List
foreach (var finding in ruleGroup)
{
sb.AppendLine($"Location: {finding.Location}");
+
+ // Show call chain if available
+ if (finding.HasCallChain && finding.CallChain != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("--- CALL CHAIN ANALYSIS ---");
+ sb.AppendLine(finding.CallChain.Summary);
+ sb.AppendLine();
+ sb.AppendLine("Attack Path:");
+ foreach (var node in finding.CallChain.Nodes)
+ {
+ var prefix = node.NodeType switch
+ {
+ CallChainNodeType.EntryPoint => "[ENTRY]",
+ CallChainNodeType.IntermediateCall => "[CALL]",
+ CallChainNodeType.SuspiciousDeclaration => "[DECL]",
+ _ => "[???"
+ };
+ sb.AppendLine($" {prefix} {node.Location}");
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ sb.AppendLine($" {node.Description}");
+ }
+ }
+ }
+
+ // Show data flow chain if available
+ if (finding.HasDataFlow && finding.DataFlowChain != null)
+ {
+ sb.AppendLine();
+ sb.AppendLine("--- DATA FLOW ANALYSIS ---");
+ sb.AppendLine($"Pattern: {finding.DataFlowChain.Pattern}");
+ sb.AppendLine($"Confidence: {finding.DataFlowChain.Confidence * 100:F0}%");
+ sb.AppendLine(finding.DataFlowChain.Summary);
+
+ if (finding.DataFlowChain.IsCrossMethod)
+ {
+ sb.AppendLine();
+ sb.AppendLine("Cross-Method Flow:");
+ foreach (var method in finding.DataFlowChain.InvolvedMethods)
+ {
+ sb.AppendLine($" - {method}");
+ }
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("Data Flow Path:");
+ for (int i = 0; i < finding.DataFlowChain.Nodes.Count; i++)
+ {
+ var node = finding.DataFlowChain.Nodes[i];
+ var arrow = i > 0 ? " -> " : " ";
+ var prefix = node.NodeType switch
+ {
+ DataFlowNodeType.Source => "[SOURCE]",
+ DataFlowNodeType.Transform => "[TRANSFORM]",
+ DataFlowNodeType.Sink => "[SINK]",
+ DataFlowNodeType.Intermediate => "[PASS]",
+ _ => "[????]"
+ };
+ sb.AppendLine($"{arrow}{prefix} {node.Operation} ({node.DataDescription})");
+ sb.AppendLine($"{new string(' ', arrow.Length)} Location: {node.Location}");
+ }
+ }
+
if (!string.IsNullOrEmpty(finding.CodeSnippet))
{
sb.AppendLine("Code Snippet:");
sb.AppendLine(finding.CodeSnippet);
}
- sb.AppendLine("");
+ sb.AppendLine();
}
}
From 0a450867acc8c89eec81d3dfa1e3014cc5b35eee Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 18:50:26 -0800
Subject: [PATCH 05/28] Add MLVScan.Core project and expand build configs
Added the MLVScan.Core project to the solution.
---
MLVScan.sln | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 82 insertions(+), 2 deletions(-)
diff --git a/MLVScan.sln b/MLVScan.sln
index 03c91d8..3adc09f 100644
--- a/MLVScan.sln
+++ b/MLVScan.sln
@@ -1,26 +1,106 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.13.35931.197 d17.13
+VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan", "MLVScan.csproj", "{219261F3-C447-4B11-80CA-B4149CECF3BE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan.Core", "..\MLVScan.Core\MLVScan.Core.csproj", "{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
MelonLoader|Any CPU = MelonLoader|Any CPU
+ MelonLoader|x64 = MelonLoader|x64
+ MelonLoader|x86 = MelonLoader|x86
BepInEx|Any CPU = BepInEx|Any CPU
+ BepInEx|x64 = BepInEx|x64
+ BepInEx|x86 = BepInEx|x86
BepInEx6Mono|Any CPU = BepInEx6Mono|Any CPU
+ BepInEx6Mono|x64 = BepInEx6Mono|x64
+ BepInEx6Mono|x86 = BepInEx6Mono|x86
BepInEx6IL2CPP|Any CPU = BepInEx6IL2CPP|Any CPU
+ BepInEx6IL2CPP|x64 = BepInEx6IL2CPP|x64
+ BepInEx6IL2CPP|x86 = BepInEx6IL2CPP|x86
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.ActiveCfg = MelonLoader|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|Any CPU.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x64.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x64.Build.0 = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x86.ActiveCfg = MelonLoader|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.MelonLoader|x86.Build.0 = MelonLoader|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.ActiveCfg = BepInEx|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|Any CPU.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x64.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x64.Build.0 = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x86.ActiveCfg = BepInEx|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx|x86.Build.0 = BepInEx|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.ActiveCfg = BepInEx6Mono|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|Any CPU.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x64.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x64.Build.0 = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x86.ActiveCfg = BepInEx6Mono|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6Mono|x86.Build.0 = BepInEx6Mono|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.ActiveCfg = BepInEx6IL2CPP|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|Any CPU.Build.0 = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x64.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x64.Build.0 = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x86.ActiveCfg = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.BepInEx6IL2CPP|x86.Build.0 = BepInEx6IL2CPP|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x64.Build.0 = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Debug|x86.Build.0 = Debug|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.ActiveCfg = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.Build.0 = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.ActiveCfg = Release|Any CPU
+ {219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
From e207206b61107382e86b1b8df2ff4cf5db4d943b Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:22:34 -0800
Subject: [PATCH 06/28] Update BepInEx/5/BepInEx5Patcher.cs
fix: Inaccurate logging
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
BepInEx/5/BepInEx5Patcher.cs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/BepInEx/5/BepInEx5Patcher.cs b/BepInEx/5/BepInEx5Patcher.cs
index 2d83ff0..9057151 100644
--- a/BepInEx/5/BepInEx5Patcher.cs
+++ b/BepInEx/5/BepInEx5Patcher.cs
@@ -96,10 +96,15 @@ public static void Initialize()
_logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
}
}
+ else if (!config.EnableAutoScan)
+ {
+ _logger.LogInfo("Automatic scanning is disabled in configuration.");
+ }
else
{
_logger.LogInfo("No suspicious plugins detected.");
}
+ }
_logger.LogInfo("MLVScan preloader scan complete.");
}
From e0f263cee558062966f4de77b32606055277577f Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:23:43 -0800
Subject: [PATCH 07/28] Update Services/DeveloperReportGenerator.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
Services/DeveloperReportGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index d766b75..7f63778 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -215,7 +215,7 @@ public string GenerateFileReport(string modName, string hash, List
CallChainNodeType.EntryPoint => "[ENTRY]",
CallChainNodeType.IntermediateCall => "[CALL]",
CallChainNodeType.SuspiciousDeclaration => "[DECL]",
- _ => "[???"
+ _ => "[???]"
};
sb.AppendLine($" {prefix} {node.Location}");
if (!string.IsNullOrEmpty(node.Description))
From 168ca7dafbf96b45677fb3eba53566c08f62cb64 Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:24:04 -0800
Subject: [PATCH 08/28] Update Services/DeveloperReportGenerator.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
Services/DeveloperReportGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index 7f63778..101e16e 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -95,7 +95,7 @@ public void GenerateConsoleReport(string modName, List findings)
CallChainNodeType.EntryPoint => "[ENTRY]",
CallChainNodeType.IntermediateCall => "[CALL]",
CallChainNodeType.SuspiciousDeclaration => "[DECL]",
- _ => "[???"
+ _ => "[???]"
};
_logger.Info($" {prefix} {node.Location}");
}
From d6dcf96f6d3f43d796e779d7e5c90afd117ddd45 Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:24:31 -0800
Subject: [PATCH 09/28] Update BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
index b789fdf..77d38ca 100644
--- a/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
+++ b/BepInEx/6/IL2CPP/BepInEx6IL2CppPatcher.cs
@@ -85,6 +85,10 @@ public override void Initialize()
_logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
}
}
+ else if (!config.EnableAutoScan)
+ {
+ _logger.LogInfo("Automatic scanning is disabled in configuration.");
+ }
else
{
_logger.LogInfo("No suspicious plugins detected.");
From a35f82200e875bf7c919dbc1f10b3f5252d1412e Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:27:37 -0800
Subject: [PATCH 10/28] feat: Build Workflow
---
.github/workflows/build.yml | 62 +++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 .github/workflows/build.yml
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..799355c
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,62 @@
+name: Build MLVScan
+
+on:
+ push:
+ branches: [ "master", "main" ]
+ pull_request:
+ branches: [ "master", "main" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore MLVScan.sln
+
+ # MelonLoader
+ - name: Build MelonLoader
+ run: dotnet build MLVScan.csproj -c MelonLoader --no-restore
+
+ - name: Upload MelonLoader Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: MLVScan.MelonLoader
+ path: bin/MelonLoader/**/MLVScan.MelonLoader.dll
+
+ # BepInEx 5
+ - name: Build BepInEx
+ run: dotnet build MLVScan.csproj -c BepInEx --no-restore
+
+ - name: Upload BepInEx Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: MLVScan.BepInEx
+ path: bin/BepInEx/**/MLVScan.BepInEx.dll
+
+ # BepInEx 6 Mono
+ - name: Build BepInEx6Mono
+ run: dotnet build MLVScan.csproj -c BepInEx6Mono --no-restore
+
+ - name: Upload BepInEx6Mono Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: MLVScan.BepInEx6.Mono
+ path: bin/BepInEx6Mono/**/MLVScan.BepInEx6.Mono.dll
+
+ # BepInEx 6 IL2CPP
+ - name: Build BepInEx6IL2CPP
+ run: dotnet build MLVScan.csproj -c BepInEx6IL2CPP --no-restore
+
+ - name: Upload BepInEx6IL2CPP Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: MLVScan.BepInEx6.IL2CPP
+ path: bin/BepInEx6IL2CPP/**/MLVScan.BepInEx6.IL2CPP.dll
From 2a24166ffb470b9292acbab97f5880330f12f044 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:27:46 -0800
Subject: [PATCH 11/28] fix: Inaccurate logging
---
BepInEx/6/Mono/BepInEx6MonoPatcher.cs | 34 +++++++++++++++------------
1 file changed, 19 insertions(+), 15 deletions(-)
diff --git a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
index 6378a5d..cf24036 100644
--- a/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
+++ b/BepInEx/6/Mono/BepInEx6MonoPatcher.cs
@@ -68,26 +68,30 @@ public override void Initialize()
var pluginDisabler = new BepInExPluginDisabler(scanLogger, config);
var reportGenerator = new BepInExReportGenerator(_logger, config);
- // Scan all plugins
- var scanResults = pluginScanner.ScanAllPlugins();
-
- if (scanResults.Count > 0)
+ if (!config.EnableAutoScan)
{
- // Disable suspicious plugins
- var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+ _logger.LogInfo("Auto-scan is disabled. Skipping plugin scan.");
+ }
+ else
+ {
+ var scanResults = pluginScanner.ScanAllPlugins();
- // Generate reports for disabled plugins
- if (disabledPlugins.Count > 0)
+ if (scanResults.Count > 0)
{
- reportGenerator.GenerateReports(disabledPlugins, scanResults);
+ var disabledPlugins = pluginDisabler.DisableSuspiciousPlugins(scanResults);
+
+ if (disabledPlugins.Count > 0)
+ {
+ reportGenerator.GenerateReports(disabledPlugins, scanResults);
- _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
- _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+ _logger.LogWarning($"MLVScan blocked {disabledPlugins.Count} suspicious plugin(s).");
+ _logger.LogWarning("Check BepInEx/MLVScan/Reports/ for details.");
+ }
+ }
+ else
+ {
+ _logger.LogInfo("No suspicious plugins detected.");
}
- }
- else
- {
- _logger.LogInfo("No suspicious plugins detected.");
}
_logger.LogInfo("MLVScan BepInEx 6 (Mono) preloader scan complete.");
From 51c3601f193c5588a1b79d5262cc5f68dd2a990f Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:27:27 -0800
Subject: [PATCH 12/28] Update README.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 063fee5..0ca14a9 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
**MLVScan** is a security-focused mod loader plugin that protects your game by scanning mods for malicious patterns *before* they execute.
-Supports **MelonLoader** and **BepInEx 5.x**.
+Supports **MelonLoader**, **BepInEx 5.x**, and **BepInEx 6.x** (IL2CPP/Mono).

From ad0ca67e9d43dab74b2662d9546c2ca6a2b14137 Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:28:30 -0800
Subject: [PATCH 13/28] Update Services/PluginScannerBase.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
Services/PluginScannerBase.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Services/PluginScannerBase.cs b/Services/PluginScannerBase.cs
index f778f4f..c9a7233 100644
--- a/Services/PluginScannerBase.cs
+++ b/Services/PluginScannerBase.cs
@@ -131,7 +131,7 @@ protected virtual void ScanSingleFile(string filePath, Dictionary= Config.SuspiciousThreshold)
{
- results.Add(filePath, actualFindings);
+ results[filePath] = actualFindings;
Logger.Warning($"Found {actualFindings.Count} suspicious patterns in {fileName}");
}
}
From 2fef5233b130dbfa7a551c547d324a860421a9aa Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:31:00 -0800
Subject: [PATCH 14/28] Update MLVScan.sln
---
MLVScan.sln | 50 +++++++++++++++++++++++++-------------------------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/MLVScan.sln b/MLVScan.sln
index 3adc09f..f13bf33 100644
--- a/MLVScan.sln
+++ b/MLVScan.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197
@@ -65,30 +65,30 @@ Global
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.Build.0 = Release|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.ActiveCfg = Release|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Debug|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Release|Any CPU
+ {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Release|Any CPU
{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU
From 29b669b9dddea87ffa7b316c0d04fc56210c2245 Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:30:16 -0800
Subject: [PATCH 15/28] Update BepInEx/5/BepInEx5Patcher.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
BepInEx/5/BepInEx5Patcher.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/BepInEx/5/BepInEx5Patcher.cs b/BepInEx/5/BepInEx5Patcher.cs
index 9057151..16144b3 100644
--- a/BepInEx/5/BepInEx5Patcher.cs
+++ b/BepInEx/5/BepInEx5Patcher.cs
@@ -104,7 +104,6 @@ public static void Initialize()
{
_logger.LogInfo("No suspicious plugins detected.");
}
- }
_logger.LogInfo("MLVScan preloader scan complete.");
}
From e5e0fc81cf9541f629aba4dd51bfe8ebb9a0b8d3 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:38:32 -0800
Subject: [PATCH 16/28] Update build config, fix typo, and clarify README
Refactored MLVScan.csproj to improve ILRepack library handling and removed redundant build targets. Fixed a minor typo in MelonLoaderPlugin.cs. Expanded and clarified README instructions for BepInEx 5.x and 6.x usage and installation.
---
MLVScan.csproj | 32 +++-----------------------------
MelonLoader/MelonLoaderPlugin.cs | 4 ++--
README.md | 10 +++++++++-
3 files changed, 14 insertions(+), 32 deletions(-)
diff --git a/MLVScan.csproj b/MLVScan.csproj
index fd4e2b3..7b76117 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -212,8 +212,10 @@
+
+
-
+
@@ -250,32 +252,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MelonLoader/MelonLoaderPlugin.cs b/MelonLoader/MelonLoaderPlugin.cs
index 9db8d3b..cf64e3d 100644
--- a/MelonLoader/MelonLoaderPlugin.cs
+++ b/MelonLoader/MelonLoaderPlugin.cs
@@ -233,7 +233,7 @@ private void GenerateDetailedReports(List disabledMods, Dict
CallChainNodeType.EntryPoint => "[ENTRY]",
CallChainNodeType.IntermediateCall => "[CALL]",
CallChainNodeType.SuspiciousDeclaration => "[DECL]",
- _ => "[???"
+ _ => "[???]"
};
LoggerInstance.Msg($" {prefix} {node.Location}");
}
@@ -357,7 +357,7 @@ private void GenerateDetailedReports(List disabledMods, Dict
CallChainNodeType.EntryPoint => "[ENTRY]",
CallChainNodeType.IntermediateCall => "[CALL]",
CallChainNodeType.SuspiciousDeclaration => "[DECL]",
- _ => "[???"
+ _ => "[???]"
};
writer.WriteLine($" {prefix} {node.Location}");
if (!string.IsNullOrEmpty(node.Description))
diff --git a/README.md b/README.md
index 0ca14a9..f69b2f0 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,18 @@ Detailed documentation is available in the **[MLVScan Wiki](https://github.com/i
- Configuration stored in `MelonPreferences.cfg`
- Reports saved to `UserData/MLVScan/Reports/`
-**BepInEx:**
+**BepInEx 5.x:**
- Runs as a preloader patcher (scans before chainloader)
- Configuration stored in `BepInEx/config/MLVScan.json`
- Reports saved to `BepInEx/MLVScan/Reports/`
+- Install via `BepInEx/patchers` folder
+
+**BepInEx 6.x (IL2CPP / Mono):**
+- Runs as a preloader patcher (scans before chainloader)
+- Configuration stored in `BepInEx/config/MLVScan.json` (same as 5.x)
+- Reports saved to `BepInEx/MLVScan/Reports/` (same as 5.x)
+- Uses `[PatcherPlugin]` attribute-based packaging instead of patchers folder
+- Plugin compatibility: BepInEx 5.x plugins may require updating for 6.x API changes
## 🛡️ Powered by MLVScan.Core
From 033d9de632da33bbe10d6e68c4c8069d9ff77797 Mon Sep 17 00:00:00 2001
From: ifBars <114284668+ifBars@users.noreply.github.com>
Date: Thu, 29 Jan 2026 19:37:21 -0800
Subject: [PATCH 17/28] Update MelonLoader/MelonEnvironment.cs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
MelonLoader/MelonEnvironment.cs | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/MelonLoader/MelonEnvironment.cs b/MelonLoader/MelonEnvironment.cs
index 34e4bdc..a9a64fa 100644
--- a/MelonLoader/MelonEnvironment.cs
+++ b/MelonLoader/MelonEnvironment.cs
@@ -54,10 +54,18 @@ public string ManagedDirectory
{
get
{
- // Unity managed assemblies location
- var managedPath = Path.Combine(_gameRoot, "Schedule I_Data", "Managed");
- if (Directory.Exists(managedPath))
- return managedPath;
+ // Find Unity data folder dynamically (pattern: *_Data)
+ try
+ {
+ var dataFolders = Directory.GetDirectories(_gameRoot, "*_Data");
+ foreach (var dataFolder in dataFolders)
+ {
+ var managedPath = Path.Combine(dataFolder, "Managed");
+ if (Directory.Exists(managedPath))
+ return managedPath;
+ }
+ }
+ catch { /* Ignore enumeration errors */ }
// Fallback for Il2Cpp games
var il2cppPath = Path.Combine(_gameRoot, "MelonLoader", "Managed");
From 3f94267bf31bd3fc6d4bd462c43414121831c1f5 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:40:29 -0800
Subject: [PATCH 18/28] Update MelonLoaderServiceFactory.cs
---
MelonLoader/MelonLoaderServiceFactory.cs | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/MelonLoader/MelonLoaderServiceFactory.cs b/MelonLoader/MelonLoaderServiceFactory.cs
index 99ed812..9da899c 100644
--- a/MelonLoader/MelonLoaderServiceFactory.cs
+++ b/MelonLoader/MelonLoaderServiceFactory.cs
@@ -38,8 +38,17 @@ public MelonLoaderServiceFactory(MelonLogger.Instance logger)
}
}
+ ///
+ /// Creates the configuration manager.
+ ///
+ /// The MelonConfigManager instance.
+ /// Thrown when the configuration manager is unavailable due to initialization failure.
public MelonConfigManager CreateConfigManager()
{
+ if (_configManager == null)
+ {
+ throw new InvalidOperationException("Configuration manager unavailable: failed to initialize during factory construction.");
+ }
return _configManager;
}
From 147b6e25b08f8654fc2192e70d6bf25c0632e352 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:43:49 -0800
Subject: [PATCH 19/28] Update MelonPluginScanner.cs
---
MelonLoader/MelonPluginScanner.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/MelonLoader/MelonPluginScanner.cs b/MelonLoader/MelonPluginScanner.cs
index cad1ce2..888322b 100644
--- a/MelonLoader/MelonPluginScanner.cs
+++ b/MelonLoader/MelonPluginScanner.cs
@@ -29,10 +29,9 @@ public MelonPluginScanner(
protected override IEnumerable GetScanDirectories()
{
- // Configured directories relative to game root
foreach (var scanDir in Config.ScanDirectories)
{
- yield return Path.Combine(MelonEnvironment.GameRootDirectory, scanDir);
+ yield return Path.Combine(_environment.GameRootDirectory, scanDir);
}
}
From 3d24b8e1c94e10c13f2f48377c1fc8142b4e1013 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:43:51 -0800
Subject: [PATCH 20/28] Create local.build.props.example
---
local.build.props.example | 47 +++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 local.build.props.example
diff --git a/local.build.props.example b/local.build.props.example
new file mode 100644
index 0000000..baba87c
--- /dev/null
+++ b/local.build.props.example
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From bdb590b3378dcc1e883e5d7d55c8c0ca987e9776 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:47:08 -0800
Subject: [PATCH 21/28] Update DeveloperReportGenerator.cs
---
Services/DeveloperReportGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Services/DeveloperReportGenerator.cs b/Services/DeveloperReportGenerator.cs
index 101e16e..6dc7160 100644
--- a/Services/DeveloperReportGenerator.cs
+++ b/Services/DeveloperReportGenerator.cs
@@ -256,7 +256,7 @@ public string GenerateFileReport(string modName, string hash, List
DataFlowNodeType.Transform => "[TRANSFORM]",
DataFlowNodeType.Sink => "[SINK]",
DataFlowNodeType.Intermediate => "[PASS]",
- _ => "[????]"
+ _ => "[???]"
};
sb.AppendLine($"{arrow}{prefix} {node.Operation} ({node.DataDescription})");
sb.AppendLine($"{new string(' ', arrow.Length)} Location: {node.Location}");
From bcfca00df5cc1b81ba0f5ddef692ad3e29befa62 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:47:09 -0800
Subject: [PATCH 22/28] Update BepInExEnvironment.cs
---
BepInEx/BepInExEnvironment.cs | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/BepInEx/BepInExEnvironment.cs b/BepInEx/BepInExEnvironment.cs
index c91b6f4..54c703f 100644
--- a/BepInEx/BepInExEnvironment.cs
+++ b/BepInEx/BepInExEnvironment.cs
@@ -13,19 +13,18 @@ public class BepInExPlatformEnvironment : IPlatformEnvironment
{
private readonly string _dataDir;
private readonly string _reportsDir;
+ private readonly string[] _pluginDirectories;
public BepInExPlatformEnvironment()
{
_dataDir = Path.Combine(Paths.BepInExRootPath, "MLVScan");
_reportsDir = Path.Combine(_dataDir, "Reports");
+ _pluginDirectories = new[] { Paths.PluginPath };
}
public string GameRootDirectory => Paths.GameRootPath;
- public string[] PluginDirectories => new[]
- {
- Paths.PluginPath
- };
+ public string[] PluginDirectories => _pluginDirectories;
public string DataDirectory
{
From 617b47defd74d1d8be7d13c6f3424bc895414218 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:51:53 -0800
Subject: [PATCH 23/28] fix: Use MLVScan.Core from NuGet
---
MLVScan.csproj | 10 ++--------
MLVScan.sln | 40 ++--------------------------------------
2 files changed, 4 insertions(+), 46 deletions(-)
diff --git a/MLVScan.csproj b/MLVScan.csproj
index 7b76117..bc966cd 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -190,11 +190,8 @@
-
-
-
-
+
@@ -207,10 +204,7 @@
-
-
-
-
+
diff --git a/MLVScan.sln b/MLVScan.sln
index f13bf33..928fada 100644
--- a/MLVScan.sln
+++ b/MLVScan.sln
@@ -5,8 +5,7 @@ VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan", "MLVScan.csproj", "{219261F3-C447-4B11-80CA-B4149CECF3BE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLVScan.Core", "..\MLVScan.Core\MLVScan.Core.csproj", "{CBE47B78-2460-4C71-B9E6-CBEC26AAED73}"
-EndProject
+
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
MelonLoader|Any CPU = MelonLoader|Any CPU
@@ -65,42 +64,7 @@ Global
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x64.Build.0 = Release|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.ActiveCfg = Release|Any CPU
{219261F3-C447-4B11-80CA-B4149CECF3BE}.Release|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|Any CPU.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x64.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.MelonLoader|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|Any CPU.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x64.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|Any CPU.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x64.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6Mono|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|Any CPU.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x64.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.BepInEx6IL2CPP|x86.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x64.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.ActiveCfg = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Debug|x86.Build.0 = Debug|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|Any CPU.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x64.Build.0 = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.ActiveCfg = Release|Any CPU
- {CBE47B78-2460-4C71-B9E6-CBEC26AAED73}.Release|x86.Build.0 = Release|Any CPU
+
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
From 5ecedddb05764814e223a2a37e16df0d56a61a1e Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 19:53:59 -0800
Subject: [PATCH 24/28] Update MLVScan.csproj
---
MLVScan.csproj | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/MLVScan.csproj b/MLVScan.csproj
index bc966cd..c45fe96 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -136,16 +136,21 @@
-
+
+
+
+
+
+
+
$(MelonLoaderPath)\MelonLoader.dll
false
-
+
$(MelonLoaderPath)\0Harmony.dll
false
-
-
+
$(GameManagedPath)\UnityEngine.CoreModule.dll
false
From 7987c23fa0a7f59657125a3a3af2ea9a2a5e9c1c Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 20:07:12 -0800
Subject: [PATCH 25/28] fix: CI
---
.github/workflows/build.yml | 4 ++--
MLVScan.csproj | 47 ++++++++++++++++++++++++-------------
2 files changed, 33 insertions(+), 18 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 799355c..d751d44 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,7 +41,7 @@ jobs:
name: MLVScan.BepInEx
path: bin/BepInEx/**/MLVScan.BepInEx.dll
- # BepInEx 6 Mono
+ # BepInEx 6.x Mono
- name: Build BepInEx6Mono
run: dotnet build MLVScan.csproj -c BepInEx6Mono --no-restore
@@ -51,7 +51,7 @@ jobs:
name: MLVScan.BepInEx6.Mono
path: bin/BepInEx6Mono/**/MLVScan.BepInEx6.Mono.dll
- # BepInEx 6 IL2CPP
+ # BepInEx 6.x IL2CPP
- name: Build BepInEx6IL2CPP
run: dotnet build MLVScan.csproj -c BepInEx6IL2CPP --no-restore
diff --git a/MLVScan.csproj b/MLVScan.csproj
index c45fe96..9752f1c 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -17,6 +17,16 @@
MelonLoader;BepInEx;BepInEx6Mono;BepInEx6IL2CPP
+
+
+
+
+
+ https://api.nuget.org/v3/index.json;
+ https://nuget.bepinex.dev/v3/index.json
+
+
+
@@ -137,8 +147,8 @@
-
-
+
+
@@ -157,11 +167,13 @@
-
+
-
-
+
+
@@ -176,19 +188,21 @@
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
@@ -211,14 +225,15 @@
+
-
-
-
-
+
+
+
+
"$(PkgILRepack)\tools\ILRepack.exe" /parallel /union
From 98ab2b202ad6db8b8a4c9b6d17c5f8215e480385 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 20:09:34 -0800
Subject: [PATCH 26/28] Update build.yml
---
.github/workflows/build.yml | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d751d44..31d6cd0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,10 +18,10 @@ jobs:
with:
dotnet-version: '8.0.x'
- - name: Restore dependencies
- run: dotnet restore MLVScan.sln
-
# MelonLoader
+ - name: Restore MelonLoader dependencies
+ run: dotnet restore MLVScan.csproj -p:Configuration=MelonLoader
+
- name: Build MelonLoader
run: dotnet build MLVScan.csproj -c MelonLoader --no-restore
@@ -32,6 +32,9 @@ jobs:
path: bin/MelonLoader/**/MLVScan.MelonLoader.dll
# BepInEx 5
+ - name: Restore BepInEx dependencies
+ run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx
+
- name: Build BepInEx
run: dotnet build MLVScan.csproj -c BepInEx --no-restore
@@ -42,6 +45,9 @@ jobs:
path: bin/BepInEx/**/MLVScan.BepInEx.dll
# BepInEx 6.x Mono
+ - name: Restore BepInEx6Mono dependencies
+ run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx6Mono
+
- name: Build BepInEx6Mono
run: dotnet build MLVScan.csproj -c BepInEx6Mono --no-restore
@@ -52,6 +58,9 @@ jobs:
path: bin/BepInEx6Mono/**/MLVScan.BepInEx6.Mono.dll
# BepInEx 6.x IL2CPP
+ - name: Restore BepInEx6IL2CPP dependencies
+ run: dotnet restore MLVScan.csproj -p:Configuration=BepInEx6IL2CPP
+
- name: Build BepInEx6IL2CPP
run: dotnet build MLVScan.csproj -c BepInEx6IL2CPP --no-restore
From 2ab298d877247a331a35bf93668e6ffa997e89b4 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 20:10:41 -0800
Subject: [PATCH 27/28] Update MLVScan.csproj
---
MLVScan.csproj | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/MLVScan.csproj b/MLVScan.csproj
index 9752f1c..e9a7b56 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -212,7 +212,7 @@
-
+
@@ -236,14 +236,15 @@
- "$(PkgILRepack)\tools\ILRepack.exe" /parallel /union
- /lib:"@(ILRepackLib, '" /lib:"')"
- $(ILRepackArgs) $(ILRepackLibArgs)
- $(ILRepackArgs) /out:"$(OutputPath)$(AssemblyName).merged.dll"
- $(ILRepackArgs) "@(InputAssemblies, '" "')"
+ $(OutputPath)$(AssemblyName).merged.dll
-
-
+
+
From 1a0eaf50851d686759c0c17ad3b86ba912bfdfa3 Mon Sep 17 00:00:00 2001
From: ifBars
Date: Thu, 29 Jan 2026 20:13:54 -0800
Subject: [PATCH 28/28] fix: CI Repack
---
.github/workflows/build.yml | 5 +++++
MLVScan.csproj | 20 ++++++++++----------
2 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 31d6cd0..e1601fa 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,6 +17,11 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
+
+ - name: Install Mono (for ILRepack)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y mono-complete
# MelonLoader
- name: Restore MelonLoader dependencies
diff --git a/MLVScan.csproj b/MLVScan.csproj
index e9a7b56..f26b150 100644
--- a/MLVScan.csproj
+++ b/MLVScan.csproj
@@ -212,7 +212,7 @@
-
+
@@ -220,7 +220,7 @@
-
+
@@ -237,13 +237,13 @@
$(OutputPath)$(AssemblyName).merged.dll
+ $(PkgILRepack)\tools\ILRepack.exe
+
+ "$(ILRepackExePath)"
+ mono "$(ILRepackExePath)"
+ /lib:"@(ILRepackLib, '" /lib:"')"
-
+
@@ -251,7 +251,7 @@
-
+
@@ -259,7 +259,7 @@
-
+