diff --git a/BepisLocaleLoader.csproj b/BepisLocaleLoader.csproj index b0c77fb..de6a0f3 100644 --- a/BepisLocaleLoader.csproj +++ b/BepisLocaleLoader.csproj @@ -3,7 +3,7 @@ 1.2.0 ResoniteModding - net9.0 + net10.0 https://github.com/ResoniteModding/BepisLocaleLoader https://github.com/ResoniteModding/BepisLocaleLoader git @@ -11,7 +11,7 @@ Bepis Locale Loader BepisLocaleLoader enable - disable + enable true false true @@ -29,16 +29,16 @@ - - - - + + + + - + @@ -68,12 +68,12 @@ - - + + - - + + diff --git a/ConfigLocale.cs b/ConfigLocale.cs index 20ceef8..10ca3a7 100644 --- a/ConfigLocale.cs +++ b/ConfigLocale.cs @@ -3,7 +3,6 @@ namespace BepisLocaleLoader; -// We could maybe add more things to this later if needed public struct ConfigLocale { public ConfigLocale(string name, string description) @@ -49,7 +48,7 @@ public static ConfigEntry BindLocalized(this ConfigFile config, string gui /// Name of the setting. /// Value of the setting if the setting was not created yet. /// Description and other metadata of the setting. The text description will be visible when editing config files via mod managers or manually. - public static ConfigEntry BindLocalized(this ConfigFile config, string guid, string section, string key, T defaultValue, ConfigDescription configDescription = null) + public static ConfigEntry BindLocalized(this ConfigFile config, string guid, string section, string key, T defaultValue, ConfigDescription? configDescription = null) { return config.BindLocalized(guid, new ConfigDefinition(section, key), defaultValue, configDescription); } @@ -63,19 +62,16 @@ public static ConfigEntry BindLocalized(this ConfigFile config, string gui /// Section and Key of the setting. /// Value of the setting if the setting was not created yet. /// Description and other metadata of the setting. The text description will be visible when editing config files via mod managers or manually. - public static ConfigEntry BindLocalized(this ConfigFile config, string guid, ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) + public static ConfigEntry BindLocalized(this ConfigFile config, string guid, ConfigDefinition configDefinition, T defaultValue, ConfigDescription? configDescription = null) { - var localeName = $"Settings.{guid}.{configDefinition.Section}.{configDefinition.Key}"; - var localeDescription = localeName + ".Description"; + string localeName = $"Settings.{guid}.{configDefinition.Section}.{configDefinition.Key}"; + string localeDescription = $"{localeName}.Description"; var locale = new ConfigLocale(localeName, localeDescription); - if(configDescription == null) - { - configDescription = new ConfigDescription(string.Empty, null, locale); - } - else - { - configDescription = new ConfigDescription(configDescription.Description, configDescription.AcceptableValues, [..configDescription.Tags, locale]); - } - return config.Bind(configDefinition, defaultValue, configDescription); + + string description = configDescription?.Description ?? string.Empty; + var acceptableValues = configDescription?.AcceptableValues; + object[] tags = configDescription != null ? [.. configDescription.Tags, locale] : [locale]; + + return config.Bind(configDefinition, defaultValue, new ConfigDescription(description, acceptableValues, tags)); } } \ No newline at end of file diff --git a/LocaleInjectionPatch.cs b/LocaleInjectionPatch.cs new file mode 100644 index 0000000..0d52df3 --- /dev/null +++ b/LocaleInjectionPatch.cs @@ -0,0 +1,190 @@ +using System.Diagnostics.CodeAnalysis; +using BepInEx; +using BepInEx.NET.Common; +using Elements.Assets; +using FrooxEngine; +using HarmonyLib; + +namespace BepisLocaleLoader; + +/// +/// Harmony patch that injects mod locales immediately after Resonite loads base locale files. +/// This eliminates race conditions by hooking directly into the locale loading flow. +/// +[HarmonyPatch(typeof(FrooxEngine.LocaleResource), "LoadTargetVariant")] +internal static class LocaleInjectionPatch +{ + private static readonly object _injectionLock = new(); + private static string _lastInjectedLocale = string.Empty; + private static DateTime _lastInjectionTime = DateTime.MinValue; + private static readonly TimeSpan _deduplicationWindow = TimeSpan.FromMilliseconds(500); + + /// + /// Postfix that runs after LoadTargetVariant completes. + /// Waits for the async method to finish, then injects all mod locales. + /// + [HarmonyPostfix] + private static async void Postfix(FrooxEngine.LocaleResource __instance, Task __result, LocaleVariantDescriptor? variant) + { + try + { + await __result.ConfigureAwait(false); + + if (__instance.Data == null) + { + Plugin.Log.LogWarning("LoadTargetVariant completed but Data is null - skipping locale injection"); + return; + } + + string targetLocale = variant?.LocaleCode ?? "en"; + + // Skip injection for temporary refresh triggers (RML uses "-" to force locale reload) + if (targetLocale == "-") + { + Plugin.Log.LogDebug("Skipping locale injection for refresh trigger (target: -)"); + return; + } + + lock (_injectionLock) + { + var now = DateTime.UtcNow; + if (_lastInjectedLocale == targetLocale && (now - _lastInjectionTime) < _deduplicationWindow) + { + Plugin.Log.LogDebug($"Skipping duplicate injection for {targetLocale} (called {(now - _lastInjectionTime).TotalMilliseconds:F0}ms after previous)"); + return; + } + + Plugin.Log.LogDebug($"Injecting mod locales after LoadTargetVariant completed (target: {targetLocale})"); + + _lastInjectedLocale = targetLocale; + _lastInjectionTime = now; + + InjectAllPluginLocales(__instance.Data, targetLocale); + } + } + catch (Exception ex) + { + Plugin.Log.LogError($"Failed to inject mod locales: {ex}"); + } + } + + /// + /// Discovers and injects locale files from all BepInEx plugins. + /// + private static void InjectAllPluginLocales(Elements.Assets.LocaleResource localeData, string targetLocale) + { + if (NetChainloader.Instance?.Plugins == null || NetChainloader.Instance.Plugins.Count == 0) + { + Plugin.Log.LogDebug("No BepInEx plugins loaded - skipping locale injection"); + return; + } + + int pluginCount = 0; + int messageCount = 0; + + foreach (var plugin in NetChainloader.Instance.Plugins.Values) + { + var localeFiles = LocaleLoader.GetPluginLocaleFiles(plugin).ToList(); + if (localeFiles.Count == 0) + continue; + + Plugin.Log.LogDebug($"Loading locales from {plugin.Metadata?.GUID ?? "unknown"}"); + + var candidates = LoadLocaleFiles(localeFiles); + var toInject = SelectMatchingLocales(candidates, targetLocale, out bool usingFallback); + + messageCount += InjectAndLogLocales(localeData, toInject, usingFallback); + + LocaleLoader.TrackPluginWithLocale(plugin); + pluginCount++; + } + + if (pluginCount > 0) + { + Plugin.Log.LogInfo($"Injected {messageCount} locale messages from {pluginCount} plugins"); + } + } + + /// + /// Loads locale data from the specified list of locale files. + /// + private static List<(string Path, LocaleData Data)> LoadLocaleFiles(List localeFiles) + { + var candidates = new List<(string Path, LocaleData Data)>(); + + foreach (string file in localeFiles) + { + var data = LocaleLoader.LoadLocaleDataFromFile(file); + if (data != null) + { + candidates.Add((file, data)); + } + } + + return candidates; + } + + /// + /// Selects locale data that matches the target locale, with fallback to English if no matches are found. + /// + private static List<(string Path, LocaleData Data)> SelectMatchingLocales( + List<(string Path, LocaleData Data)> candidates, + string targetLocale, + out bool usingFallback) + { + var matches = candidates.Where(c => IsLocaleMatch(c.Data.LocaleCode, targetLocale)).ToList(); + + usingFallback = false; + if (matches.Count == 0 && !IsLocaleMatch(targetLocale, "en")) + { + matches = candidates.Where(c => IsLocaleMatch(c.Data.LocaleCode, "en")).ToList(); + usingFallback = true; + } + + return matches; + } + + /// + /// Injects the selected locale data into the locale resource and logs the results. + /// + private static int InjectAndLogLocales( + Elements.Assets.LocaleResource localeData, + List<(string Path, LocaleData Data)> toInject, + bool usingFallback) + { + int messageCount = 0; + + foreach (var (file, data) in toInject) + { + localeData.LoadDataAdditively(data); + messageCount += data.Messages.Count; + + string fileLocale = data.LocaleCode ?? "unknown"; + string fallbackSuffix = usingFallback ? " (fallback)" : string.Empty; + Plugin.Log.LogDebug($" - {Path.GetFileName(file)}: {fileLocale}, {data.Messages.Count} messages{fallbackSuffix}"); + } + + return messageCount; + } + + /// + /// Checks if the file's locale matches the target locale. + /// Handles cases like "en-US" matching "en", or exact matches. + /// + private static bool IsLocaleMatch(string fileLocale, string targetLocale) + { + if (string.IsNullOrEmpty(fileLocale) || string.IsNullOrEmpty(targetLocale)) + return false; + + fileLocale = fileLocale.ToLowerInvariant(); + targetLocale = targetLocale.ToLowerInvariant(); + + if (fileLocale == targetLocale) + return true; + + string fileBase = Elements.Assets.LocaleResource.GetMainLanguage(fileLocale); + string targetBase = Elements.Assets.LocaleResource.GetMainLanguage(targetLocale); + + return fileBase == targetBase; + } +} \ No newline at end of file diff --git a/LocaleLoader.cs b/LocaleLoader.cs index 525ae18..6bf01e5 100644 --- a/LocaleLoader.cs +++ b/LocaleLoader.cs @@ -1,202 +1,223 @@ -using System.Text.Json; -using BepInEx; -using Elements.Assets; -using Elements.Core; -using FrooxEngine; - -namespace BepisLocaleLoader; - -// Edited Locale Code is from - https://github.com/Xlinka/Project-Obsidian/blob/main/ProjectObsidian/Settings/LocaleHelper.cs -public static class LocaleLoader -{ - private static StaticLocaleProvider _localeProvider; - private static string _lastOverrideLocale; - private const string OverrideLocaleString = "somethingRandomJustToMakeItChange"; - - private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }; - - public static readonly HashSet PluginsWithLocales = new HashSet(); - - public static void AddLocaleString(string rawString, string localeString, bool force = false, string authors = null) - { - List finalAuthors; - - if (!string.IsNullOrWhiteSpace(authors)) - { - finalAuthors = authors.Split(", ", StringSplitOptions.RemoveEmptyEntries).ToList(); - } - else if (!string.IsNullOrWhiteSpace(PluginMetadata.AUTHORS)) - { - finalAuthors = PluginMetadata.AUTHORS.Split(", ", StringSplitOptions.RemoveEmptyEntries).ToList(); - } - else - { - finalAuthors = new List { "BepInEx" }; - } - - LocaleData localeData = new LocaleData - { - LocaleCode = "en-US", - Authors = finalAuthors, - Messages = new Dictionary - { - { rawString, localeString } - } - }; - - Update(localeData, force); - } - - public static void AddLocaleFromPlugin(PluginInfo plugin) - { - string dir = Path.GetDirectoryName(plugin.Location); - string locale = Path.Combine(dir, "Locale"); - - if (!Path.Exists(locale)) return; - - Plugin.Log.LogDebug($"Adding locale for {plugin.Metadata.GUID}"); - ProcessPath(locale, AddLocaleFromFile); - - PluginsWithLocales.Add(plugin); - } - - public static void AddLocaleFromFile(string path) - { - if (!File.Exists(path)) return; - - string json = File.ReadAllText(path); - LocaleData localeData; - try - { - localeData = JsonSerializer.Deserialize(json, JsonOptions); - } - catch (Exception e) - { - Plugin.Log.LogError(e); - return; - } - - Plugin.Log.LogDebug($"- LocaleCode: {localeData.LocaleCode}, Message Count: {localeData.Messages.Count}"); - - Update(localeData, true); - } - - private static void Update(LocaleData localeData, bool force) - { - UpdateDelayed(localeData, force); - Settings.RegisterValueChanges(_ => UpdateDelayed(localeData, force)); - } - - private static void UpdateDelayed(LocaleData localeData, bool force) - { - Userspace.UserspaceWorld?.RunInUpdates(15, () => UpdateIntern(localeData, force)); - } - - private static void UpdateIntern(LocaleData localeData, bool force) - { - _localeProvider = Userspace.UserspaceWorld?.GetCoreLocale(); - if (_localeProvider?.Asset?.Data is null) - { - Userspace.UserspaceWorld?.RunSynchronously(() => UpdateIntern(localeData, force)); - } - else - { - UpdateLocale(localeData, force); - } - } - - private static void UpdateLocale(LocaleData localeData, bool force) - { - if (_localeProvider?.Asset?.Data != null) - { - if (!force) - { - string firstKey = localeData.Messages.Keys.FirstOrDefault(); - - bool alreadyExists = _localeProvider.Asset.Data.Messages.Any(ld => ld.Key == firstKey); - if (alreadyExists) return; - } - - _localeProvider.Asset.Data.LoadDataAdditively(localeData); - - // force asset update for locale provider - if (_localeProvider.OverrideLocale.Value != null && _localeProvider.OverrideLocale.Value != OverrideLocaleString) - { - _lastOverrideLocale = _localeProvider.OverrideLocale.Value; - } - - _localeProvider.OverrideLocale.Value = OverrideLocaleString; - Userspace.UserspaceWorld.RunInUpdates(1, () => { _localeProvider.OverrideLocale.Value = _lastOverrideLocale; }); - } - else if (_localeProvider?.Asset?.Data == null) - { - Plugin.Log.LogError("Locale data is null when it shouldn't be!"); - } - } - - private static void ProcessPath(string path, Action fileAction) - { - if (!Path.Exists(path)) - { - throw new DirectoryNotFoundException("Directory not found: " + path); - } - - ProcessDirectory(path, fileAction); - } - - private static void ProcessDirectory(string directory, Action fileAction) - { - string[] files = Directory.GetFiles(directory); - foreach (string file in files) - { - fileAction(file); - } - - string[] subdirectories = Directory.GetDirectories(directory); - foreach (string subdir in subdirectories) - { - ProcessDirectory(subdir, fileAction); - } - } - - private static string GetFormattedLocaleString(this string key, string format, Dictionary dict, (string, object)[] arguments) - { - Dictionary merged = dict != null ? new Dictionary(dict) : new Dictionary(); - - if (arguments != null) - { - foreach ((string name, object value) in arguments) - { - merged[name] = value; - } - } - - string formatted = _localeProvider?.Asset?.Format(key, merged); - - if (!string.IsNullOrWhiteSpace(format)) - { - formatted = string.Format(format, formatted); - } - - return formatted; - } - - public static string GetFormattedLocaleString(this string key) => key.GetFormattedLocaleString(null, null, null); - public static string GetFormattedLocaleString(this string key, string format) => key.GetFormattedLocaleString(format, null, null); - public static string GetFormattedLocaleString(this string key, string argName, object argField) => key.GetFormattedLocaleString(null, null, new (string, object)[] { (argName, argField) }); - public static string GetFormattedLocaleString(this string key, string format, string argName, object argField) => key.GetFormattedLocaleString(format, null, new (string, object)[] { (argName, argField) }); - public static string GetFormattedLocaleString(this string key, params (string, object)[] arguments) => key.GetFormattedLocaleString(null, null, arguments); - public static string GetFormattedLocaleString(this string key, string format, params (string, object)[] arguments) => key.GetFormattedLocaleString(format, null, arguments); - - public static LocaleString T(this string str, string argName, object argField) => str.AsLocaleKey(null, (argName, argField)); - public static LocaleString T(this string str, string format, string argName, object argField) => str.AsLocaleKey(format, (argName, argField)); - public static LocaleString T(this string str, params (string, object)[] arguments) => str.AsLocaleKey(null, arguments); - public static LocaleString T(this string str, string format, params (string, object)[] arguments) => str.AsLocaleKey(format, arguments); - public static LocaleString T(this string str, bool continuous, Dictionary arguments = null) => new LocaleString(str, null, true, continuous, arguments); - public static LocaleString T(this string str, string format = null, bool continuous = true, Dictionary arguments = null) => new LocaleString(str, format, true, continuous, arguments); -} \ No newline at end of file +using System.Text.Json; +using BepInEx; +using Elements.Assets; +using Elements.Core; +using FrooxEngine; + +namespace BepisLocaleLoader; + +/// +/// Public API for locale loading. +/// Primary locale injection happens via Harmony patch in LocaleInjectionPatch. +/// This class provides runtime APIs for adding locales after initial load. +/// +public static class LocaleLoader +{ + internal static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + private static readonly object _pluginsLock = new(); + + /// + /// Tracks which plugins have locale files. + /// + public static readonly HashSet PluginsWithLocales = new(); + + /// + /// Thread-safe method to add a plugin to the tracking set. + /// + internal static void TrackPluginWithLocale(PluginInfo plugin) + { + lock (_pluginsLock) + { + PluginsWithLocales.Add(plugin); + } + } + + /// + /// Add a single locale string at runtime. + /// For bulk loading, use the Locale/ folder in your plugin directory. + /// + public static void AddLocaleString(string rawString, string localeString, bool force = false, string? authors = null) + { + string authorsSource = !string.IsNullOrWhiteSpace(authors) ? authors + : !string.IsNullOrWhiteSpace(Plugin.AUTHORS) ? Plugin.AUTHORS + : "BepInEx"; + + List finalAuthors = authorsSource.Split(", ", StringSplitOptions.RemoveEmptyEntries).ToList(); + + LocaleData localeData = new() + { + LocaleCode = "en-US", + Authors = finalAuthors, + Messages = new Dictionary + { + { rawString, localeString } + } + }; + + InjectLocaleData(localeData, force); + } + + /// + /// Add locales from a plugin's Locale/ folder at runtime. + /// Note: This is automatically handled by the Harmony patch during locale loading. + /// + public static void AddLocaleFromPlugin(PluginInfo plugin) + { + var localeFiles = GetPluginLocaleFiles(plugin).ToList(); + if (localeFiles.Count == 0) return; + + Plugin.Log.LogDebug($"Adding locale for {plugin.Metadata?.GUID ?? "unknown"}"); + + foreach (string file in localeFiles) + { + AddLocaleFromFile(file); + } + + TrackPluginWithLocale(plugin); + } + + /// + /// Gets all locale JSON files from a plugin's Locale/ folder. + /// + internal static IEnumerable GetPluginLocaleFiles(PluginInfo plugin) + { + string? pluginDir = Path.GetDirectoryName(plugin.Location); + if (string.IsNullOrEmpty(pluginDir)) + return []; + + string localeDir = Path.Combine(pluginDir, "Locale"); + + if (!Directory.Exists(localeDir)) + return []; + + return Directory.GetFiles(localeDir, "*.json", SearchOption.AllDirectories); + } + + /// + /// Add locale from a specific file at runtime. + /// + public static void AddLocaleFromFile(string path) + { + var localeData = LoadLocaleDataFromFile(path); + if (localeData == null) return; + + Plugin.Log.LogDebug($"- LocaleCode: {localeData.LocaleCode}, Message Count: {localeData.Messages.Count}"); + + InjectLocaleData(localeData, force: true); + } + + /// + /// Loads and parses a locale JSON file, returning the LocaleData or null on failure. + /// + internal static LocaleData? LoadLocaleDataFromFile(string path) + { + if (!File.Exists(path)) return null; + + string json; + try + { + json = File.ReadAllText(path); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error reading locale file {path}: {e}"); + return null; + } + + LocaleData? localeData; + try + { + localeData = JsonSerializer.Deserialize(json, JsonOptions); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error parsing locale file {path}: {e}"); + return null; + } + + if (localeData?.Messages == null) + { + Plugin.Log.LogError($"Invalid locale file (missing messages): {path}"); + return null; + } + + return localeData; + } + + /// + /// Inject locale data into the current locale provider. + /// + private static void InjectLocaleData(LocaleData localeData, bool force) + { + var localeProvider = Userspace.UserspaceWorld?.GetCoreLocale(); + + if (localeProvider?.Asset?.Data == null) + { + Plugin.Log.LogWarning("Cannot inject locale data - locale provider not available yet"); + return; + } + + if (!force) + { + string? firstKey = localeData.Messages.Keys.FirstOrDefault(); + if (firstKey != null) + { + bool alreadyExists = localeProvider.Asset.Data.Messages.Any(ld => ld.Key == firstKey); + if (alreadyExists) return; + } + } + + localeProvider.Asset.Data.LoadDataAdditively(localeData); + } + + #region String Formatting Extensions + + private static string GetFormattedLocaleString(this string key, string? format, Dictionary? dict, (string, object)[]? arguments) + { + var localeProvider = Userspace.UserspaceWorld?.GetCoreLocale(); + + Dictionary merged = dict != null ? new Dictionary(dict) : []; + if (arguments != null) + { + foreach ((string name, object value) in arguments) + { + merged[name] = value; + } + } + + string? formatted = localeProvider?.Asset?.Format(key, merged); + + if (!string.IsNullOrWhiteSpace(format)) + { + formatted = string.Format(format, formatted); + } + + return formatted ?? key; + } + + public static string GetFormattedLocaleString(this string key) => key.GetFormattedLocaleString(null, null, null); + public static string GetFormattedLocaleString(this string key, string format) => key.GetFormattedLocaleString(format, null, null); + public static string GetFormattedLocaleString(this string key, string argName, object argField) => key.GetFormattedLocaleString(null, null, [(argName, argField)]); + public static string GetFormattedLocaleString(this string key, string format, string argName, object argField) => key.GetFormattedLocaleString(format, null, [(argName, argField)]); + public static string GetFormattedLocaleString(this string key, params (string, object)[] arguments) => key.GetFormattedLocaleString(null, null, arguments); + public static string GetFormattedLocaleString(this string key, string format, params (string, object)[] arguments) => key.GetFormattedLocaleString(format, null, arguments); + + #endregion + + #region LocaleString Extensions + + public static LocaleString T(this string str, string argName, object argField) => str.AsLocaleKey(null, (argName, argField)); + public static LocaleString T(this string str, string format, string argName, object argField) => str.AsLocaleKey(format, (argName, argField)); + public static LocaleString T(this string str, params (string, object)[] arguments) => str.AsLocaleKey(null, arguments); + public static LocaleString T(this string str, string format, params (string, object)[] arguments) => str.AsLocaleKey(format, arguments); + public static LocaleString T(this string str, bool continuous, Dictionary? arguments = null) => new(str, null, true, continuous, arguments); + public static LocaleString T(this string str, string? format = null, bool continuous = true, Dictionary? arguments = null) => new(str, format, true, continuous, arguments); + + #endregion +} diff --git a/Plugin.cs b/Plugin.cs index 9e40211..5ab0e64 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,33 +1,22 @@ -using BepInEx; -using BepInEx.Logging; -using BepInEx.NET.Common; -using BepInExResoniteShim; -using BepisResoniteWrapper; -using FrooxEngine; -using HarmonyLib; - -namespace BepisLocaleLoader; - -[ResonitePlugin(PluginMetadata.GUID, PluginMetadata.NAME, PluginMetadata.VERSION, PluginMetadata.AUTHORS, PluginMetadata.REPOSITORY_URL)] -[BepInDependency(BepInExResoniteShim.PluginMetadata.GUID, BepInDependency.DependencyFlags.HardDependency)] -public class Plugin : BasePlugin -{ - internal new static ManualLogSource Log; - - public override void Load() - { - // Plugin startup logic - Log = base.Log; - - ResoniteHooks.OnEngineReady += async () => - { - await Task.Delay(5000); - - if (NetChainloader.Instance.Plugins.Count <= 0) return; - - NetChainloader.Instance.Plugins.Values.Do(LocaleLoader.AddLocaleFromPlugin); - }; - - Log.LogInfo($"Plugin {PluginMetadata.GUID} is loaded!"); - } -} \ No newline at end of file +using BepInEx; +using BepInEx.Logging; +using BepInEx.NET.Common; +using HarmonyLib; + +namespace BepisLocaleLoader; + +[BepInAutoPlugin] +[BepInDependency(BepInExResoniteShim.PluginMetadata.GUID, BepInDependency.DependencyFlags.HardDependency)] +public partial class Plugin : BasePlugin +{ + internal new static ManualLogSource Log = null!; + + public override void Load() + { + Log = base.Log; + + HarmonyInstance.PatchAll(); + + Log.LogInfo($"Plugin {GUID} is loaded!"); + } +}