diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..71fbbf4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# https://EditorConfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +charset = utf-8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de80eeb..87c9fa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: repository: neos-modding-group/Reference-Assemblies path: Libs ssh-key: ${{ secrets.REFERENCE_ASSEMBLIES_AUTH }} + - name: lint + run: dotnet format --verbosity detailed --verify-no-changes ./NeosModLoader.sln - name: build run: dotnet build ./NeosModLoader.sln - - name: lint - run: dotnet format --verify-no-changes --include-generated ./NeosModLoader.sln diff --git a/NeosModLoader.sln b/NeosModLoader.sln index 8643dae..c7025cf 100644 --- a/NeosModLoader.sln +++ b/NeosModLoader.sln @@ -6,20 +6,20 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeosModLoader", "NeosModLoader\NeosModLoader.csproj", "{D4627C7F-8091-477A-ABDC-F1465D94D8D9}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {757072E6-E985-4EC2-AB38-C4D1588F6A15} - EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {757072E6-E985-4EC2-AB38-C4D1588F6A15} + EndGlobalSection EndGlobal diff --git a/NeosModLoader/AssemblyFile.cs b/NeosModLoader/AssemblyFile.cs index 8ee614a..70fcf6c 100644 --- a/NeosModLoader/AssemblyFile.cs +++ b/NeosModLoader/AssemblyFile.cs @@ -1,15 +1,15 @@ -using System.Reflection; +using System.Reflection; namespace NeosModLoader { - internal class AssemblyFile + internal class AssemblyFile + { + internal string File { get; } + internal Assembly Assembly { get; set; } + internal AssemblyFile(string file, Assembly assembly) { - internal string File { get; } - internal Assembly Assembly { get; set; } - internal AssemblyFile(string file, Assembly assembly) - { - File = file; - Assembly = assembly; - } + File = file; + Assembly = assembly; } + } } diff --git a/NeosModLoader/AssemblyLoader.cs b/NeosModLoader/AssemblyLoader.cs index 753609e..e31bd21 100644 --- a/NeosModLoader/AssemblyLoader.cs +++ b/NeosModLoader/AssemblyLoader.cs @@ -5,87 +5,87 @@ namespace NeosModLoader { - internal static class AssemblyLoader + internal static class AssemblyLoader + { + private static string[]? GetAssemblyPathsFromDir(string dirName) { - private static string[]? GetAssemblyPathsFromDir(string dirName) - { - string assembliesDirectory = Path.Combine(Directory.GetCurrentDirectory(), dirName); + string assembliesDirectory = Path.Combine(Directory.GetCurrentDirectory(), dirName); - Logger.MsgInternal($"loading assemblies from {dirName}"); + Logger.MsgInternal($"loading assemblies from {dirName}"); - string[]? assembliesToLoad = null; - try - { - assembliesToLoad = Directory.GetFiles(assembliesDirectory, "*.dll", SearchOption.AllDirectories); - Array.Sort(assembliesToLoad, (a, b) => string.CompareOrdinal(a, b)); - } - catch (Exception e) - { - if (e is DirectoryNotFoundException) - { - Logger.MsgInternal($"{dirName} directory not found, creating it now."); - try - { - Directory.CreateDirectory(assembliesDirectory); - } - catch (Exception e2) - { - Logger.ErrorInternal($"Error creating ${dirName} directory:\n{e2}"); - } - } - else - { - Logger.ErrorInternal($"Error enumerating ${dirName} directory:\n{e}"); - } - } - return assembliesToLoad; + string[]? assembliesToLoad = null; + try + { + assembliesToLoad = Directory.GetFiles(assembliesDirectory, "*.dll", SearchOption.AllDirectories); + Array.Sort(assembliesToLoad, (a, b) => string.CompareOrdinal(a, b)); + } + catch (Exception e) + { + if (e is DirectoryNotFoundException) + { + Logger.MsgInternal($"{dirName} directory not found, creating it now."); + try + { + Directory.CreateDirectory(assembliesDirectory); + } + catch (Exception e2) + { + Logger.ErrorInternal($"Error creating ${dirName} directory:\n{e2}"); + } } - - private static Assembly? LoadAssembly(string filepath) + else { - string filename = Path.GetFileName(filepath); - SplashChanger.SetCustom($"Loading file: {filename}"); - Assembly assembly; - try - { - Logger.MsgInternal( $"load assembly {filename} with sha256hash: {Util.GenerateSHA256(filepath)}"); - assembly = Assembly.LoadFile(filepath); - } - catch (Exception e) - { - Logger.ErrorInternal($"error loading assembly from {filepath}: {e}"); - return null; - } - if (assembly == null) - { - Logger.ErrorInternal($"unexpected null loading assembly from {filepath}"); - return null; - } - return assembly; + Logger.ErrorInternal($"Error enumerating ${dirName} directory:\n{e}"); } + } + return assembliesToLoad; + } + + private static Assembly? LoadAssembly(string filepath) + { + string filename = Path.GetFileName(filepath); + SplashChanger.SetCustom($"Loading file: {filename}"); + Assembly assembly; + try + { + Logger.MsgInternal($"load assembly {filename} with sha256hash: {Util.GenerateSHA256(filepath)}"); + assembly = Assembly.LoadFile(filepath); + } + catch (Exception e) + { + Logger.ErrorInternal($"error loading assembly from {filepath}: {e}"); + return null; + } + if (assembly == null) + { + Logger.ErrorInternal($"unexpected null loading assembly from {filepath}"); + return null; + } + return assembly; + } - internal static AssemblyFile[] LoadAssembliesFromDir(string dirName) + internal static AssemblyFile[] LoadAssembliesFromDir(string dirName) + { + List assemblyFiles = new(); + if (GetAssemblyPathsFromDir(dirName) is string[] assemblyPaths) + { + foreach (string assemblyFilepath in assemblyPaths) { - List assemblyFiles = new(); - if (GetAssemblyPathsFromDir(dirName) is string[] assemblyPaths) + try + { + if (LoadAssembly(assemblyFilepath) is Assembly assembly) { - foreach (string assemblyFilepath in assemblyPaths) - { - try - { - if (LoadAssembly(assemblyFilepath) is Assembly assembly) - { - assemblyFiles.Add(new AssemblyFile(assemblyFilepath, assembly)); - } - } - catch (Exception e) - { - Logger.ErrorInternal($"Unexpected exception loading assembly from {assemblyFilepath}:\n{e}"); - } - } + assemblyFiles.Add(new AssemblyFile(assemblyFilepath, assembly)); } - - return assemblyFiles.ToArray(); + } + catch (Exception e) + { + Logger.ErrorInternal($"Unexpected exception loading assembly from {assemblyFilepath}:\n{e}"); + } } + } + + return assemblyFiles.ToArray(); } + } } diff --git a/NeosModLoader/AutoRegisterConfigKeyAttribute.cs b/NeosModLoader/AutoRegisterConfigKeyAttribute.cs index 3c75fdd..5c6b596 100644 --- a/NeosModLoader/AutoRegisterConfigKeyAttribute.cs +++ b/NeosModLoader/AutoRegisterConfigKeyAttribute.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace NeosModLoader { - [AttributeUsage(AttributeTargets.Field)] - public class AutoRegisterConfigKeyAttribute : Attribute - { - } + [AttributeUsage(AttributeTargets.Field)] + public class AutoRegisterConfigKeyAttribute : Attribute + { + } } diff --git a/NeosModLoader/ConfigurationChangedEvent.cs b/NeosModLoader/ConfigurationChangedEvent.cs index 3902ef7..f9de866 100644 --- a/NeosModLoader/ConfigurationChangedEvent.cs +++ b/NeosModLoader/ConfigurationChangedEvent.cs @@ -1,27 +1,27 @@ -namespace NeosModLoader +namespace NeosModLoader { - public class ConfigurationChangedEvent - { - /// - /// The configuration the change occurred in - /// - public ModConfiguration Config { get; private set; } + public class ConfigurationChangedEvent + { + /// + /// The configuration the change occurred in + /// + public ModConfiguration Config { get; private set; } - /// - /// The specific key who's value changed - /// - public ModConfigurationKey Key { get; private set; } + /// + /// The specific key who's value changed + /// + public ModConfigurationKey Key { get; private set; } - /// - /// A custom label that may be set by whoever changed the configuration - /// - public string? Label { get; private set; } + /// + /// A custom label that may be set by whoever changed the configuration + /// + public string? Label { get; private set; } - internal ConfigurationChangedEvent(ModConfiguration config, ModConfigurationKey key, string? label) - { - Config = config; - Key = key; - Label = label; - } + internal ConfigurationChangedEvent(ModConfiguration config, ModConfigurationKey key, string? label) + { + Config = config; + Key = key; + Label = label; } + } } diff --git a/NeosModLoader/DebugInfo.cs b/NeosModLoader/DebugInfo.cs index 05b6386..3699620 100644 --- a/NeosModLoader/DebugInfo.cs +++ b/NeosModLoader/DebugInfo.cs @@ -1,26 +1,26 @@ -using System; +using System; using System.Reflection; using System.Runtime.Versioning; namespace NeosModLoader { - internal class DebugInfo + internal class DebugInfo + { + internal static void Log() { - internal static void Log() - { - Logger.MsgInternal($"NeosModLoader v{ModLoader.VERSION} starting up!{(ModLoaderConfiguration.Get().Debug ? " Debug logs will be shown." : "")}"); - Logger.MsgInternal($"CLR v{Environment.Version}"); - Logger.DebugFuncInternal(() => $"Using .NET Framework: \"{AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\""); - Logger.DebugFuncInternal(() => $"Using .NET Core: \"{Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName}\""); - Logger.MsgInternal($"Using Harmony v{GetAssemblyVersion(typeof(HarmonyLib.Harmony))}"); - Logger.MsgInternal($"Using BaseX v{GetAssemblyVersion(typeof(BaseX.floatQ))}"); - Logger.MsgInternal($"Using FrooxEngine v{GetAssemblyVersion(typeof(FrooxEngine.IComponent))}"); - Logger.MsgInternal($"Using Json.NET v{GetAssemblyVersion(typeof(Newtonsoft.Json.JsonSerializer))}"); - } + Logger.MsgInternal($"NeosModLoader v{ModLoader.VERSION} starting up!{(ModLoaderConfiguration.Get().Debug ? " Debug logs will be shown." : "")}"); + Logger.MsgInternal($"CLR v{Environment.Version}"); + Logger.DebugFuncInternal(() => $"Using .NET Framework: \"{AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\""); + Logger.DebugFuncInternal(() => $"Using .NET Core: \"{Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName}\""); + Logger.MsgInternal($"Using Harmony v{GetAssemblyVersion(typeof(HarmonyLib.Harmony))}"); + Logger.MsgInternal($"Using BaseX v{GetAssemblyVersion(typeof(BaseX.floatQ))}"); + Logger.MsgInternal($"Using FrooxEngine v{GetAssemblyVersion(typeof(FrooxEngine.IComponent))}"); + Logger.MsgInternal($"Using Json.NET v{GetAssemblyVersion(typeof(Newtonsoft.Json.JsonSerializer))}"); + } - private static string? GetAssemblyVersion(Type typeFromAssembly) - { - return typeFromAssembly.Assembly.GetName()?.Version?.ToString(); - } + private static string? GetAssemblyVersion(Type typeFromAssembly) + { + return typeFromAssembly.Assembly.GetName()?.Version?.ToString(); } + } } diff --git a/NeosModLoader/DelegateExtensions.cs b/NeosModLoader/DelegateExtensions.cs index 12a10c8..c7eeeeb 100644 --- a/NeosModLoader/DelegateExtensions.cs +++ b/NeosModLoader/DelegateExtensions.cs @@ -1,31 +1,31 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; namespace NeosModLoader { - internal static class DelegateExtensions + internal static class DelegateExtensions + { + internal static void SafeInvoke(this Delegate del, params object[] args) { - internal static void SafeInvoke(this Delegate del, params object[] args) - { - var exceptions = new List(); - - foreach (var handler in del.GetInvocationList()) - { - try - { - handler.Method.Invoke(handler.Target, args); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } + var exceptions = new List(); - if (exceptions.Any()) - { - throw new AggregateException(exceptions); - } + foreach (var handler in del.GetInvocationList()) + { + try + { + handler.Method.Invoke(handler.Target, args); } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + if (exceptions.Any()) + { + throw new AggregateException(exceptions); + } } + } } diff --git a/NeosModLoader/ExecutionHook.cs b/NeosModLoader/ExecutionHook.cs index ac0f3f3..307fa57 100644 --- a/NeosModLoader/ExecutionHook.cs +++ b/NeosModLoader/ExecutionHook.cs @@ -4,54 +4,54 @@ namespace NeosModLoader { - [ImplementableClass(true)] - internal class ExecutionHook - { + [ImplementableClass(true)] + internal class ExecutionHook + { #pragma warning disable CS0169 - // fields must exist due to reflective access - private static Type? __connectorType; // needed in all Neos versions - private static Type? __connectorTypes; // needed in Neos 2021.10.17.1326 and later + // fields must exist due to reflective access + private static Type? __connectorType; // needed in all Neos versions + private static Type? __connectorTypes; // needed in Neos 2021.10.17.1326 and later #pragma warning restore CS0169 - static ExecutionHook() + static ExecutionHook() + { + try + { + SplashChanger.SetCustom("Loading libraries"); + AssemblyFile[] loadedAssemblies = AssemblyLoader.LoadAssembliesFromDir("nml_libs"); + if (loadedAssemblies.Length != 0) { - try - { - SplashChanger.SetCustom("Loading libraries"); - AssemblyFile[] loadedAssemblies = AssemblyLoader.LoadAssembliesFromDir("nml_libs"); - if (loadedAssemblies.Length != 0) - { - string loadedAssemblyList = string.Join("\n", loadedAssemblies.Select(a => a.Assembly.FullName)); - Logger.MsgInternal($"Loaded libraries from nml_libs:\n{loadedAssemblyList}"); - } - - SplashChanger.SetCustom("Initializing"); - DebugInfo.Log(); - NeosVersionReset.Initialize(); - ModLoader.LoadMods(); - SplashChanger.SetCustom("Loaded"); - } - catch (Exception e) // it's important that this doesn't send exceptions back to Neos - { - Logger.ErrorInternal($"Exception in execution hook!\n{e}"); - } + string loadedAssemblyList = string.Join("\n", loadedAssemblies.Select(a => a.Assembly.FullName)); + Logger.MsgInternal($"Loaded libraries from nml_libs:\n{loadedAssemblyList}"); } - // implementation not strictly required, but method must exist due to reflective access - private static DummyConnector InstantiateConnector() - { - return new DummyConnector(); - } + SplashChanger.SetCustom("Initializing"); + DebugInfo.Log(); + NeosVersionReset.Initialize(); + ModLoader.LoadMods(); + SplashChanger.SetCustom("Loaded"); + } + catch (Exception e) // it's important that this doesn't send exceptions back to Neos + { + Logger.ErrorInternal($"Exception in execution hook!\n{e}"); + } + } - // type must match return type of InstantiateConnector() - private class DummyConnector : IConnector - { - public IImplementable? Owner { get; private set; } - public void ApplyChanges() { } - public void AssignOwner(IImplementable owner) => Owner = owner; - public void Destroy(bool destroyingWorld) { } - public void Initialize() { } - public void RemoveOwner() => Owner = null; - } + // implementation not strictly required, but method must exist due to reflective access + private static DummyConnector InstantiateConnector() + { + return new DummyConnector(); + } + + // type must match return type of InstantiateConnector() + private class DummyConnector : IConnector + { + public IImplementable? Owner { get; private set; } + public void ApplyChanges() { } + public void AssignOwner(IImplementable owner) => Owner = owner; + public void Destroy(bool destroyingWorld) { } + public void Initialize() { } + public void RemoveOwner() => Owner = null; } + } } diff --git a/NeosModLoader/JsonConverters/EnumConverter.cs b/NeosModLoader/JsonConverters/EnumConverter.cs index 57adefb..d7664b9 100644 --- a/NeosModLoader/JsonConverters/EnumConverter.cs +++ b/NeosModLoader/JsonConverters/EnumConverter.cs @@ -1,53 +1,53 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace NeosModLoader.JsonConverters { - // serializes and deserializes enums as strings - internal class EnumConverter : JsonConverter + // serializes and deserializes enums as strings + internal class EnumConverter : JsonConverter + { + public override bool CanConvert(Type objectType) { - public override bool CanConvert(Type objectType) - { - return objectType.IsEnum; - } + return objectType.IsEnum; + } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - // handle old behavior where enums were serialized as underlying type - Type underlyingType = Enum.GetUnderlyingType(objectType); - if (TryConvert(reader!.Value!, underlyingType, out object? deserialized)) - { - Logger.DebugFuncInternal(() => $"Deserializing a BaseX type: {objectType} from a {reader!.Value!.GetType()}"); - return deserialized!; - } + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + // handle old behavior where enums were serialized as underlying type + Type underlyingType = Enum.GetUnderlyingType(objectType); + if (TryConvert(reader!.Value!, underlyingType, out object? deserialized)) + { + Logger.DebugFuncInternal(() => $"Deserializing a BaseX type: {objectType} from a {reader!.Value!.GetType()}"); + return deserialized!; + } - // handle new behavior where enums are serialized as strings - if (reader.Value is string serialized) - { - return Enum.Parse(objectType, serialized); - } + // handle new behavior where enums are serialized as strings + if (reader.Value is string serialized) + { + return Enum.Parse(objectType, serialized); + } - throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}. Expected underlying type was {underlyingType}"); - } + throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}. Expected underlying type was {underlyingType}"); + } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - string serialized = Enum.GetName(value!.GetType(), value); - writer.WriteValue(serialized); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + string serialized = Enum.GetName(value!.GetType(), value); + writer.WriteValue(serialized); + } - private bool TryConvert(object value, Type newType, out object? converted) - { - try - { - converted = Convert.ChangeType(value, newType); - return true; - } - catch - { - converted = null; - return false; - } - } + private bool TryConvert(object value, Type newType, out object? converted) + { + try + { + converted = Convert.ChangeType(value, newType); + return true; + } + catch + { + converted = null; + return false; + } } + } } diff --git a/NeosModLoader/JsonConverters/NeosPrimitiveConverter.cs b/NeosModLoader/JsonConverters/NeosPrimitiveConverter.cs index ecbb6dc..c8a0fcf 100644 --- a/NeosModLoader/JsonConverters/NeosPrimitiveConverter.cs +++ b/NeosModLoader/JsonConverters/NeosPrimitiveConverter.cs @@ -1,35 +1,35 @@ -using BaseX; +using BaseX; using Newtonsoft.Json; using System; using System.Reflection; namespace NeosModLoader.JsonConverters { - internal class NeosPrimitiveConverter : JsonConverter - { - private static readonly Assembly BASEX = typeof(color).Assembly; + internal class NeosPrimitiveConverter : JsonConverter + { + private static readonly Assembly BASEX = typeof(color).Assembly; - public override bool CanConvert(Type objectType) - { - // handle all non-enum Neos Primitives in the BaseX assembly - return !objectType.IsEnum && BASEX.Equals(objectType.Assembly) && Coder.IsNeosPrimitive(objectType); - } + public override bool CanConvert(Type objectType) + { + // handle all non-enum Neos Primitives in the BaseX assembly + return !objectType.IsEnum && BASEX.Equals(objectType.Assembly) && Coder.IsNeosPrimitive(objectType); + } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.Value is string serialized) - { - // use Neos's built-in decoding if the value was serialized as a string - return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString").Invoke(null, new object[] { serialized }); - } + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.Value is string serialized) + { + // use Neos's built-in decoding if the value was serialized as a string + return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString").Invoke(null, new object[] { serialized }); + } - throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}"); - } + throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}"); + } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - string serialized = (string)typeof(Coder<>).MakeGenericType(value!.GetType()).GetMethod("EncodeToString").Invoke(null, new object[] { value }); - writer.WriteValue(serialized); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + string serialized = (string)typeof(Coder<>).MakeGenericType(value!.GetType()).GetMethod("EncodeToString").Invoke(null, new object[] { value }); + writer.WriteValue(serialized); } + } } diff --git a/NeosModLoader/LoadedNeosMod.cs b/NeosModLoader/LoadedNeosMod.cs index 3a4e31d..26dbf56 100644 --- a/NeosModLoader/LoadedNeosMod.cs +++ b/NeosModLoader/LoadedNeosMod.cs @@ -1,18 +1,18 @@ -namespace NeosModLoader +namespace NeosModLoader { - internal class LoadedNeosMod + internal class LoadedNeosMod + { + internal LoadedNeosMod(NeosMod neosMod, AssemblyFile modAssembly) { - internal LoadedNeosMod(NeosMod neosMod, AssemblyFile modAssembly) - { - NeosMod = neosMod; - ModAssembly = modAssembly; - } - - internal NeosMod NeosMod { get; private set; } - internal AssemblyFile ModAssembly { get; private set; } - internal ModConfiguration? ModConfiguration { get; set; } - internal bool AllowSavingConfiguration = true; - internal bool FinishedLoading { get => NeosMod.FinishedLoading; set => NeosMod.FinishedLoading = value; } - internal string Name { get => NeosMod.Name; } + NeosMod = neosMod; + ModAssembly = modAssembly; } + + internal NeosMod NeosMod { get; private set; } + internal AssemblyFile ModAssembly { get; private set; } + internal ModConfiguration? ModConfiguration { get; set; } + internal bool AllowSavingConfiguration = true; + internal bool FinishedLoading { get => NeosMod.FinishedLoading; set => NeosMod.FinishedLoading = value; } + internal string Name { get => NeosMod.Name; } + } } diff --git a/NeosModLoader/Logger.cs b/NeosModLoader/Logger.cs index 84f45dc..3b034e9 100644 --- a/NeosModLoader/Logger.cs +++ b/NeosModLoader/Logger.cs @@ -3,109 +3,109 @@ namespace NeosModLoader { - internal class Logger - { - // logged for null objects - internal readonly static string NULL_STRING = "null"; + internal class Logger + { + // logged for null objects + internal readonly static string NULL_STRING = "null"; - internal static bool IsDebugEnabled() - { - return ModLoaderConfiguration.Get().Debug; - } + internal static bool IsDebugEnabled() + { + return ModLoaderConfiguration.Get().Debug; + } - internal static void DebugFuncInternal(Func messageProducer) - { - if (IsDebugEnabled()) - { - LogInternal(LogType.DEBUG, messageProducer()); - } - } + internal static void DebugFuncInternal(Func messageProducer) + { + if (IsDebugEnabled()) + { + LogInternal(LogType.DEBUG, messageProducer()); + } + } - internal static void DebugFuncExternal(Func messageProducer) - { - if (IsDebugEnabled()) - { - LogInternal(LogType.DEBUG, messageProducer(), SourceFromStackTrace()); - } - } + internal static void DebugFuncExternal(Func messageProducer) + { + if (IsDebugEnabled()) + { + LogInternal(LogType.DEBUG, messageProducer(), SourceFromStackTrace()); + } + } - internal static void DebugInternal(string message) - { - if (IsDebugEnabled()) - { - LogInternal(LogType.DEBUG, message); - } - } + internal static void DebugInternal(string message) + { + if (IsDebugEnabled()) + { + LogInternal(LogType.DEBUG, message); + } + } - internal static void DebugExternal(object message) - { - if (IsDebugEnabled()) - { - LogInternal(LogType.DEBUG, message, SourceFromStackTrace()); - } - } + internal static void DebugExternal(object message) + { + if (IsDebugEnabled()) + { + LogInternal(LogType.DEBUG, message, SourceFromStackTrace()); + } + } - internal static void DebugListExternal(object[] messages) - { - if (IsDebugEnabled()) - { - LogListInternal(LogType.DEBUG, messages, SourceFromStackTrace()); - } - } + internal static void DebugListExternal(object[] messages) + { + if (IsDebugEnabled()) + { + LogListInternal(LogType.DEBUG, messages, SourceFromStackTrace()); + } + } - internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); - internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, SourceFromStackTrace()); - internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, SourceFromStackTrace()); - internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); - internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, SourceFromStackTrace()); - internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, SourceFromStackTrace()); - internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); - internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace()); - internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace()); + internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); + internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, SourceFromStackTrace()); + internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, SourceFromStackTrace()); + internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); + internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, SourceFromStackTrace()); + internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, SourceFromStackTrace()); + internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); + internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace()); + internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace()); - private static void LogInternal(string logTypePrefix, object message, string? source = null) - { - if (message == null) - { - message = NULL_STRING; - } - if (source == null) - { - UniLog.Log($"{logTypePrefix}[NeosModLoader] {message}"); - } - else - { - UniLog.Log($"{logTypePrefix}[NeosModLoader/{source}] {message}"); - } - } + private static void LogInternal(string logTypePrefix, object message, string? source = null) + { + if (message == null) + { + message = NULL_STRING; + } + if (source == null) + { + UniLog.Log($"{logTypePrefix}[NeosModLoader] {message}"); + } + else + { + UniLog.Log($"{logTypePrefix}[NeosModLoader/{source}] {message}"); + } + } - private static void LogListInternal(string logTypePrefix, object[] messages, string? source) + private static void LogListInternal(string logTypePrefix, object[] messages, string? source) + { + if (messages == null) + { + LogInternal(logTypePrefix, NULL_STRING, source); + } + else + { + foreach (object element in messages) { - if (messages == null) - { - LogInternal(logTypePrefix, NULL_STRING, source); - } - else - { - foreach (object element in messages) - { - LogInternal(logTypePrefix, element.ToString(), source); - } - } + LogInternal(logTypePrefix, element.ToString(), source); } + } + } - private static string? SourceFromStackTrace() - { - // MsgExternal() and Msg() are above us in the stack - return Util.ExecutingMod(2)?.Name; - } + private static string? SourceFromStackTrace() + { + // MsgExternal() and Msg() are above us in the stack + return Util.ExecutingMod(2)?.Name; + } - private sealed class LogType - { - internal readonly static string DEBUG = "[DEBUG]"; - internal readonly static string INFO = "[INFO] "; - internal readonly static string WARN = "[WARN] "; - internal readonly static string ERROR = "[ERROR]"; - } + private sealed class LogType + { + internal readonly static string DEBUG = "[DEBUG]"; + internal readonly static string INFO = "[INFO] "; + internal readonly static string WARN = "[WARN] "; + internal readonly static string ERROR = "[ERROR]"; } + } } diff --git a/NeosModLoader/ModConfiguration.cs b/NeosModLoader/ModConfiguration.cs index 636dfde..ca39700 100644 --- a/NeosModLoader/ModConfiguration.cs +++ b/NeosModLoader/ModConfiguration.cs @@ -1,4 +1,4 @@ -using FrooxEngine; +using FrooxEngine; using HarmonyLib; using NeosModLoader.JsonConverters; using Newtonsoft.Json; @@ -14,699 +14,699 @@ namespace NeosModLoader { - public interface IModConfigurationDefinition - { - /// - /// Mod that owns this configuration definition - /// - NeosModBase Owner { get; } - - /// - /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible - /// - Version Version { get; } - - /// - /// The set of coniguration keys defined in this configuration definition - /// - ISet ConfigurationItemDefinitions { get; } - } - + public interface IModConfigurationDefinition + { /// - /// Defines a mod configuration. This should be defined by a NeosMod using the NeosMod.DefineConfiguration() method. + /// Mod that owns this configuration definition /// - public class ModConfigurationDefinition : IModConfigurationDefinition - { - /// - /// Mod that owns this configuration definition - /// - public NeosModBase Owner { get; private set; } - - /// - /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible - /// - public Version Version { get; private set; } - - internal bool AutoSave; - - // this is a ridiculous hack because HashSet.TryGetValue doesn't exist in .NET 4.6.2 - private Dictionary configurationItemDefinitionsSelfMap; - - /// - /// The set of coniguration keys defined in this configuration definition - /// - public ISet ConfigurationItemDefinitions - { - // clone the collection because I don't trust giving public API users shallow copies one bit - get => new HashSet(configurationItemDefinitionsSelfMap.Keys); - } + NeosModBase Owner { get; } - internal bool TryGetDefiningKey(ModConfigurationKey key, out ModConfigurationKey? definingKey) - { - if (key.DefiningKey != null) - { - // we've already cached the defining key - definingKey = key.DefiningKey; - return true; - } + /// + /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible + /// + Version Version { get; } - // first time we've seen this key instance: we need to hit the map - if (configurationItemDefinitionsSelfMap.TryGetValue(key, out definingKey)) - { - // initialize the cache for this key - key.DefiningKey = definingKey; - return true; - } - else - { - // not a real key - definingKey = null; - return false; - } + /// + /// The set of coniguration keys defined in this configuration definition + /// + ISet ConfigurationItemDefinitions { get; } + } + + /// + /// Defines a mod configuration. This should be defined by a NeosMod using the NeosMod.DefineConfiguration() method. + /// + public class ModConfigurationDefinition : IModConfigurationDefinition + { + /// + /// Mod that owns this configuration definition + /// + public NeosModBase Owner { get; private set; } - } + /// + /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible + /// + public Version Version { get; private set; } - internal ModConfigurationDefinition(NeosModBase owner, Version version, HashSet configurationItemDefinitions, bool autoSave) - { - Owner = owner; - Version = version; - AutoSave = autoSave; + internal bool AutoSave; - configurationItemDefinitionsSelfMap = new Dictionary(configurationItemDefinitions.Count); - foreach (ModConfigurationKey key in configurationItemDefinitions) - { - key.DefiningKey = key; // early init this property for the defining key itself - configurationItemDefinitionsSelfMap.Add(key, key); - } - } - } + // this is a ridiculous hack because HashSet.TryGetValue doesn't exist in .NET 4.6.2 + private Dictionary configurationItemDefinitionsSelfMap; /// - /// The configuration for a mod. Each mod has zero or one configuration. The configuration object will never be reassigned once initialized. + /// The set of coniguration keys defined in this configuration definition /// - public class ModConfiguration : IModConfigurationDefinition + public ISet ConfigurationItemDefinitions { - private readonly ModConfigurationDefinition Definition; - internal LoadedNeosMod LoadedNeosMod { get; private set; } - - private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "nml_config"); - private static readonly string VERSION_JSON_KEY = "version"; - private static readonly string VALUES_JSON_KEY = "values"; - - /// - /// Mod that owns this configuration definition - /// - public NeosModBase Owner => Definition.Owner; + // clone the collection because I don't trust giving public API users shallow copies one bit + get => new HashSet(configurationItemDefinitionsSelfMap.Keys); + } - /// - /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible - /// - public Version Version => Definition.Version; + internal bool TryGetDefiningKey(ModConfigurationKey key, out ModConfigurationKey? definingKey) + { + if (key.DefiningKey != null) + { + // we've already cached the defining key + definingKey = key.DefiningKey; + return true; + } + + // first time we've seen this key instance: we need to hit the map + if (configurationItemDefinitionsSelfMap.TryGetValue(key, out definingKey)) + { + // initialize the cache for this key + key.DefiningKey = definingKey; + return true; + } + else + { + // not a real key + definingKey = null; + return false; + } - /// - /// The set of coniguration keys defined in this configuration definition - /// - public ISet ConfigurationItemDefinitions => Definition.ConfigurationItemDefinitions; + } - private bool AutoSave => Definition.AutoSave; + internal ModConfigurationDefinition(NeosModBase owner, Version version, HashSet configurationItemDefinitions, bool autoSave) + { + Owner = owner; + Version = version; + AutoSave = autoSave; + + configurationItemDefinitionsSelfMap = new Dictionary(configurationItemDefinitions.Count); + foreach (ModConfigurationKey key in configurationItemDefinitions) + { + key.DefiningKey = key; // early init this property for the defining key itself + configurationItemDefinitionsSelfMap.Add(key, key); + } + } + } - /// - /// The delegate that is called for configuration change events. - /// - /// The event containing details about the configuration change - public delegate void ConfigurationChangedEventHandler(ConfigurationChangedEvent configurationChangedEvent); + /// + /// The configuration for a mod. Each mod has zero or one configuration. The configuration object will never be reassigned once initialized. + /// + public class ModConfiguration : IModConfigurationDefinition + { + private readonly ModConfigurationDefinition Definition; + internal LoadedNeosMod LoadedNeosMod { get; private set; } - /// - /// Called if any config value for any mod changed. - /// - public static event ConfigurationChangedEventHandler? OnAnyConfigurationChanged; + private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "nml_config"); + private static readonly string VERSION_JSON_KEY = "version"; + private static readonly string VALUES_JSON_KEY = "values"; - /// - /// Called if one of the values in this mod's config changed. - /// - public event ConfigurationChangedEventHandler? OnThisConfigurationChanged; + /// + /// Mod that owns this configuration definition + /// + public NeosModBase Owner => Definition.Owner; - // used to track how frequenly Save() is being called - private Stopwatch saveTimer = new Stopwatch(); + /// + /// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible + /// + public Version Version => Definition.Version; - // time that save must not be called for a save to actually go through - private int debounceMilliseconds = 3000; + /// + /// The set of coniguration keys defined in this configuration definition + /// + public ISet ConfigurationItemDefinitions => Definition.ConfigurationItemDefinitions; - // used to keep track of mods that spam Save(): - // any mod that calls Save() for the ModConfiguration within debounceMilliseconds of the previous call to the same ModConfiguration - // will be put into Ultimate Punishment Mode, and ALL their Save() calls, regardless of ModConfiguration, will be debounced. - // The naughty list is global, while the actual debouncing is per-configuration. - private static ISet naughtySavers = new HashSet(); + private bool AutoSave => Definition.AutoSave; - // used to keep track of the debouncers for this configuration. - private Dictionary> saveActionForCallee = new(); + /// + /// The delegate that is called for configuration change events. + /// + /// The event containing details about the configuration change + public delegate void ConfigurationChangedEventHandler(ConfigurationChangedEvent configurationChangedEvent); - private static readonly JsonSerializer jsonSerializer = CreateJsonSerializer(); + /// + /// Called if any config value for any mod changed. + /// + public static event ConfigurationChangedEventHandler? OnAnyConfigurationChanged; - private static JsonSerializer CreateJsonSerializer() - { - JsonSerializerSettings settings = new() - { - MaxDepth = 32, - ReferenceLoopHandling = ReferenceLoopHandling.Error, - DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate - }; - List converters = new(); - IList defaultConverters = settings.Converters; - if (defaultConverters != null && defaultConverters.Count() != 0) - { - Logger.DebugFuncInternal(() => $"Using {defaultConverters.Count()} default json converters"); - converters.AddRange(defaultConverters); - } - converters.Add(new EnumConverter()); - converters.Add(new NeosPrimitiveConverter()); - settings.Converters = converters; - return JsonSerializer.Create(settings); - } + /// + /// Called if one of the values in this mod's config changed. + /// + public event ConfigurationChangedEventHandler? OnThisConfigurationChanged; - private ModConfiguration(LoadedNeosMod loadedNeosMod, ModConfigurationDefinition definition) - { - LoadedNeosMod = loadedNeosMod; - Definition = definition; - } + // used to track how frequenly Save() is being called + private Stopwatch saveTimer = new Stopwatch(); - internal static void EnsureDirectoryExists() - { - Directory.CreateDirectory(ConfigDirectory); - } + // time that save must not be called for a save to actually go through + private int debounceMilliseconds = 3000; - private static string GetModConfigPath(LoadedNeosMod mod) - { + // used to keep track of mods that spam Save(): + // any mod that calls Save() for the ModConfiguration within debounceMilliseconds of the previous call to the same ModConfiguration + // will be put into Ultimate Punishment Mode, and ALL their Save() calls, regardless of ModConfiguration, will be debounced. + // The naughty list is global, while the actual debouncing is per-configuration. + private static ISet naughtySavers = new HashSet(); - string filename = Path.ChangeExtension(Path.GetFileName(mod.ModAssembly.File), ".json"); - return Path.Combine(ConfigDirectory, filename); - } + // used to keep track of the debouncers for this configuration. + private Dictionary> saveActionForCallee = new(); - private static bool AreVersionsCompatible(Version serializedVersion, Version currentVersion) - { - if (serializedVersion.Major != currentVersion.Major) - { - // major version differences are hard incompatible - return false; - } + private static readonly JsonSerializer jsonSerializer = CreateJsonSerializer(); - if (serializedVersion.Minor > currentVersion.Minor) - { - // if serialized config has a newer minor version than us - // in other words, someone downgraded the mod but not the config - // then we cannot load the config - return false; - } + private static JsonSerializer CreateJsonSerializer() + { + JsonSerializerSettings settings = new() + { + MaxDepth = 32, + ReferenceLoopHandling = ReferenceLoopHandling.Error, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate + }; + List converters = new(); + IList defaultConverters = settings.Converters; + if (defaultConverters != null && defaultConverters.Count() != 0) + { + Logger.DebugFuncInternal(() => $"Using {defaultConverters.Count()} default json converters"); + converters.AddRange(defaultConverters); + } + converters.Add(new EnumConverter()); + converters.Add(new NeosPrimitiveConverter()); + settings.Converters = converters; + return JsonSerializer.Create(settings); + } - // none of the checks failed! - return true; - } + private ModConfiguration(LoadedNeosMod loadedNeosMod, ModConfigurationDefinition definition) + { + LoadedNeosMod = loadedNeosMod; + Definition = definition; + } - /// - /// Check if a key is defined in this config - /// - /// the key to check - /// true if the key is defined - public bool IsKeyDefined(ModConfigurationKey key) - { - // if a key has a non-null defining key it's guaranteed a real key. Lets check for that. - ModConfigurationKey? definingKey = key.DefiningKey; - if (definingKey != null) - { - return true; - } + internal static void EnsureDirectoryExists() + { + Directory.CreateDirectory(ConfigDirectory); + } - // okay, the defining key was null, so lets try to get the defining key from the hashtable instead - if (Definition.TryGetDefiningKey(key, out definingKey)) - { - // we might as well set this now that we have the real defining key - key.DefiningKey = definingKey; - return true; - } + private static string GetModConfigPath(LoadedNeosMod mod) + { - // there was no definition - return false; - } + string filename = Path.ChangeExtension(Path.GetFileName(mod.ModAssembly.File), ".json"); + return Path.Combine(ConfigDirectory, filename); + } - /// - /// Check if a key is the defining key - /// - /// the key to check - /// true if the key is the defining key - internal bool IsKeyDefiningKey(ModConfigurationKey key) - { - // a key is the defining key if and only if its DefiningKey property references itself - return ReferenceEquals(key, key.DefiningKey); // this is safe because we'll throw a NRE if key is null - } + private static bool AreVersionsCompatible(Version serializedVersion, Version currentVersion) + { + if (serializedVersion.Major != currentVersion.Major) + { + // major version differences are hard incompatible + return false; + } + + if (serializedVersion.Minor > currentVersion.Minor) + { + // if serialized config has a newer minor version than us + // in other words, someone downgraded the mod but not the config + // then we cannot load the config + return false; + } + + // none of the checks failed! + return true; + } - /// - /// Get a value, throwing an exception if the key is not found - /// - /// The key to find - /// The found value - /// key does not exist in the collection - public object GetValue(ModConfigurationKey key) - { - if (TryGetValue(key, out object? value)) - { - return value!; - } - else - { - throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration"); - } - } + /// + /// Check if a key is defined in this config + /// + /// the key to check + /// true if the key is defined + public bool IsKeyDefined(ModConfigurationKey key) + { + // if a key has a non-null defining key it's guaranteed a real key. Lets check for that. + ModConfigurationKey? definingKey = key.DefiningKey; + if (definingKey != null) + { + return true; + } + + // okay, the defining key was null, so lets try to get the defining key from the hashtable instead + if (Definition.TryGetDefiningKey(key, out definingKey)) + { + // we might as well set this now that we have the real defining key + key.DefiningKey = definingKey; + return true; + } + + // there was no definition + return false; + } - /// - /// Get a value, throwing an exception if the key is not found - /// - /// The value's type - /// The key to find - /// The found value - /// key does not exist in the collection - public T? GetValue(ModConfigurationKey key) - { - if (TryGetValue(key, out T? value)) - { - return value; - } - else - { - throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration"); - } - } + /// + /// Check if a key is the defining key + /// + /// the key to check + /// true if the key is the defining key + internal bool IsKeyDefiningKey(ModConfigurationKey key) + { + // a key is the defining key if and only if its DefiningKey property references itself + return ReferenceEquals(key, key.DefiningKey); // this is safe because we'll throw a NRE if key is null + } - /// - /// Try to read a configuration value - /// - /// The key - /// The value if we succeeded, or null if we failed. - /// true if the value was read successfully - public bool TryGetValue(ModConfigurationKey key, out object? value) - { - if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) - { - // not in definition - value = null; - return false; - } + /// + /// Get a value, throwing an exception if the key is not found + /// + /// The key to find + /// The found value + /// key does not exist in the collection + public object GetValue(ModConfigurationKey key) + { + if (TryGetValue(key, out object? value)) + { + return value!; + } + else + { + throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration"); + } + } - if (definingKey!.TryGetValue(out object? valueObject)) - { - value = valueObject; - return true; - } - else if (definingKey.TryComputeDefault(out value)) - { - return true; - } - else - { - value = null; - return false; - } - } + /// + /// Get a value, throwing an exception if the key is not found + /// + /// The value's type + /// The key to find + /// The found value + /// key does not exist in the collection + public T? GetValue(ModConfigurationKey key) + { + if (TryGetValue(key, out T? value)) + { + return value; + } + else + { + throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration"); + } + } - /// - /// Try to read a typed configuration value - /// - /// The value type - /// The key - /// The value if we succeeded, or default(T) if we failed. - /// true if the value was read successfully - public bool TryGetValue(ModConfigurationKey key, out T? value) - { - if (TryGetValue(key, out object? valueObject)) - { - value = (T)valueObject!; - return true; - } - else - { - value = default; - return false; - } - } + /// + /// Try to read a configuration value + /// + /// The key + /// The value if we succeeded, or null if we failed. + /// true if the value was read successfully + public bool TryGetValue(ModConfigurationKey key, out object? value) + { + if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) + { + // not in definition + value = null; + return false; + } + + if (definingKey!.TryGetValue(out object? valueObject)) + { + value = valueObject; + return true; + } + else if (definingKey.TryComputeDefault(out value)) + { + return true; + } + else + { + value = null; + return false; + } + } - /// - /// Set a configuration value - /// - /// The key - /// The new value - /// A custom label you may assign to this event - public void Set(ModConfigurationKey key, object value, string? eventLabel = null) - { - if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) - { - throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); - } + /// + /// Try to read a typed configuration value + /// + /// The value type + /// The key + /// The value if we succeeded, or default(T) if we failed. + /// true if the value was read successfully + public bool TryGetValue(ModConfigurationKey key, out T? value) + { + if (TryGetValue(key, out object? valueObject)) + { + value = (T)valueObject!; + return true; + } + else + { + value = default; + return false; + } + } - if (!definingKey!.ValueType().IsAssignableFrom(value.GetType())) - { - throw new ArgumentException($"{value.GetType()} cannot be assigned to {definingKey.ValueType()}"); - } + /// + /// Set a configuration value + /// + /// The key + /// The new value + /// A custom label you may assign to this event + public void Set(ModConfigurationKey key, object value, string? eventLabel = null) + { + if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) + { + throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); + } + + if (!definingKey!.ValueType().IsAssignableFrom(value.GetType())) + { + throw new ArgumentException($"{value.GetType()} cannot be assigned to {definingKey.ValueType()}"); + } + + if (!definingKey.Validate(value)) + { + throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\""); + } + + definingKey.Set(value); + FireConfigurationChangedEvent(definingKey, eventLabel); + } - if (!definingKey.Validate(value)) - { - throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\""); - } + /// + /// Set a typed configuration value + /// + /// The value type + /// The key + /// The new value + /// A custom label you may assign to this event + public void Set(ModConfigurationKey key, T value, string? eventLabel = null) + { + // the reason we don't fall back to untyped Set() here is so we can skip the type check - definingKey.Set(value); - FireConfigurationChangedEvent(definingKey, eventLabel); - } + if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) + { + throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); + } - /// - /// Set a typed configuration value - /// - /// The value type - /// The key - /// The new value - /// A custom label you may assign to this event - public void Set(ModConfigurationKey key, T value, string? eventLabel = null) - { - // the reason we don't fall back to untyped Set() here is so we can skip the type check + if (!definingKey!.Validate(value)) + { + throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\""); + } - if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) - { - throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); - } + definingKey.Set(value); + FireConfigurationChangedEvent(definingKey, eventLabel); + } - if (!definingKey!.Validate(value)) - { - throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\""); - } + /// + /// Removes a configuration value, if set + /// + /// + /// true if a value was successfully found and removed, false if there was no value to remove + public bool Unset(ModConfigurationKey key) + { + if (Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) + { + return definingKey!.Unset(); + } + else + { + throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); + } + } - definingKey.Set(value); - FireConfigurationChangedEvent(definingKey, eventLabel); - } + private bool AnyValuesSet() + { + return ConfigurationItemDefinitions + .Where(key => key.HasValue) + .Any(); + } - /// - /// Removes a configuration value, if set - /// - /// - /// true if a value was successfully found and removed, false if there was no value to remove - public bool Unset(ModConfigurationKey key) + internal static ModConfiguration? LoadConfigForMod(LoadedNeosMod mod) + { + ModConfigurationDefinition? definition = mod.NeosMod.BuildConfigurationDefinition(); + if (definition == null) + { + // if there's no definition, then there's nothing for us to do here + return null; + } + + string configFile = GetModConfigPath(mod); + + try + { + using StreamReader file = File.OpenText(configFile); + using JsonTextReader reader = new(file); + JObject json = JObject.Load(reader); + Version version = new(json[VERSION_JSON_KEY]!.ToObject(jsonSerializer)); + if (!AreVersionsCompatible(version, definition.Version)) { - if (Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey)) - { - return definingKey!.Unset(); - } - else - { - throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}"); - } + var handlingMode = mod.NeosMod.HandleIncompatibleConfigurationVersions(definition.Version, version); + switch (handlingMode) + { + case IncompatibleConfigurationHandlingOption.CLOBBER: + Logger.WarnInternal($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}. Clobbering old config and starting fresh."); + return new ModConfiguration(mod, definition); + case IncompatibleConfigurationHandlingOption.FORCE_LOAD: + // continue processing + break; + case IncompatibleConfigurationHandlingOption.ERROR: // fall through to default + default: + mod.AllowSavingConfiguration = false; + throw new ModConfigurationException($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}"); + } } - - private bool AnyValuesSet() + foreach (ModConfigurationKey key in definition.ConfigurationItemDefinitions) { - return ConfigurationItemDefinitions - .Where(key => key.HasValue) - .Any(); + string keyName = key.Name; + try + { + JToken? token = json[VALUES_JSON_KEY]?[keyName]; + if (token != null) + { + object? value = token.ToObject(key.ValueType(), jsonSerializer); + key.Set(value); + } + } + catch (Exception e) + { + // I know not what exceptions the JSON library will throw, but they must be contained + mod.AllowSavingConfiguration = false; + throw new ModConfigurationException($"Error loading {key.ValueType()} config key \"{keyName}\" for {mod.NeosMod.Name}", e); + } } + } + catch (FileNotFoundException) + { + // return early + return new ModConfiguration(mod, definition); + } + catch (Exception e) + { + // I know not what exceptions the JSON library will throw, but they must be contained + mod.AllowSavingConfiguration = false; + throw new ModConfigurationException($"Error loading config for {mod.NeosMod.Name}", e); + } + + return new ModConfiguration(mod, definition); + } - internal static ModConfiguration? LoadConfigForMod(LoadedNeosMod mod) - { - ModConfigurationDefinition? definition = mod.NeosMod.BuildConfigurationDefinition(); - if (definition == null) - { - // if there's no definition, then there's nothing for us to do here - return null; - } + /// + /// Persist this configuration to disk. This method is not called automatically. Default values are not automatically saved. + /// + public void Save() // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + { + Save(false, false); + } - string configFile = GetModConfigPath(mod); + /// + /// Persist this configuration to disk. This method is not called automatically. + /// + /// If true, default values will also be persisted + public void Save(bool saveDefaultValues = false) // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + { + Save(saveDefaultValues, false); + } - try - { - using StreamReader file = File.OpenText(configFile); - using JsonTextReader reader = new(file); - JObject json = JObject.Load(reader); - Version version = new(json[VERSION_JSON_KEY]!.ToObject(jsonSerializer)); - if (!AreVersionsCompatible(version, definition.Version)) - { - var handlingMode = mod.NeosMod.HandleIncompatibleConfigurationVersions(definition.Version, version); - switch (handlingMode) - { - case IncompatibleConfigurationHandlingOption.CLOBBER: - Logger.WarnInternal($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}. Clobbering old config and starting fresh."); - return new ModConfiguration(mod, definition); - case IncompatibleConfigurationHandlingOption.FORCE_LOAD: - // continue processing - break; - case IncompatibleConfigurationHandlingOption.ERROR: // fall through to default - default: - mod.AllowSavingConfiguration = false; - throw new ModConfigurationException($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}"); - } - } - foreach (ModConfigurationKey key in definition.ConfigurationItemDefinitions) - { - string keyName = key.Name; - try - { - JToken? token = json[VALUES_JSON_KEY]?[keyName]; - if (token != null) - { - object? value = token.ToObject(key.ValueType(), jsonSerializer); - key.Set(value); - } - } - catch (Exception e) - { - // I know not what exceptions the JSON library will throw, but they must be contained - mod.AllowSavingConfiguration = false; - throw new ModConfigurationException($"Error loading {key.ValueType()} config key \"{keyName}\" for {mod.NeosMod.Name}", e); - } - } - } - catch (FileNotFoundException) - { - // return early - return new ModConfiguration(mod, definition); - } - catch (Exception e) - { - // I know not what exceptions the JSON library will throw, but they must be contained - mod.AllowSavingConfiguration = false; - throw new ModConfigurationException($"Error loading config for {mod.NeosMod.Name}", e); - } - return new ModConfiguration(mod, definition); - } + /// + /// Persist this configuration to disk. This method is not called automatically. + /// + /// If true, default values will also be persisted + /// Skip the debouncing and save immediately + internal void Save(bool saveDefaultValues = false, bool immediate = false) + { - /// - /// Persist this configuration to disk. This method is not called automatically. Default values are not automatically saved. - /// - public void Save() // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + Thread thread = Thread.CurrentThread; + NeosMod? callee = Util.ExecutingMod(); + Action? saveAction = null; + + // get saved state for this callee + if (callee != null && naughtySavers.Contains(callee.Name) && !saveActionForCallee.TryGetValue(callee.Name, out saveAction)) + { + // handle case where the callee was marked as naughty from a different ModConfiguration being spammed + saveAction = Util.Debounce(SaveInternal, debounceMilliseconds); + saveActionForCallee.Add(callee.Name, saveAction); + } + + if (saveTimer.IsRunning) + { + float elapsedMillis = saveTimer.ElapsedMilliseconds; + saveTimer.Restart(); + if (elapsedMillis < debounceMilliseconds) { - Save(false, false); + Logger.WarnInternal($"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago. This is very recent! Do not spam calls to ModConfiguration.Save()! All Save() calls by this mod are now subject to a {debounceMilliseconds}ms debouncing delay."); + if (saveAction == null && callee != null) + { + // congrats, you've switched into Ultimate Punishment Mode where now I don't trust you and your Save() calls get debounced + saveAction = Util.Debounce(SaveInternal, debounceMilliseconds); + saveActionForCallee.Add(callee.Name, saveAction); + naughtySavers.Add(callee.Name); + } } - - /// - /// Persist this configuration to disk. This method is not called automatically. - /// - /// If true, default values will also be persisted - public void Save(bool saveDefaultValues = false) // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + else { - Save(saveDefaultValues, false); + Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago."); } + } + else + { + saveTimer.Start(); + Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\""); + } + + // prevent saving if we've determined something is amiss with the configuration + if (!LoadedNeosMod.AllowSavingConfiguration) + { + Logger.WarnInternal($"ModConfiguration for {LoadedNeosMod.NeosMod.Name} will NOT be saved due to a safety check failing. This is probably due to you downgrading a mod."); + return; + } + + if (immediate || saveAction == null) + { + // infrequent callers get to save immediately + Task.Run(() => SaveInternal(saveDefaultValues)); + } + else + { + // bad callers get debounced + saveAction(saveDefaultValues); + } + } - - /// - /// Persist this configuration to disk. This method is not called automatically. - /// - /// If true, default values will also be persisted - /// Skip the debouncing and save immediately - internal void Save(bool saveDefaultValues = false, bool immediate = false) + // performs the actual, synchronous save + private void SaveInternal(bool saveDefaultValues) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + JObject json = new() + { + [VERSION_JSON_KEY] = JToken.FromObject(Definition.Version.ToString(), jsonSerializer) + }; + + JObject valueMap = new(); + foreach (ModConfigurationKey key in ConfigurationItemDefinitions) + { + if (key.TryGetValue(out object? value)) { - - Thread thread = Thread.CurrentThread; - NeosMod? callee = Util.ExecutingMod(); - Action? saveAction = null; - - // get saved state for this callee - if (callee != null && naughtySavers.Contains(callee.Name) && !saveActionForCallee.TryGetValue(callee.Name, out saveAction)) - { - // handle case where the callee was marked as naughty from a different ModConfiguration being spammed - saveAction = Util.Debounce(SaveInternal, debounceMilliseconds); - saveActionForCallee.Add(callee.Name, saveAction); - } - - if (saveTimer.IsRunning) - { - float elapsedMillis = saveTimer.ElapsedMilliseconds; - saveTimer.Restart(); - if (elapsedMillis < debounceMilliseconds) - { - Logger.WarnInternal($"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago. This is very recent! Do not spam calls to ModConfiguration.Save()! All Save() calls by this mod are now subject to a {debounceMilliseconds}ms debouncing delay."); - if (saveAction == null && callee != null) - { - // congrats, you've switched into Ultimate Punishment Mode where now I don't trust you and your Save() calls get debounced - saveAction = Util.Debounce(SaveInternal, debounceMilliseconds); - saveActionForCallee.Add(callee.Name, saveAction); - naughtySavers.Add(callee.Name); - } - } - else - { - Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago."); - } - } - else - { - saveTimer.Start(); - Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\""); - } - - // prevent saving if we've determined something is amiss with the configuration - if (!LoadedNeosMod.AllowSavingConfiguration) - { - Logger.WarnInternal($"ModConfiguration for {LoadedNeosMod.NeosMod.Name} will NOT be saved due to a safety check failing. This is probably due to you downgrading a mod."); - return; - } - - if (immediate || saveAction == null) - { - // infrequent callers get to save immediately - Task.Run(() => SaveInternal(saveDefaultValues)); - } - else - { - // bad callers get debounced - saveAction(saveDefaultValues); - } + // I don't need to typecheck this as there's no way to sneak a bad type past my Set() API + valueMap[key.Name] = value == null ? null : JToken.FromObject(value, jsonSerializer); } - - // performs the actual, synchronous save - private void SaveInternal(bool saveDefaultValues) + else if (saveDefaultValues && key.TryComputeDefault(out object? defaultValue)) { - Stopwatch stopwatch = Stopwatch.StartNew(); - JObject json = new() - { - [VERSION_JSON_KEY] = JToken.FromObject(Definition.Version.ToString(), jsonSerializer) - }; + // I don't need to typecheck this as there's no way to sneak a bad type past my computeDefault API + // like and say defaultValue can't be null because the Json.Net + valueMap[key.Name] = defaultValue == null ? null : JToken.FromObject(defaultValue, jsonSerializer); + } + } - JObject valueMap = new(); - foreach (ModConfigurationKey key in ConfigurationItemDefinitions) - { - if (key.TryGetValue(out object? value)) - { - // I don't need to typecheck this as there's no way to sneak a bad type past my Set() API - valueMap[key.Name] = value == null ? null : JToken.FromObject(value, jsonSerializer); - } - else if (saveDefaultValues && key.TryComputeDefault(out object? defaultValue)) - { - // I don't need to typecheck this as there's no way to sneak a bad type past my computeDefault API - // like and say defaultValue can't be null because the Json.Net - valueMap[key.Name] = defaultValue == null ? null : JToken.FromObject(defaultValue, jsonSerializer); - } - } + json[VALUES_JSON_KEY] = valueMap; - json[VALUES_JSON_KEY] = valueMap; + string configFile = GetModConfigPath(LoadedNeosMod); + using FileStream file = File.OpenWrite(configFile); + using StreamWriter streamWriter = new(file); + using JsonTextWriter jsonTextWriter = new(streamWriter); + json.WriteTo(jsonTextWriter); - string configFile = GetModConfigPath(LoadedNeosMod); - using FileStream file = File.OpenWrite(configFile); - using StreamWriter streamWriter = new(file); - using JsonTextWriter jsonTextWriter = new(streamWriter); - json.WriteTo(jsonTextWriter); + // I actually cannot believe I have to truncate the file myself + file.SetLength(file.Position); + jsonTextWriter.Flush(); - // I actually cannot believe I have to truncate the file myself - file.SetLength(file.Position); - jsonTextWriter.Flush(); + Logger.DebugFuncInternal(() => $"Saved ModConfiguration for \"{LoadedNeosMod.NeosMod.Name}\" in {stopwatch.ElapsedMilliseconds}ms"); + } - Logger.DebugFuncInternal(() => $"Saved ModConfiguration for \"{LoadedNeosMod.NeosMod.Name}\" in {stopwatch.ElapsedMilliseconds}ms"); - } + private void FireConfigurationChangedEvent(ModConfigurationKey key, string? label) + { + try + { + OnAnyConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label)); + } + catch (Exception e) + { + Logger.ErrorInternal($"An OnAnyConfigurationChanged event subscriber threw an exception:\n{e}"); + } + + try + { + OnThisConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label)); + } + catch (Exception e) + { + Logger.ErrorInternal($"An OnThisConfigurationChanged event subscriber threw an exception:\n{e}"); + } + } - private void FireConfigurationChangedEvent(ModConfigurationKey key, string? label) + internal static void RegisterShutdownHook(Harmony harmony) + { + try + { + MethodInfo shutdown = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Shutdown)); + if (shutdown == null) { - try - { - OnAnyConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label)); - } - catch (Exception e) - { - Logger.ErrorInternal($"An OnAnyConfigurationChanged event subscriber threw an exception:\n{e}"); - } - - try - { - OnThisConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label)); - } - catch (Exception e) - { - Logger.ErrorInternal($"An OnThisConfigurationChanged event subscriber threw an exception:\n{e}"); - } + Logger.ErrorInternal("Could not find method Engine.Shutdown(). Will not be able to autosave configs on close!"); + return; } - - internal static void RegisterShutdownHook(Harmony harmony) + MethodInfo patch = AccessTools.DeclaredMethod(typeof(ModConfiguration), nameof(ShutdownHook)); + if (patch == null) { - try - { - MethodInfo shutdown = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Shutdown)); - if (shutdown == null) - { - Logger.ErrorInternal("Could not find method Engine.Shutdown(). Will not be able to autosave configs on close!"); - return; - } - MethodInfo patch = AccessTools.DeclaredMethod(typeof(ModConfiguration), nameof(ShutdownHook)); - if (patch == null) - { - Logger.ErrorInternal("Could not find method ModConfiguration.ShutdownHook(). Will not be able to autosave configs on close!"); - return; - } - harmony.Patch(shutdown, prefix: new HarmonyMethod(patch)); - } - catch (Exception e) - { - Logger.ErrorInternal($"Unexpected exception applying shutdown hook!\n{e}"); - } + Logger.ErrorInternal("Could not find method ModConfiguration.ShutdownHook(). Will not be able to autosave configs on close!"); + return; } + harmony.Patch(shutdown, prefix: new HarmonyMethod(patch)); + } + catch (Exception e) + { + Logger.ErrorInternal($"Unexpected exception applying shutdown hook!\n{e}"); + } + } - private static void ShutdownHook() - { - int count = 0; - ModLoader.Mods() - .Select(mod => mod.GetConfiguration()) - .Where(config => config != null) - .Where(config => config!.AutoSave) - .Where(config => config!.AnyValuesSet()) - .Do(config => - { - config!.Save(); - count += 1; - }); - Logger.MsgInternal($"Configs saved for {count} mods."); - } + private static void ShutdownHook() + { + int count = 0; + ModLoader.Mods() + .Select(mod => mod.GetConfiguration()) + .Where(config => config != null) + .Where(config => config!.AutoSave) + .Where(config => config!.AnyValuesSet()) + .Do(config => + { + config!.Save(); + count += 1; + }); + Logger.MsgInternal($"Configs saved for {count} mods."); } + } - public class ModConfigurationException : Exception + public class ModConfigurationException : Exception + { + internal ModConfigurationException(string message) : base(message) { - internal ModConfigurationException(string message) : base(message) - { - } + } - internal ModConfigurationException(string message, Exception innerException) : base(message, innerException) - { - } + internal ModConfigurationException(string message, Exception innerException) : base(message, innerException) + { } + } + /// + /// Defines handling of incompatible configuration versions + /// + public enum IncompatibleConfigurationHandlingOption + { /// - /// Defines handling of incompatible configuration versions + /// Fail to read the config, and block saving over the config on disk. /// - public enum IncompatibleConfigurationHandlingOption - { - /// - /// Fail to read the config, and block saving over the config on disk. - /// - ERROR, - - /// - /// Destroy the saved config and start over from scratch. - /// - CLOBBER, - - /// - /// Ignore the version number and attempt to load the config from disk - /// - FORCE_LOAD, - } + ERROR, + + /// + /// Destroy the saved config and start over from scratch. + /// + CLOBBER, + + /// + /// Ignore the version number and attempt to load the config from disk + /// + FORCE_LOAD, + } } diff --git a/NeosModLoader/ModConfigurationDefinitionBuilder.cs b/NeosModLoader/ModConfigurationDefinitionBuilder.cs index 3ef8ba9..4bf978d 100644 --- a/NeosModLoader/ModConfigurationDefinitionBuilder.cs +++ b/NeosModLoader/ModConfigurationDefinitionBuilder.cs @@ -1,4 +1,4 @@ -using HarmonyLib; +using HarmonyLib; using System; using System.Collections.Generic; using System.Linq; @@ -6,90 +6,90 @@ namespace NeosModLoader { - public class ModConfigurationDefinitionBuilder - { - private readonly NeosModBase Owner; - private Version ConfigVersion = new(1, 0, 0); - private readonly HashSet Keys = new(); - private bool AutoSaveConfig = true; + public class ModConfigurationDefinitionBuilder + { + private readonly NeosModBase Owner; + private Version ConfigVersion = new(1, 0, 0); + private readonly HashSet Keys = new(); + private bool AutoSaveConfig = true; - internal ModConfigurationDefinitionBuilder(NeosModBase owner) - { - Owner = owner; - } + internal ModConfigurationDefinitionBuilder(NeosModBase owner) + { + Owner = owner; + } - /// - /// Sets the semantic version of this configuration definition. Default is 1.0.0. - /// - /// The config's semantic version. - /// This builder - public ModConfigurationDefinitionBuilder Version(Version version) - { - ConfigVersion = version; - return this; - } + /// + /// Sets the semantic version of this configuration definition. Default is 1.0.0. + /// + /// The config's semantic version. + /// This builder + public ModConfigurationDefinitionBuilder Version(Version version) + { + ConfigVersion = version; + return this; + } - /// - /// Sets the semantic version of this configuration definition. Default is 1.0.0. - /// - /// The config's semantic version, as a string. - /// This builder - public ModConfigurationDefinitionBuilder Version(string version) - { - ConfigVersion = new Version(version); - return this; - } + /// + /// Sets the semantic version of this configuration definition. Default is 1.0.0. + /// + /// The config's semantic version, as a string. + /// This builder + public ModConfigurationDefinitionBuilder Version(string version) + { + ConfigVersion = new Version(version); + return this; + } - /// - /// Adds a new key to this configuration definition - /// - /// A configuration key - /// This builder - public ModConfigurationDefinitionBuilder Key(ModConfigurationKey key) - { - Keys.Add(key); - return this; - } + /// + /// Adds a new key to this configuration definition + /// + /// A configuration key + /// This builder + public ModConfigurationDefinitionBuilder Key(ModConfigurationKey key) + { + Keys.Add(key); + return this; + } - /// - /// Sets the AutoSave property of this configuration definition. Default is true. - /// - /// If false, the config will not be autosaved on Neos close - /// This builder - public ModConfigurationDefinitionBuilder AutoSave(bool autoSave) - { - AutoSaveConfig = autoSave; - return this; - } + /// + /// Sets the AutoSave property of this configuration definition. Default is true. + /// + /// If false, the config will not be autosaved on Neos close + /// This builder + public ModConfigurationDefinitionBuilder AutoSave(bool autoSave) + { + AutoSaveConfig = autoSave; + return this; + } - internal void ProcessAttributes() - { - var fields = AccessTools.GetDeclaredFields(Owner.GetType()); - fields - .Where(field => Attribute.GetCustomAttribute(field, typeof(AutoRegisterConfigKeyAttribute)) != null) - .Do(ProcessField); - } + internal void ProcessAttributes() + { + var fields = AccessTools.GetDeclaredFields(Owner.GetType()); + fields + .Where(field => Attribute.GetCustomAttribute(field, typeof(AutoRegisterConfigKeyAttribute)) != null) + .Do(ProcessField); + } - private void ProcessField(FieldInfo field) - { - if (!typeof(ModConfigurationKey).IsAssignableFrom(field.FieldType)) - { - // wrong type - Logger.WarnInternal($"{Owner.Name} had an [AutoRegisterConfigKey] field of the wrong type: {field}"); - return; - } + private void ProcessField(FieldInfo field) + { + if (!typeof(ModConfigurationKey).IsAssignableFrom(field.FieldType)) + { + // wrong type + Logger.WarnInternal($"{Owner.Name} had an [AutoRegisterConfigKey] field of the wrong type: {field}"); + return; + } - ModConfigurationKey fieldValue = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : Owner); - Keys.Add(fieldValue); - } + ModConfigurationKey fieldValue = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : Owner); + Keys.Add(fieldValue); + } - internal ModConfigurationDefinition? Build() - { - if (Keys.Count > 0) - { - return new ModConfigurationDefinition(Owner, ConfigVersion, Keys, AutoSaveConfig); - } - return null; - } + internal ModConfigurationDefinition? Build() + { + if (Keys.Count > 0) + { + return new ModConfigurationDefinition(Owner, ConfigVersion, Keys, AutoSaveConfig); + } + return null; } + } } diff --git a/NeosModLoader/ModConfigurationKey.cs b/NeosModLoader/ModConfigurationKey.cs index 93f92a7..8305a21 100644 --- a/NeosModLoader/ModConfigurationKey.cs +++ b/NeosModLoader/ModConfigurationKey.cs @@ -1,200 +1,200 @@ -using System; +using System; using System.Collections.Generic; namespace NeosModLoader { + /// + /// Untyped mod configuration key. + /// + public abstract class ModConfigurationKey + { + internal ModConfigurationKey(string name, string? description, bool internalAccessOnly) + { + Name = name ?? throw new ArgumentNullException("Configuration key name must not be null"); + Description = description; + InternalAccessOnly = internalAccessOnly; + } + + /// + /// Unique name of this config item. Must be present. + /// + public string Name { get; private set; } + + /// + /// Human-readable description of this config item. Should be specified by the defining mod. + /// + public string? Description { get; private set; } + + /// + /// If true, only the owning mod should have access to this config item + /// + public bool InternalAccessOnly { get; private set; } + + /// Type of the config item + public abstract Type ValueType(); + + /// + /// Checks if a value is valid for this configuration item + /// + /// The value to check + /// true if the value is valid + public abstract bool Validate(object? value); + + /// + /// Computes a default value for this key, if a default provider is set + /// + /// the computed default value, or null if no default provider was set + /// true if a default was computed + public abstract bool TryComputeDefault(out object? defaultValue); + + // We only care about key name for non-defining keys. + // For defining keys all of the other properties (default, validator, etc.) also matter. + public override bool Equals(object obj) + { + return obj is ModConfigurationKey key && + Name == key.Name; + } + + public override int GetHashCode() + { + return 539060726 + EqualityComparer.Default.GetHashCode(Name); + } + + private object? Value; + internal bool HasValue; + + /// + /// Each configuration item has exactly ONE defining key, and that is the key defined by the mod. + /// Duplicate keys can be created (they only need to share the same Name) and they'll still work + /// for reading configs. + /// + /// This is a non-null self-reference for the defining key itself as soon as the definition is done initializing. + /// + internal ModConfigurationKey? DefiningKey; + + internal bool TryGetValue(out object? value) + { + if (HasValue) + { + value = Value; + return true; + } + else + { + value = null; + return false; + } + } + + internal void Set(object? value) + { + Value = value; + HasValue = true; + } + + internal bool Unset() + { + bool hadValue = HasValue; + HasValue = false; + return hadValue; + } + } + + /// + /// Typed mod configuration key. + /// + /// The value's type + public class ModConfigurationKey : ModConfigurationKey + { /// - /// Untyped mod configuration key. + /// Creates a new ModConfigurationKey /// - public abstract class ModConfigurationKey + /// Unique name of this config item + /// Human-readable description of this config item + /// Function that, if present, computes a default value for this key + /// If true, only the owning mod should have access to this config item + /// Checks if a value is valid for this configuration item + public ModConfigurationKey(string name, string? description = null, Func? computeDefault = null, bool internalAccessOnly = false, Predicate? valueValidator = null) : base(name, description, internalAccessOnly) + { + ComputeDefault = computeDefault; + IsValueValid = valueValidator; + } + + private readonly Func? ComputeDefault; + private readonly Predicate? IsValueValid; + + /// + /// Get the type of this key's value + /// + /// the type of this key's value + public override Type ValueType() => typeof(T); + + /// + /// Checks if a value is valid for this configuration item + /// + /// value to check + /// true if the value is valid + public override bool Validate(object? value) + { + // specifically allow nulls for class types + if (value is T || (value is null && !typeof(T).IsValueType)) + { + return ValidateTyped((T?)value); + } + else + { + return false; + } + } + + /// + /// Checks if a value is valid for this configuration item + /// + /// value to check + /// true if the value is valid + public bool ValidateTyped(T? value) + { + if (IsValueValid == null) + { + return true; + } + else + { + return IsValueValid(value); + } + } + + public override bool TryComputeDefault(out object? defaultValue) { - internal ModConfigurationKey(string name, string? description, bool internalAccessOnly) - { - Name = name ?? throw new ArgumentNullException("Configuration key name must not be null"); - Description = description; - InternalAccessOnly = internalAccessOnly; - } - - /// - /// Unique name of this config item. Must be present. - /// - public string Name { get; private set; } - - /// - /// Human-readable description of this config item. Should be specified by the defining mod. - /// - public string? Description { get; private set; } - - /// - /// If true, only the owning mod should have access to this config item - /// - public bool InternalAccessOnly { get; private set; } - - /// Type of the config item - public abstract Type ValueType(); - - /// - /// Checks if a value is valid for this configuration item - /// - /// The value to check - /// true if the value is valid - public abstract bool Validate(object? value); - - /// - /// Computes a default value for this key, if a default provider is set - /// - /// the computed default value, or null if no default provider was set - /// true if a default was computed - public abstract bool TryComputeDefault(out object? defaultValue); - - // We only care about key name for non-defining keys. - // For defining keys all of the other properties (default, validator, etc.) also matter. - public override bool Equals(object obj) - { - return obj is ModConfigurationKey key && - Name == key.Name; - } - - public override int GetHashCode() - { - return 539060726 + EqualityComparer.Default.GetHashCode(Name); - } - - private object? Value; - internal bool HasValue; - - /// - /// Each configuration item has exactly ONE defining key, and that is the key defined by the mod. - /// Duplicate keys can be created (they only need to share the same Name) and they'll still work - /// for reading configs. - /// - /// This is a non-null self-reference for the defining key itself as soon as the definition is done initializing. - /// - internal ModConfigurationKey? DefiningKey; - - internal bool TryGetValue(out object? value) - { - if (HasValue) - { - value = Value; - return true; - } - else - { - value = null; - return false; - } - } - - internal void Set(object? value) - { - Value = value; - HasValue = true; - } - - internal bool Unset() - { - bool hadValue = HasValue; - HasValue = false; - return hadValue; - } + if (TryComputeDefaultTyped(out T? defaultTypedValue)) + { + defaultValue = defaultTypedValue; + return true; + } + else + { + defaultValue = null; + return false; + } } /// - /// Typed mod configuration key. + /// Computes a default value for this key, if a default provider is set /// - /// The value's type - public class ModConfigurationKey : ModConfigurationKey + /// the computed default value, or default(T) if no default provider was set + /// true if a default was computed + public bool TryComputeDefaultTyped(out T? defaultValue) { - /// - /// Creates a new ModConfigurationKey - /// - /// Unique name of this config item - /// Human-readable description of this config item - /// Function that, if present, computes a default value for this key - /// If true, only the owning mod should have access to this config item - /// Checks if a value is valid for this configuration item - public ModConfigurationKey(string name, string? description = null, Func? computeDefault = null, bool internalAccessOnly = false, Predicate? valueValidator = null) : base(name, description, internalAccessOnly) - { - ComputeDefault = computeDefault; - IsValueValid = valueValidator; - } - - private readonly Func? ComputeDefault; - private readonly Predicate? IsValueValid; - - /// - /// Get the type of this key's value - /// - /// the type of this key's value - public override Type ValueType() => typeof(T); - - /// - /// Checks if a value is valid for this configuration item - /// - /// value to check - /// true if the value is valid - public override bool Validate(object? value) - { - // specifically allow nulls for class types - if (value is T || (value is null && !typeof(T).IsValueType)) - { - return ValidateTyped((T?)value); - } - else - { - return false; - } - } - - /// - /// Checks if a value is valid for this configuration item - /// - /// value to check - /// true if the value is valid - public bool ValidateTyped(T? value) - { - if (IsValueValid == null) - { - return true; - } - else - { - return IsValueValid(value); - } - } - - public override bool TryComputeDefault(out object? defaultValue) - { - if (TryComputeDefaultTyped(out T? defaultTypedValue)) - { - defaultValue = defaultTypedValue; - return true; - } - else - { - defaultValue = null; - return false; - } - } - - /// - /// Computes a default value for this key, if a default provider is set - /// - /// the computed default value, or default(T) if no default provider was set - /// true if a default was computed - public bool TryComputeDefaultTyped(out T? defaultValue) - { - if (ComputeDefault == null) - { - defaultValue = default; - return false; - } - else - { - defaultValue = ComputeDefault(); - return true; - } - } + if (ComputeDefault == null) + { + defaultValue = default; + return false; + } + else + { + defaultValue = ComputeDefault(); + return true; + } } + } } diff --git a/NeosModLoader/ModLoader.cs b/NeosModLoader/ModLoader.cs index b51f365..700a306 100644 --- a/NeosModLoader/ModLoader.cs +++ b/NeosModLoader/ModLoader.cs @@ -8,223 +8,223 @@ namespace NeosModLoader { - public class ModLoader + public class ModLoader + { + internal const string VERSION_CONSTANT = "1.11.3"; + /// + /// NeosModLoader's version + /// + public static readonly string VERSION = VERSION_CONSTANT; + private static readonly Type NEOS_MOD_TYPE = typeof(NeosMod); + private static readonly List LoadedMods = new(); // used for mod enumeration + internal static readonly Dictionary AssemblyLookupMap = new(); // used for logging + private static readonly Dictionary ModNameLookupMap = new(); // used for duplicate mod checking + + /// + /// Allows reading metadata for all loaded mods + /// + /// A new list containing each loaded mod + public static IEnumerable Mods() { - internal const string VERSION_CONSTANT = "1.11.3"; - /// - /// NeosModLoader's version - /// - public static readonly string VERSION = VERSION_CONSTANT; - private static readonly Type NEOS_MOD_TYPE = typeof(NeosMod); - private static readonly List LoadedMods = new(); // used for mod enumeration - internal static readonly Dictionary AssemblyLookupMap = new(); // used for logging - private static readonly Dictionary ModNameLookupMap = new(); // used for duplicate mod checking - - /// - /// Allows reading metadata for all loaded mods - /// - /// A new list containing each loaded mod - public static IEnumerable Mods() + return LoadedMods + .Select(m => (NeosModBase)m.NeosMod) + .ToList(); + } + + internal static void LoadMods() + { + ModLoaderConfiguration config = ModLoaderConfiguration.Get(); + if (config.NoMods) + { + Logger.DebugInternal("mods will not be loaded due to configuration file"); + return; + } + SplashChanger.SetCustom("Looking for mods"); + + // generate list of assemblies to load + AssemblyFile[] modsToLoad; + if (AssemblyLoader.LoadAssembliesFromDir("nml_mods") is AssemblyFile[] arr) + { + modsToLoad = arr; + } + else + { + return; + } + + ModConfiguration.EnsureDirectoryExists(); + + // call Initialize() each mod + foreach (AssemblyFile mod in modsToLoad) + { + try { - return LoadedMods - .Select(m => (NeosModBase)m.NeosMod) - .ToList(); + LoadedNeosMod? loaded = InitializeMod(mod); + if (loaded != null) + { + // if loading succeeded, then we need to register the mod + RegisterMod(loaded); + } } - - internal static void LoadMods() + catch (ReflectionTypeLoadException reflectionTypeLoadException) { - ModLoaderConfiguration config = ModLoaderConfiguration.Get(); - if (config.NoMods) - { - Logger.DebugInternal("mods will not be loaded due to configuration file"); - return; - } - SplashChanger.SetCustom("Looking for mods"); - - // generate list of assemblies to load - AssemblyFile[] modsToLoad; - if (AssemblyLoader.LoadAssembliesFromDir("nml_mods") is AssemblyFile[] arr) + // this exception type has some inner exceptions we must also log to gain any insight into what went wrong + StringBuilder sb = new(); + sb.AppendLine(reflectionTypeLoadException.ToString()); + foreach (Exception loaderException in reflectionTypeLoadException.LoaderExceptions) + { + sb.AppendLine($"Loader Exception: {loaderException.Message}"); + if (loaderException is FileNotFoundException fileNotFoundException) { - modsToLoad = arr; - } - else - { - return; + if (!string.IsNullOrEmpty(fileNotFoundException.FusionLog)) + { + sb.Append(" Fusion Log:\n "); + sb.AppendLine(fileNotFoundException.FusionLog); + } } + } + Logger.ErrorInternal($"ReflectionTypeLoadException initializing mod from {mod.File}:\n{sb}"); + } + catch (Exception e) + { + Logger.ErrorInternal($"Unexpected exception initializing mod from {mod.File}:\n{e}"); + } + } - ModConfiguration.EnsureDirectoryExists(); + SplashChanger.SetCustom("Hooking big fish"); + Harmony harmony = new("net.michaelripley.neosmodloader"); + ModConfiguration.RegisterShutdownHook(harmony); - // call Initialize() each mod - foreach (AssemblyFile mod in modsToLoad) - { - try - { - LoadedNeosMod? loaded = InitializeMod(mod); - if (loaded != null) - { - // if loading succeeded, then we need to register the mod - RegisterMod(loaded); - } - } - catch (ReflectionTypeLoadException reflectionTypeLoadException) - { - // this exception type has some inner exceptions we must also log to gain any insight into what went wrong - StringBuilder sb = new(); - sb.AppendLine(reflectionTypeLoadException.ToString()); - foreach (Exception loaderException in reflectionTypeLoadException.LoaderExceptions) - { - sb.AppendLine($"Loader Exception: {loaderException.Message}"); - if (loaderException is FileNotFoundException fileNotFoundException) - { - if (!string.IsNullOrEmpty(fileNotFoundException.FusionLog)) - { - sb.Append(" Fusion Log:\n "); - sb.AppendLine(fileNotFoundException.FusionLog); - } - } - } - Logger.ErrorInternal($"ReflectionTypeLoadException initializing mod from {mod.File}:\n{sb}"); - } - catch (Exception e) - { - Logger.ErrorInternal($"Unexpected exception initializing mod from {mod.File}:\n{e}"); - } - } - - SplashChanger.SetCustom("Hooking big fish"); - Harmony harmony = new("net.michaelripley.neosmodloader"); - ModConfiguration.RegisterShutdownHook(harmony); + foreach (LoadedNeosMod mod in LoadedMods) + { + try + { + HookMod(mod); + } + catch (Exception e) + { + Logger.ErrorInternal($"Unexpected exception in OnEngineInit() for mod {mod.NeosMod.Name} from {mod.ModAssembly.File}:\n{e}"); + } + } - foreach (LoadedNeosMod mod in LoadedMods) - { - try - { - HookMod(mod); - } - catch (Exception e) - { - Logger.ErrorInternal($"Unexpected exception in OnEngineInit() for mod {mod.NeosMod.Name} from {mod.ModAssembly.File}:\n{e}"); - } - } + // log potential conflicts + if (config.LogConflicts) + { + SplashChanger.SetCustom("Looking for conflicts"); - // log potential conflicts - if (config.LogConflicts) + IEnumerable patchedMethods = Harmony.GetAllPatchedMethods(); + foreach (MethodBase patchedMethod in patchedMethods) + { + Patches patches = Harmony.GetPatchInfo(patchedMethod); + HashSet owners = new(patches.Owners); + if (owners.Count > 1) + { + Logger.WarnInternal($"method \"{patchedMethod.FullDescription()}\" has been patched by the following:"); + foreach (string owner in owners) { - SplashChanger.SetCustom("Looking for conflicts"); - - IEnumerable patchedMethods = Harmony.GetAllPatchedMethods(); - foreach (MethodBase patchedMethod in patchedMethods) - { - Patches patches = Harmony.GetPatchInfo(patchedMethod); - HashSet owners = new(patches.Owners); - if (owners.Count > 1) - { - Logger.WarnInternal($"method \"{patchedMethod.FullDescription()}\" has been patched by the following:"); - foreach (string owner in owners) - { - Logger.WarnInternal($" \"{owner}\" ({TypesForOwner(patches, owner)})"); - } - } - else if (config.Debug) - { - string owner = owners.FirstOrDefault(); - Logger.DebugFuncInternal(() => $"method \"{patchedMethod.FullDescription()}\" has been patched by \"{owner}\""); - } - } + Logger.WarnInternal($" \"{owner}\" ({TypesForOwner(patches, owner)})"); } + } + else if (config.Debug) + { + string owner = owners.FirstOrDefault(); + Logger.DebugFuncInternal(() => $"method \"{patchedMethod.FullDescription()}\" has been patched by \"{owner}\""); + } } + } + } - /// - /// We have a bunch of maps and things the mod needs to be registered in. This method does all that jazz. - /// - /// The successfully loaded mod to register - private static void RegisterMod(LoadedNeosMod mod) - { - try - { - ModNameLookupMap.Add(mod.NeosMod.Name, mod); - } - catch (ArgumentException) - { - LoadedNeosMod existing = ModNameLookupMap[mod.NeosMod.Name]; - Logger.ErrorInternal($"{mod.ModAssembly.File} declares duplicate mod {mod.NeosMod.Name} already declared in {existing.ModAssembly.File}. The new mod will be ignored."); - return; - } + /// + /// We have a bunch of maps and things the mod needs to be registered in. This method does all that jazz. + /// + /// The successfully loaded mod to register + private static void RegisterMod(LoadedNeosMod mod) + { + try + { + ModNameLookupMap.Add(mod.NeosMod.Name, mod); + } + catch (ArgumentException) + { + LoadedNeosMod existing = ModNameLookupMap[mod.NeosMod.Name]; + Logger.ErrorInternal($"{mod.ModAssembly.File} declares duplicate mod {mod.NeosMod.Name} already declared in {existing.ModAssembly.File}. The new mod will be ignored."); + return; + } + + LoadedMods.Add(mod); + AssemblyLookupMap.Add(mod.ModAssembly.Assembly, mod.NeosMod); + mod.NeosMod.loadedNeosMod = mod; // complete the circular reference (used to look up config) + mod.FinishedLoading = true; // used to signal that the mod is truly loaded + } - LoadedMods.Add(mod); - AssemblyLookupMap.Add(mod.ModAssembly.Assembly, mod.NeosMod); - mod.NeosMod.loadedNeosMod = mod; // complete the circular reference (used to look up config) - mod.FinishedLoading = true; // used to signal that the mod is truly loaded - } + private static string TypesForOwner(Patches patches, string owner) + { + bool ownerEquals(Patch patch) => Equals(patch.owner, owner); + int prefixCount = patches.Prefixes.Where(ownerEquals).Count(); + int postfixCount = patches.Postfixes.Where(ownerEquals).Count(); + int transpilerCount = patches.Transpilers.Where(ownerEquals).Count(); + int finalizerCount = patches.Finalizers.Where(ownerEquals).Count(); + return $"prefix={prefixCount}; postfix={postfixCount}; transpiler={transpilerCount}; finalizer={finalizerCount}"; + } - private static string TypesForOwner(Patches patches, string owner) + // loads mod class and mod config + private static LoadedNeosMod? InitializeMod(AssemblyFile mod) + { + if (mod.Assembly == null) + { + return null; + } + + Type[] modClasses = mod.Assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && NEOS_MOD_TYPE.IsAssignableFrom(t)).ToArray(); + if (modClasses.Length == 0) + { + Logger.ErrorInternal($"no mods found in {mod.File}"); + return null; + } + else if (modClasses.Length != 1) + { + Logger.ErrorInternal($"more than one mod found in {mod.File}. no mods will be loaded."); + return null; + } + else + { + Type modClass = modClasses[0]; + NeosMod? neosMod = null; + try { - bool ownerEquals(Patch patch) => Equals(patch.owner, owner); - int prefixCount = patches.Prefixes.Where(ownerEquals).Count(); - int postfixCount = patches.Postfixes.Where(ownerEquals).Count(); - int transpilerCount = patches.Transpilers.Where(ownerEquals).Count(); - int finalizerCount = patches.Finalizers.Where(ownerEquals).Count(); - return $"prefix={prefixCount}; postfix={postfixCount}; transpiler={transpilerCount}; finalizer={finalizerCount}"; + neosMod = (NeosMod)AccessTools.CreateInstance(modClass); } - - // loads mod class and mod config - private static LoadedNeosMod? InitializeMod(AssemblyFile mod) + catch (Exception e) { - if (mod.Assembly == null) - { - return null; - } - - Type[] modClasses = mod.Assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && NEOS_MOD_TYPE.IsAssignableFrom(t)).ToArray(); - if (modClasses.Length == 0) - { - Logger.ErrorInternal($"no mods found in {mod.File}"); - return null; - } - else if (modClasses.Length != 1) - { - Logger.ErrorInternal($"more than one mod found in {mod.File}. no mods will be loaded."); - return null; - } - else - { - Type modClass = modClasses[0]; - NeosMod? neosMod = null; - try - { - neosMod = (NeosMod)AccessTools.CreateInstance(modClass); - } - catch (Exception e) - { - Logger.ErrorInternal($"error instantiating mod {modClass.FullName} from {mod.File}:\n{e}"); - return null; - } - if (neosMod == null) - { - Logger.ErrorInternal($"unexpected null instantiating mod {modClass.FullName} from {mod.File}"); - return null; - } - SplashChanger.SetCustom($"Loading configuration for [{neosMod.Name}/{neosMod.Version}]"); - - LoadedNeosMod loadedMod = new(neosMod, mod); - Logger.MsgInternal($"loaded mod [{neosMod.Name}/{neosMod.Version}] ({Path.GetFileName(mod.File)}) by {neosMod.Author}"); - loadedMod.ModConfiguration = ModConfiguration.LoadConfigForMod(loadedMod); - return loadedMod; - } + Logger.ErrorInternal($"error instantiating mod {modClass.FullName} from {mod.File}:\n{e}"); + return null; } - - private static void HookMod(LoadedNeosMod mod) + if (neosMod == null) { - SplashChanger.SetCustom($"Starting mod [{mod.NeosMod.Name}/{mod.NeosMod.Version}]"); - Logger.DebugFuncInternal(() => $"calling OnEngineInit() for [{mod.NeosMod.Name}]"); - try - { - mod.NeosMod.OnEngineInit(); - } - catch (Exception e) - { - Logger.ErrorInternal($"mod {mod.NeosMod.Name} from {mod.ModAssembly.File} threw error from OnEngineInit():\n{e}"); - } + Logger.ErrorInternal($"unexpected null instantiating mod {modClass.FullName} from {mod.File}"); + return null; } + SplashChanger.SetCustom($"Loading configuration for [{neosMod.Name}/{neosMod.Version}]"); + + LoadedNeosMod loadedMod = new(neosMod, mod); + Logger.MsgInternal($"loaded mod [{neosMod.Name}/{neosMod.Version}] ({Path.GetFileName(mod.File)}) by {neosMod.Author}"); + loadedMod.ModConfiguration = ModConfiguration.LoadConfigForMod(loadedMod); + return loadedMod; + } + } + + private static void HookMod(LoadedNeosMod mod) + { + SplashChanger.SetCustom($"Starting mod [{mod.NeosMod.Name}/{mod.NeosMod.Version}]"); + Logger.DebugFuncInternal(() => $"calling OnEngineInit() for [{mod.NeosMod.Name}]"); + try + { + mod.NeosMod.OnEngineInit(); + } + catch (Exception e) + { + Logger.ErrorInternal($"mod {mod.NeosMod.Name} from {mod.ModAssembly.File} threw error from OnEngineInit():\n{e}"); + } } + } } diff --git a/NeosModLoader/ModLoaderConfiguration.cs b/NeosModLoader/ModLoaderConfiguration.cs index de6bbae..29384c1 100644 --- a/NeosModLoader/ModLoaderConfiguration.cs +++ b/NeosModLoader/ModLoaderConfiguration.cs @@ -4,97 +4,97 @@ namespace NeosModLoader { - internal class ModLoaderConfiguration - { - private static readonly string CONFIG_FILENAME = "NeosModLoader.config"; + internal class ModLoaderConfiguration + { + private static readonly string CONFIG_FILENAME = "NeosModLoader.config"; + + private static ModLoaderConfiguration? _configuration; - private static ModLoaderConfiguration? _configuration; + internal static ModLoaderConfiguration Get() + { + if (_configuration == null) + { + // the config file can just sit next to the dll. Simple. + string path = Path.Combine(GetAssemblyDirectory(), CONFIG_FILENAME); + _configuration = new ModLoaderConfiguration(); - internal static ModLoaderConfiguration Get() + // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. + // Time to reinvent the wheel. This parses simple key=value style properties from a text file. + try { - if (_configuration == null) + var lines = File.ReadAllLines(path); + foreach (var line in lines) + { + int splitIdx = line.IndexOf('='); + if (splitIdx != -1) { - // the config file can just sit next to the dll. Simple. - string path = Path.Combine(GetAssemblyDirectory(), CONFIG_FILENAME); - _configuration = new ModLoaderConfiguration(); - - // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. - // Time to reinvent the wheel. This parses simple key=value style properties from a text file. - try - { - var lines = File.ReadAllLines(path); - foreach (var line in lines) - { - int splitIdx = line.IndexOf('='); - if (splitIdx != -1) - { - string key = line.Substring(0, splitIdx); - string value = line.Substring(splitIdx + 1); + string key = line.Substring(0, splitIdx); + string value = line.Substring(splitIdx + 1); - if ("unsafe".Equals(key) && "true".Equals(value)) - { - _configuration.Unsafe = true; - } - else if ("debug".Equals(key) && "true".Equals(value)) - { - _configuration.Debug = true; - } - else if ("hidevisuals".Equals(key) && "true".Equals(value)) - { - _configuration.HideVisuals = true; - } - else if ("nomods".Equals(key) && "true".Equals(value)) - { - _configuration.NoMods = true; - } - else if ("nolibraries".Equals(key) && "true".Equals(value)) - { - _configuration.NoLibraries = true; - } - else if ("advertiseversion".Equals(key) && "true".Equals(value)) - { - _configuration.AdvertiseVersion = true; - } - else if ("logconflicts".Equals(key) && "false".Equals(value)) - { - _configuration.LogConflicts = false; - } - } - } - } - catch (Exception e) - { - if (e is FileNotFoundException) - { - Logger.MsgInternal($"{path} is missing! This is probably fine."); - } - else if (e is DirectoryNotFoundException || e is IOException || e is UnauthorizedAccessException) - { - Logger.WarnInternal(e.ToString()); - } - else - { - throw; - } - } + if ("unsafe".Equals(key) && "true".Equals(value)) + { + _configuration.Unsafe = true; + } + else if ("debug".Equals(key) && "true".Equals(value)) + { + _configuration.Debug = true; + } + else if ("hidevisuals".Equals(key) && "true".Equals(value)) + { + _configuration.HideVisuals = true; + } + else if ("nomods".Equals(key) && "true".Equals(value)) + { + _configuration.NoMods = true; + } + else if ("nolibraries".Equals(key) && "true".Equals(value)) + { + _configuration.NoLibraries = true; + } + else if ("advertiseversion".Equals(key) && "true".Equals(value)) + { + _configuration.AdvertiseVersion = true; + } + else if ("logconflicts".Equals(key) && "false".Equals(value)) + { + _configuration.LogConflicts = false; + } } - return _configuration; + } } - - private static string GetAssemblyDirectory() + catch (Exception e) { - string codeBase = Assembly.GetExecutingAssembly().CodeBase; - UriBuilder uri = new(codeBase); - string path = Uri.UnescapeDataString(uri.Path); - return Path.GetDirectoryName(path); + if (e is FileNotFoundException) + { + Logger.MsgInternal($"{path} is missing! This is probably fine."); + } + else if (e is DirectoryNotFoundException || e is IOException || e is UnauthorizedAccessException) + { + Logger.WarnInternal(e.ToString()); + } + else + { + throw; + } } + } + return _configuration; + } - public bool Unsafe { get; private set; } = false; - public bool Debug { get; private set; } = false; - public bool HideVisuals { get; private set; } = false; - public bool NoMods { get; private set; } = false; - public bool NoLibraries { get; private set; } = false; - public bool AdvertiseVersion { get; private set; } = false; - public bool LogConflicts { get; private set; } = true; + private static string GetAssemblyDirectory() + { + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + UriBuilder uri = new(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); } + + public bool Unsafe { get; private set; } = false; + public bool Debug { get; private set; } = false; + public bool HideVisuals { get; private set; } = false; + public bool NoMods { get; private set; } = false; + public bool NoLibraries { get; private set; } = false; + public bool AdvertiseVersion { get; private set; } = false; + public bool LogConflicts { get; private set; } = true; + } } diff --git a/NeosModLoader/NeosMod.cs b/NeosModLoader/NeosMod.cs index 7bfd915..961dd4b 100644 --- a/NeosModLoader/NeosMod.cs +++ b/NeosModLoader/NeosMod.cs @@ -3,106 +3,106 @@ namespace NeosModLoader { - // contains members that only the modloader or the mod itself are intended to access - public abstract class NeosMod : NeosModBase - { - public static bool IsDebugEnabled() => Logger.IsDebugEnabled(); - public static void DebugFunc(Func messageProducer) => Logger.DebugFuncExternal(messageProducer); - public static void Debug(string message) => Logger.DebugExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) - public static void Debug(object message) => Logger.DebugExternal(message); - public static void Debug(params object[] messages) => Logger.DebugListExternal(messages); + // contains members that only the modloader or the mod itself are intended to access + public abstract class NeosMod : NeosModBase + { + public static bool IsDebugEnabled() => Logger.IsDebugEnabled(); + public static void DebugFunc(Func messageProducer) => Logger.DebugFuncExternal(messageProducer); + public static void Debug(string message) => Logger.DebugExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + public static void Debug(object message) => Logger.DebugExternal(message); + public static void Debug(params object[] messages) => Logger.DebugListExternal(messages); - public static void Msg(string message) => Logger.MsgExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) - public static void Msg(object message) => Logger.MsgExternal(message); - public static void Msg(params object[] messages) => Logger.MsgListExternal(messages); + public static void Msg(string message) => Logger.MsgExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + public static void Msg(object message) => Logger.MsgExternal(message); + public static void Msg(params object[] messages) => Logger.MsgListExternal(messages); - public static void Warn(string message) => Logger.WarnExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) - public static void Warn(object message) => Logger.WarnExternal(message); - public static void Warn(params object[] messages) => Logger.WarnListExternal(messages); + public static void Warn(string message) => Logger.WarnExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + public static void Warn(object message) => Logger.WarnExternal(message); + public static void Warn(params object[] messages) => Logger.WarnListExternal(messages); - public static void Error(string message) => Logger.ErrorExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) - public static void Error(object message) => Logger.ErrorExternal(message); - public static void Error(params object[] messages) => Logger.ErrorListExternal(messages); + public static void Error(string message) => Logger.ErrorExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) + public static void Error(object message) => Logger.ErrorExternal(message); + public static void Error(params object[] messages) => Logger.ErrorListExternal(messages); - /// - /// Called once immediately after NeosModLoader begins execution - /// - public virtual void OnEngineInit() { } + /// + /// Called once immediately after NeosModLoader begins execution + /// + public virtual void OnEngineInit() { } - /// - /// Build the defined configuration for this mod. - /// - /// This mod's configuration definition. - internal ModConfigurationDefinition? BuildConfigurationDefinition() - { - ModConfigurationDefinitionBuilder builder = new(this); - builder.ProcessAttributes(); - DefineConfiguration(builder); - return builder.Build(); - } + /// + /// Build the defined configuration for this mod. + /// + /// This mod's configuration definition. + internal ModConfigurationDefinition? BuildConfigurationDefinition() + { + ModConfigurationDefinitionBuilder builder = new(this); + builder.ProcessAttributes(); + DefineConfiguration(builder); + return builder.Build(); + } - /// - /// Get the defined configuration for this mod. This should be overridden by your mod if necessary. - /// - /// This mod's configuration definition. calls DefineConfiguration(ModConfigurationDefinitionBuilder) by default. - [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION - public virtual ModConfigurationDefinition? GetConfigurationDefinition() - { - return BuildConfigurationDefinition(); - } + /// + /// Get the defined configuration for this mod. This should be overridden by your mod if necessary. + /// + /// This mod's configuration definition. calls DefineConfiguration(ModConfigurationDefinitionBuilder) by default. + [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION + public virtual ModConfigurationDefinition? GetConfigurationDefinition() + { + return BuildConfigurationDefinition(); + } - /// - /// Create a configuration definition for this mod. - /// - /// The semantic version of the configuration definition - /// A list of configuration items - /// - [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION - public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions) // needed for binary compatibility - { - return DefineConfiguration(version, configurationItemDefinitions, true); - } + /// + /// Create a configuration definition for this mod. + /// + /// The semantic version of the configuration definition + /// A list of configuration items + /// + [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION + public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions) // needed for binary compatibility + { + return DefineConfiguration(version, configurationItemDefinitions, true); + } - /// - /// Create a configuration definition for this mod. - /// - /// The semantic version of the configuration definition - /// A list of configuration items - /// If false, the config will not be autosaved on Neos close - /// - [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION - public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions, bool autoSave = true) - { - if (version == null) - { - throw new ArgumentNullException("version must be non-null"); - } + /// + /// Create a configuration definition for this mod. + /// + /// The semantic version of the configuration definition + /// A list of configuration items + /// If false, the config will not be autosaved on Neos close + /// + [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION + public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions, bool autoSave = true) + { + if (version == null) + { + throw new ArgumentNullException("version must be non-null"); + } - if (configurationItemDefinitions == null) - { - throw new ArgumentNullException("configurationItemDefinitions must be non-null"); - } + if (configurationItemDefinitions == null) + { + throw new ArgumentNullException("configurationItemDefinitions must be non-null"); + } - return new ModConfigurationDefinition(this, version, new HashSet(configurationItemDefinitions), autoSave); - } + return new ModConfigurationDefinition(this, version, new HashSet(configurationItemDefinitions), autoSave); + } - /// - /// Define this mod's configuration via a builder - /// - /// A builder you can use to define the mod's configuration - public virtual void DefineConfiguration(ModConfigurationDefinitionBuilder builder) - { - } + /// + /// Define this mod's configuration via a builder + /// + /// A builder you can use to define the mod's configuration + public virtual void DefineConfiguration(ModConfigurationDefinitionBuilder builder) + { + } - /// - /// Defines handling of incompatible configuration versions - /// - /// Configuration version read from the config file - /// Configuration version defined in the mod code - /// - public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) - { - return IncompatibleConfigurationHandlingOption.ERROR; - } + /// + /// Defines handling of incompatible configuration versions + /// + /// Configuration version read from the config file + /// Configuration version defined in the mod code + /// + public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) + { + return IncompatibleConfigurationHandlingOption.ERROR; } + } } diff --git a/NeosModLoader/NeosModBase.cs b/NeosModLoader/NeosModBase.cs index 9286188..1b5fe39 100644 --- a/NeosModLoader/NeosModBase.cs +++ b/NeosModLoader/NeosModBase.cs @@ -1,44 +1,44 @@ -namespace NeosModLoader +namespace NeosModLoader { - // contains public metadata about the mod - public abstract class NeosModBase - { - /// - /// Name of the mod. This must be unique. - /// - public abstract string Name { get; } - - /// - /// Mod's author. - /// - public abstract string Author { get; } + // contains public metadata about the mod + public abstract class NeosModBase + { + /// + /// Name of the mod. This must be unique. + /// + public abstract string Name { get; } - /// - /// Semantic version of this mod. - /// - public abstract string Version { get; } + /// + /// Mod's author. + /// + public abstract string Author { get; } - /// - /// Optional hyperlink to this mod's homepage - /// - public virtual string? Link { get; } + /// + /// Semantic version of this mod. + /// + public abstract string Version { get; } - /// - /// A circular reference back to the LoadedNeosMod that contains this NeosModBase. - /// The reference is set once the mod is successfully loaded, and is null before that. - /// - internal LoadedNeosMod? loadedNeosMod; + /// + /// Optional hyperlink to this mod's homepage + /// + public virtual string? Link { get; } - /// This mod's current configuration. This method will always return the same ModConfiguration instance. - public ModConfiguration? GetConfiguration() - { - if (!FinishedLoading) - { - throw new ModConfigurationException($"GetConfiguration() was called before {Name} was done initializing. Consider calling GetConfiguration() from within OnEngineInit()"); - } - return loadedNeosMod?.ModConfiguration; - } + /// + /// A circular reference back to the LoadedNeosMod that contains this NeosModBase. + /// The reference is set once the mod is successfully loaded, and is null before that. + /// + internal LoadedNeosMod? loadedNeosMod; - internal bool FinishedLoading { get; set; } + /// This mod's current configuration. This method will always return the same ModConfiguration instance. + public ModConfiguration? GetConfiguration() + { + if (!FinishedLoading) + { + throw new ModConfigurationException($"GetConfiguration() was called before {Name} was done initializing. Consider calling GetConfiguration() from within OnEngineInit()"); + } + return loadedNeosMod?.ModConfiguration; } + + internal bool FinishedLoading { get; set; } + } } diff --git a/NeosModLoader/NeosVersionReset.cs b/NeosModLoader/NeosVersionReset.cs index 3ead030..86676e5 100644 --- a/NeosModLoader/NeosVersionReset.cs +++ b/NeosModLoader/NeosVersionReset.cs @@ -10,178 +10,178 @@ namespace NeosModLoader { - internal class NeosVersionReset + internal class NeosVersionReset + { + internal static void Initialize() { - internal static void Initialize() + ModLoaderConfiguration config = ModLoaderConfiguration.Get(); + Engine engine = Engine.Current; + + List extraAssemblies = Engine.ExtraAssemblies; + string assemblyFilename = Path.GetFileName(Assembly.GetExecutingAssembly().Location); + bool nmlPresent = extraAssemblies.Contains(assemblyFilename); + + if (!nmlPresent) + { + throw new Exception($"Assertion failed: Engine.ExtraAssemblies did not contain \"{assemblyFilename}\""); + } + + bool otherPluginsPresent = extraAssemblies.Count > 1; + bool shouldSpoofCompatibility = !otherPluginsPresent || config.Unsafe; + bool shouldSpoofVersion = !config.AdvertiseVersion && shouldSpoofCompatibility; + + if (shouldSpoofVersion) + { + // we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value + // this is to prevent the case where a player simply doesn't know their version string is wrong + extraAssemblies.Clear(); + if (!SpoofVersionString(engine)) { - ModLoaderConfiguration config = ModLoaderConfiguration.Get(); - Engine engine = Engine.Current; - - List extraAssemblies = Engine.ExtraAssemblies; - string assemblyFilename = Path.GetFileName(Assembly.GetExecutingAssembly().Location); - bool nmlPresent = extraAssemblies.Contains(assemblyFilename); - - if (!nmlPresent) - { - throw new Exception($"Assertion failed: Engine.ExtraAssemblies did not contain \"{assemblyFilename}\""); - } - - bool otherPluginsPresent = extraAssemblies.Count > 1; - bool shouldSpoofCompatibility = !otherPluginsPresent || config.Unsafe; - bool shouldSpoofVersion = !config.AdvertiseVersion && shouldSpoofCompatibility; - - if (shouldSpoofVersion) - { - // we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value - // this is to prevent the case where a player simply doesn't know their version string is wrong - extraAssemblies.Clear(); - if (!SpoofVersionString(engine)) - { - Logger.WarnInternal("Version string spoofing failed"); - return; - } - } - else - { - Logger.MsgInternal("Version string not being spoofed due to config."); - } - - if (shouldSpoofCompatibility) - { - if (!SpoofCompatibilityHash(engine)) - { - Logger.WarnInternal("Compatibility hash spoofing failed"); - return; - } - } - else - { - Logger.WarnInternal("Version spoofing was not performed due to another plugin being present! Either remove unknown plugins or enable NeosModLoader's unsafe mode."); - return; - } - - Logger.MsgInternal("Compatibility hash spoofing succeeded"); + Logger.WarnInternal("Version string spoofing failed"); + return; } - - private static bool SpoofCompatibilityHash(Engine engine) + } + else + { + Logger.MsgInternal("Version string not being spoofed due to config."); + } + + if (shouldSpoofCompatibility) + { + if (!SpoofCompatibilityHash(engine)) { - string vanillaCompatibilityHash; - int? vanillaProtocolVersionMaybe = GetVanillaProtocolVersion(); - if (vanillaProtocolVersionMaybe is int vanillaProtocolVersion) - { - Logger.DebugFuncInternal(() => $"Vanilla protocol version is {vanillaProtocolVersion}"); - vanillaCompatibilityHash = CalculateCompatibilityHash(vanillaProtocolVersion); - return SetCompatibilityHash(engine, vanillaCompatibilityHash); - } - else - { - Logger.ErrorInternal("Unable to determine vanilla protocol version"); - return false; - } + Logger.WarnInternal("Compatibility hash spoofing failed"); + return; } + } + else + { + Logger.WarnInternal("Version spoofing was not performed due to another plugin being present! Either remove unknown plugins or enable NeosModLoader's unsafe mode."); + return; + } + + Logger.MsgInternal("Compatibility hash spoofing succeeded"); + } - private static string CalculateCompatibilityHash(int ProtocolVersion) - { - using MD5CryptoServiceProvider cryptoServiceProvider = new(); - byte[] hash = cryptoServiceProvider.ComputeHash(new MemoryStream(BitConverter.GetBytes(ProtocolVersion))); - return Convert.ToBase64String(hash); - } + private static bool SpoofCompatibilityHash(Engine engine) + { + string vanillaCompatibilityHash; + int? vanillaProtocolVersionMaybe = GetVanillaProtocolVersion(); + if (vanillaProtocolVersionMaybe is int vanillaProtocolVersion) + { + Logger.DebugFuncInternal(() => $"Vanilla protocol version is {vanillaProtocolVersion}"); + vanillaCompatibilityHash = CalculateCompatibilityHash(vanillaProtocolVersion); + return SetCompatibilityHash(engine, vanillaCompatibilityHash); + } + else + { + Logger.ErrorInternal("Unable to determine vanilla protocol version"); + return false; + } + } - private static bool SetCompatibilityHash(Engine engine, string Target) - { - // This is super sketchy and liable to break with new compiler versions. - // I have a good reason for doing it though... if I just called the setter it would recursively - // end up calling itself, because I'm HOOKINGthe CompatibilityHash setter. - FieldInfo field = AccessTools.DeclaredField(typeof(Engine), $"<{nameof(Engine.CompatibilityHash)}>k__BackingField"); - - if (field == null) - { - Logger.WarnInternal("Unable to write Engine.CompatibilityHash"); - return false; - } - else - { - Logger.DebugFuncInternal(() => $"Changing compatibility hash from {engine.CompatibilityHash} to {Target}"); - field.SetValue(engine, Target); - return true; - } - } + private static string CalculateCompatibilityHash(int ProtocolVersion) + { + using MD5CryptoServiceProvider cryptoServiceProvider = new(); + byte[] hash = cryptoServiceProvider.ComputeHash(new MemoryStream(BitConverter.GetBytes(ProtocolVersion))); + return Convert.ToBase64String(hash); + } - private static bool SpoofVersionString(Engine engine) + private static bool SetCompatibilityHash(Engine engine, string Target) + { + // This is super sketchy and liable to break with new compiler versions. + // I have a good reason for doing it though... if I just called the setter it would recursively + // end up calling itself, because I'm HOOKINGthe CompatibilityHash setter. + FieldInfo field = AccessTools.DeclaredField(typeof(Engine), $"<{nameof(Engine.CompatibilityHash)}>k__BackingField"); + + if (field == null) + { + Logger.WarnInternal("Unable to write Engine.CompatibilityHash"); + return false; + } + else + { + Logger.DebugFuncInternal(() => $"Changing compatibility hash from {engine.CompatibilityHash} to {Target}"); + field.SetValue(engine, Target); + return true; + } + } + + private static bool SpoofVersionString(Engine engine) + { + // calculate correct version string + string target = Engine.VersionNumber; + + if (!engine.VersionString.Equals(target)) + { + FieldInfo field = AccessTools.DeclaredField(engine.GetType(), "_versionString"); + if (field == null) { - // calculate correct version string - string target = Engine.VersionNumber; - - if (!engine.VersionString.Equals(target)) - { - FieldInfo field = AccessTools.DeclaredField(engine.GetType(), "_versionString"); - if (field == null) - { - Logger.WarnInternal("Unable to write Engine._versionString"); - return false; - } - Logger.DebugFuncInternal(() => $"Changing version string from {engine.VersionString} to {target}"); - field.SetValue(engine, target); - } - return true; + Logger.WarnInternal("Unable to write Engine._versionString"); + return false; } + Logger.DebugFuncInternal(() => $"Changing version string from {engine.VersionString} to {target}"); + field.SetValue(engine, target); + } + return true; + } - // perform incredible bullshit to rip the hardcoded protocol version out of the dang IL - private static int? GetVanillaProtocolVersion() + // perform incredible bullshit to rip the hardcoded protocol version out of the dang IL + private static int? GetVanillaProtocolVersion() + { + // raw IL immediately surrounding the number we need to find, which in this example is 770 + // ldc.i4 770 + // call unsigned int8[] [mscorlib]System.BitConverter::GetBytes(int32) + + // we're going to search for that method call, then grab the operand of the ldc.i4 that precedes it + + MethodInfo targetCallee = AccessTools.DeclaredMethod(typeof(BitConverter), nameof(BitConverter.GetBytes), new Type[] { typeof(int) }); + if (targetCallee == null) + { + Logger.ErrorInternal("Could not find System.BitConverter::GetBytes(System.Int32)"); + return null; + } + + MethodInfo initializeShim = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Initialize)); + if (initializeShim == null) + { + Logger.ErrorInternal("Could not find Engine.Initialize(*)"); + return null; + } + + AsyncStateMachineAttribute asyncAttribute = (AsyncStateMachineAttribute)initializeShim.GetCustomAttribute(typeof(AsyncStateMachineAttribute)); + if (asyncAttribute == null) + { + Logger.ErrorInternal("Could not find AsyncStateMachine for Engine.Initialize"); + return null; + } + + // async methods are weird. Their body is just some setup code that passes execution... elsewhere. + // The compiler generates a companion type for async methods. This companion type has some ridiculous nondeterministic name, but luckily + // we can just ask this attribute what the type is. The companion type should have a MoveNext() method that contains the actual IL we need. + Type asyncStateMachineType = asyncAttribute.StateMachineType; + MethodInfo initializeImpl = AccessTools.DeclaredMethod(asyncStateMachineType, "MoveNext"); + if (initializeImpl == null) + { + Logger.ErrorInternal("Could not find MoveNext method for Engine.Initialize"); + return null; + } + + List instructions = PatchProcessor.GetOriginalInstructions(initializeImpl); + for (int i = 1; i < instructions.Count; i++) + { + if (instructions[i].Calls(targetCallee)) { - // raw IL immediately surrounding the number we need to find, which in this example is 770 - // ldc.i4 770 - // call unsigned int8[] [mscorlib]System.BitConverter::GetBytes(int32) - - // we're going to search for that method call, then grab the operand of the ldc.i4 that precedes it - - MethodInfo targetCallee = AccessTools.DeclaredMethod(typeof(BitConverter), nameof(BitConverter.GetBytes), new Type[] { typeof(int) }); - if (targetCallee == null) - { - Logger.ErrorInternal("Could not find System.BitConverter::GetBytes(System.Int32)"); - return null; - } - - MethodInfo initializeShim = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Initialize)); - if (initializeShim == null) - { - Logger.ErrorInternal("Could not find Engine.Initialize(*)"); - return null; - } - - AsyncStateMachineAttribute asyncAttribute = (AsyncStateMachineAttribute)initializeShim.GetCustomAttribute(typeof(AsyncStateMachineAttribute)); - if (asyncAttribute == null) - { - Logger.ErrorInternal("Could not find AsyncStateMachine for Engine.Initialize"); - return null; - } - - // async methods are weird. Their body is just some setup code that passes execution... elsewhere. - // The compiler generates a companion type for async methods. This companion type has some ridiculous nondeterministic name, but luckily - // we can just ask this attribute what the type is. The companion type should have a MoveNext() method that contains the actual IL we need. - Type asyncStateMachineType = asyncAttribute.StateMachineType; - MethodInfo initializeImpl = AccessTools.DeclaredMethod(asyncStateMachineType, "MoveNext"); - if (initializeImpl == null) - { - Logger.ErrorInternal("Could not find MoveNext method for Engine.Initialize"); - return null; - } - - List instructions = PatchProcessor.GetOriginalInstructions(initializeImpl); - for (int i = 1; i < instructions.Count; i++) - { - if (instructions[i].Calls(targetCallee)) - { - // we're guaranteed to have a previous instruction because we began iteration from 1 - CodeInstruction previous = instructions[i - 1]; - if (OpCodes.Ldc_I4.Equals(previous.opcode)) - { - return (int)previous.operand; - } - } - } - - return null; + // we're guaranteed to have a previous instruction because we began iteration from 1 + CodeInstruction previous = instructions[i - 1]; + if (OpCodes.Ldc_I4.Equals(previous.opcode)) + { + return (int)previous.operand; + } } + } + + return null; } + } } diff --git a/NeosModLoader/SplashChanger.cs b/NeosModLoader/SplashChanger.cs index 5989491..e40ce5c 100644 --- a/NeosModLoader/SplashChanger.cs +++ b/NeosModLoader/SplashChanger.cs @@ -4,84 +4,84 @@ namespace NeosModLoader { - // Custom splash screen logic failing shouldn't fail the rest of the modloader. - // Keep that in mind when editing later on. - internal static class SplashChanger - { - private static bool failed = false; + // Custom splash screen logic failing shouldn't fail the rest of the modloader. + // Keep that in mind when editing later on. + internal static class SplashChanger + { + private static bool failed = false; - private static MethodInfo? _updatePhase = null; - private static MethodInfo? UpdatePhase + private static MethodInfo? _updatePhase = null; + private static MethodInfo? UpdatePhase + { + get + { + if (_updatePhase is null) { - get + try + { + _updatePhase = typeof(Engine) + .GetMethod("UpdateInitPhase", BindingFlags.NonPublic | BindingFlags.Instance); + } + catch (Exception ex) + { + if (!failed) { - if (_updatePhase is null) - { - try - { - _updatePhase = typeof(Engine) - .GetMethod("UpdateInitPhase", BindingFlags.NonPublic | BindingFlags.Instance); - } - catch (Exception ex) - { - if (!failed) - { - Logger.WarnInternal("UpdatePhase not found: " + ex.ToString()); - } - failed = true; - } - } - return _updatePhase; + Logger.WarnInternal("UpdatePhase not found: " + ex.ToString()); } + failed = true; + } } - private static MethodInfo? _updateSubPhase = null; - private static MethodInfo? UpdateSubPhase + return _updatePhase; + } + } + private static MethodInfo? _updateSubPhase = null; + private static MethodInfo? UpdateSubPhase + { + get + { + if (_updateSubPhase is null) { - get + try + { + _updateSubPhase = typeof(Engine) + .GetMethod("UpdateInitSubphase", BindingFlags.NonPublic | BindingFlags.Instance); + } + catch (Exception ex) + { + if (!failed) { - if (_updateSubPhase is null) - { - try - { - _updateSubPhase = typeof(Engine) - .GetMethod("UpdateInitSubphase", BindingFlags.NonPublic | BindingFlags.Instance); - } - catch (Exception ex) - { - if (!failed) - { - Logger.WarnInternal("UpdateSubPhase not found: " + ex.ToString()); - } - failed = true; - } - } - return _updateSubPhase; + Logger.WarnInternal("UpdateSubPhase not found: " + ex.ToString()); } + failed = true; + } } + return _updateSubPhase; + } + } - // Returned true means success, false means something went wrong. - internal static bool SetCustom(string text) + // Returned true means success, false means something went wrong. + internal static bool SetCustom(string text) + { + if (ModLoaderConfiguration.Get().HideVisuals) return true; + try + { + // VerboseInit does extra logging, so turning it if off while we change the phase. + bool ogVerboseInit = Engine.Current.VerboseInit; + Engine.Current.VerboseInit = false; + UpdatePhase?.Invoke(Engine.Current, new object[] { "~ NeosModLoader ~", false }); + UpdateSubPhase?.Invoke(Engine.Current, new object[] { text, false }); + Engine.Current.VerboseInit = ogVerboseInit; + return true; + } + catch (Exception ex) + { + if (!failed) { - if (ModLoaderConfiguration.Get().HideVisuals) return true; - try - { - // VerboseInit does extra logging, so turning it if off while we change the phase. - bool ogVerboseInit = Engine.Current.VerboseInit; - Engine.Current.VerboseInit = false; - UpdatePhase?.Invoke(Engine.Current, new object[] { "~ NeosModLoader ~", false }); - UpdateSubPhase?.Invoke(Engine.Current, new object[] { text, false }); - Engine.Current.VerboseInit = ogVerboseInit; - return true; - } - catch (Exception ex) - { - if (!failed) - { - Logger.WarnInternal("Splash change failed: " + ex.ToString()); - failed = true; - } - return false; - } + Logger.WarnInternal("Splash change failed: " + ex.ToString()); + failed = true; } + return false; + } } + } } diff --git a/NeosModLoader/Util.cs b/NeosModLoader/Util.cs index 4acec96..8aac83b 100644 --- a/NeosModLoader/Util.cs +++ b/NeosModLoader/Util.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Diagnostics; using System.Reflection; @@ -8,81 +8,81 @@ namespace NeosModLoader { - internal static class Util + internal static class Util + { + /// + /// Get the executing mod by stack trace analysis. Always skips the first two frames, being this method and you, the caller. + /// You may skip extra frames if you know your callers are guaranteed to be NML code. + /// + /// The number NML method calls above you in the stack + /// The executing mod, or null if none found + internal static NeosMod? ExecutingMod(int nmlCalleeDepth = 0) { - /// - /// Get the executing mod by stack trace analysis. Always skips the first two frames, being this method and you, the caller. - /// You may skip extra frames if you know your callers are guaranteed to be NML code. - /// - /// The number NML method calls above you in the stack - /// The executing mod, or null if none found - internal static NeosMod? ExecutingMod(int nmlCalleeDepth = 0) + // example: ExecutingMod(), SourceFromStackTrace(), MsgExternal(), Msg(), ACTUAL MOD CODE + // you'd skip 4 frames + // we always skip ExecutingMod() and whoever called us (as this is an internal method), which is where the 2 comes from + StackTrace stackTrace = new(2 + nmlCalleeDepth); + for (int i = 0; i < stackTrace.FrameCount; i++) + { + Assembly assembly = stackTrace.GetFrame(i).GetMethod().DeclaringType.Assembly; + if (ModLoader.AssemblyLookupMap.TryGetValue(assembly, out NeosMod mod)) { - // example: ExecutingMod(), SourceFromStackTrace(), MsgExternal(), Msg(), ACTUAL MOD CODE - // you'd skip 4 frames - // we always skip ExecutingMod() and whoever called us (as this is an internal method), which is where the 2 comes from - StackTrace stackTrace = new(2 + nmlCalleeDepth); - for (int i = 0; i < stackTrace.FrameCount; i++) - { - Assembly assembly = stackTrace.GetFrame(i).GetMethod().DeclaringType.Assembly; - if (ModLoader.AssemblyLookupMap.TryGetValue(assembly, out NeosMod mod)) - { - return mod; - } - } - return null; + return mod; } + } + return null; + } - /// - /// Used to debounce a method call. The underlying method will be called after there have been no additional calls - /// for n milliseconds. - /// - /// The Action returned by this function has internal state used for the debouncing, so you will need to store and reuse the Action - /// for each call. - /// - /// - /// underlying function call - /// debounce delay - /// a debounced wrapper to a method call - // credit: https://stackoverflow.com/questions/28472205/c-sharp-event-debounce - internal static Action Debounce(this Action func, int milliseconds) - { - // this variable gets embedded in the returned Action via the magic of closures - CancellationTokenSource? cancelTokenSource = null; + /// + /// Used to debounce a method call. The underlying method will be called after there have been no additional calls + /// for n milliseconds. + /// + /// The Action returned by this function has internal state used for the debouncing, so you will need to store and reuse the Action + /// for each call. + /// + /// + /// underlying function call + /// debounce delay + /// a debounced wrapper to a method call + // credit: https://stackoverflow.com/questions/28472205/c-sharp-event-debounce + internal static Action Debounce(this Action func, int milliseconds) + { + // this variable gets embedded in the returned Action via the magic of closures + CancellationTokenSource? cancelTokenSource = null; - return arg => - { - // if there's already a scheduled call, then cancel it - cancelTokenSource?.Cancel(); - cancelTokenSource = new CancellationTokenSource(); + return arg => + { + // if there's already a scheduled call, then cancel it + cancelTokenSource?.Cancel(); + cancelTokenSource = new CancellationTokenSource(); - // schedule a new call - Task.Delay(milliseconds, cancelTokenSource.Token) - .ContinueWith(t => - { - if (t.IsCompletedSuccessfully()) - { - Task.Run(() => func(arg)); - } - }, TaskScheduler.Default); - }; - } + // schedule a new call + Task.Delay(milliseconds, cancelTokenSource.Token) + .ContinueWith(t => + { + if (t.IsCompletedSuccessfully()) + { + Task.Run(() => func(arg)); + } + }, TaskScheduler.Default); + }; + } - // shim because this doesn't exist in .NET 4.6 - private static bool IsCompletedSuccessfully(this Task t) - { - return t.IsCompleted && !t.IsFaulted && !t.IsCanceled; - } - - //credit to delta for this method https://github.com/XDelta/ - internal static string GenerateSHA256(string filepath) - { - using var hasher = SHA256.Create(); - using var stream = File.OpenRead(filepath); - var hash = hasher.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", ""); - } + // shim because this doesn't exist in .NET 4.6 + private static bool IsCompletedSuccessfully(this Task t) + { + return t.IsCompleted && !t.IsFaulted && !t.IsCanceled; + } + //credit to delta for this method https://github.com/XDelta/ + internal static string GenerateSHA256(string filepath) + { + using var hasher = SHA256.Create(); + using var stream = File.OpenRead(filepath); + var hash = hasher.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); } + + } } diff --git a/NeosModLoader/Utility/EnumerableInjector.cs b/NeosModLoader/Utility/EnumerableInjector.cs index b8e04ad..7ddf8fd 100644 --- a/NeosModLoader/Utility/EnumerableInjector.cs +++ b/NeosModLoader/Utility/EnumerableInjector.cs @@ -1,177 +1,177 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; namespace NeosModLoader.Utility { + /// + /// Provides the ability to inject actions into the execution of an enumeration while transforming it.

+ /// This example shows how to apply the when patching a function.
+ /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. + /// + /// private static void Postfix<Original, Transformed>(ref IEnumerable<Original> __result) where Transformed : Original + /// { + /// __result = new EnumerableInjector<Original, Transformed>(__result, + /// item => { Msg("Change what the item is exactly"); return new Transformed(item); }) + /// { + /// Prefix = () => Msg("Before the first item is returned"), + /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, + /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), + /// Postfix = () => Msg("When the generator stopped returning items") + /// }; + /// } + /// + ///
+ /// The type of the original enumeration's items. + /// The type of the transformed enumeration's items.
Must be assignable to TOriginal for compatibility.
+ public class EnumerableInjector : IEnumerable + where TTransformed : TOriginal + { /// - /// Provides the ability to inject actions into the execution of an enumeration while transforming it.

- /// This example shows how to apply the when patching a function.
- /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. - /// - /// private static void Postfix<Original, Transformed>(ref IEnumerable<Original> __result) where Transformed : Original - /// { - /// __result = new EnumerableInjector<Original, Transformed>(__result, - /// item => { Msg("Change what the item is exactly"); return new Transformed(item); }) - /// { - /// Prefix = () => Msg("Before the first item is returned"), - /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, - /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), - /// Postfix = () => Msg("When the generator stopped returning items") - /// }; - /// } - /// + /// Internal enumerator for iteration. ///
- /// The type of the original enumeration's items. - /// The type of the transformed enumeration's items.
Must be assignable to TOriginal for compatibility.
- public class EnumerableInjector : IEnumerable - where TTransformed : TOriginal + private readonly IEnumerator enumerator; + + private Action postfix = () => { }; + private Action postItem = (original, transformed, returned) => { }; + private Action prefix = () => { }; + private Func preItem = item => true; + private Func transformItem = item => throw new NotImplementedException("You're supposed to insert your own transformation function here!"); + + /// + /// Gets called when the wrapped enumeration returned the last item. + /// + public Action Postfix + { + get => postfix; + set => postfix = value ?? throw new ArgumentNullException(nameof(value), "Postfix can't be null!"); + } + + /// + /// Gets called for each item, with the transformed item, and whether it was passed through. + /// First thing to be called after execution returns to the enumerator after a yield return. + /// + public Action PostItem + { + get => postItem; + set => postItem = value ?? throw new ArgumentNullException(nameof(value), "PostItem can't be null!"); + } + + /// + /// Gets called before the enumeration returns the first item. + /// + public Action Prefix + { + get => prefix; + set => prefix = value ?? throw new ArgumentNullException(nameof(value), "Prefix can't be null!"); + } + + /// + /// Gets called for each item to determine whether it should be passed through. + /// + public Func PreItem + { + get => preItem; + set => preItem = value ?? throw new ArgumentNullException(nameof(value), "PreItem can't be null!"); + } + + /// + /// Gets called for each item to transform it, even if it won't be passed through. + /// + public Func TransformItem { - /// - /// Internal enumerator for iteration. - /// - private readonly IEnumerator enumerator; - - private Action postfix = () => { }; - private Action postItem = (original, transformed, returned) => { }; - private Action prefix = () => { }; - private Func preItem = item => true; - private Func transformItem = item => throw new NotImplementedException("You're supposed to insert your own transformation function here!"); - - /// - /// Gets called when the wrapped enumeration returned the last item. - /// - public Action Postfix - { - get => postfix; - set => postfix = value ?? throw new ArgumentNullException(nameof(value), "Postfix can't be null!"); - } - - /// - /// Gets called for each item, with the transformed item, and whether it was passed through. - /// First thing to be called after execution returns to the enumerator after a yield return. - /// - public Action PostItem - { - get => postItem; - set => postItem = value ?? throw new ArgumentNullException(nameof(value), "PostItem can't be null!"); - } - - /// - /// Gets called before the enumeration returns the first item. - /// - public Action Prefix - { - get => prefix; - set => prefix = value ?? throw new ArgumentNullException(nameof(value), "Prefix can't be null!"); - } - - /// - /// Gets called for each item to determine whether it should be passed through. - /// - public Func PreItem - { - get => preItem; - set => preItem = value ?? throw new ArgumentNullException(nameof(value), "PreItem can't be null!"); - } - - /// - /// Gets called for each item to transform it, even if it won't be passed through. - /// - public Func TransformItem - { - get => transformItem; - set => transformItem = value ?? throw new ArgumentNullException(nameof(value), "TransformItem can't be null!"); - } - - /// - /// Creates a new instance of the class using the supplied input enumerable and transform function. - /// - /// The enumerable to inject into and transform. - /// The transformation function. - public EnumerableInjector(IEnumerable enumerable, Func transformItem) - : this(enumerable.GetEnumerator(), transformItem) - { } - - /// - /// Creates a new instance of the class using the supplied input enumerator and transform function. - /// - /// The enumerator to inject into and transform. - /// The transformation function. - public EnumerableInjector(IEnumerator enumerator, Func transformItem) - { - this.enumerator = enumerator; - TransformItem = transformItem; - } - - /// - /// Injects into and transforms the input enumeration. - /// - /// The injected and transformed enumeration. - public IEnumerator GetEnumerator() - { - Prefix(); - - while (enumerator.MoveNext()) - { - var item = enumerator.Current; - var returnItem = PreItem(item); - var transformedItem = TransformItem(item); - - if (returnItem) - yield return transformedItem; - - PostItem(item, transformedItem, returnItem); - } - - Postfix(); - } - - /// - /// Injects into and transforms the input enumeration without a generic type. - /// - /// The injected and transformed enumeration without a generic type. - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + get => transformItem; + set => transformItem = value ?? throw new ArgumentNullException(nameof(value), "TransformItem can't be null!"); } /// - /// Provides the ability to inject actions into the execution of an enumeration without transforming it.

- /// This example shows how to apply the when patching a function.
- /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. - /// - /// static void Postfix<T>(ref IEnumerable<T> __result) - /// { - /// __result = new EnumerableInjector<T>(__result) - /// { - /// Prefix = () => Msg("Before the first item is returned"), - /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, - /// TransformItem = item => { Msg("Change what the item is exactly"); return item; }, - /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), - /// Postfix = () => Msg("When the generator stopped returning items") - /// }; - /// } - /// + /// Creates a new instance of the class using the supplied input enumerable and transform function. + ///
+ /// The enumerable to inject into and transform. + /// The transformation function. + public EnumerableInjector(IEnumerable enumerable, Func transformItem) + : this(enumerable.GetEnumerator(), transformItem) + { } + + /// + /// Creates a new instance of the class using the supplied input enumerator and transform function. /// - /// The type of the enumeration's items. - public class EnumerableInjector : EnumerableInjector + /// The enumerator to inject into and transform. + /// The transformation function. + public EnumerableInjector(IEnumerator enumerator, Func transformItem) { - /// - /// Creates a new instance of the class using the supplied input enumerable. - /// - /// The enumerable to inject into. - public EnumerableInjector(IEnumerable enumerable) - : this(enumerable.GetEnumerator()) - { } - - /// - /// Creates a new instance of the class using the supplied input enumerator. - /// - /// The enumerator to inject into. - public EnumerableInjector(IEnumerator enumerator) - : base(enumerator, item => item) - { } + this.enumerator = enumerator; + TransformItem = transformItem; } + + /// + /// Injects into and transforms the input enumeration. + /// + /// The injected and transformed enumeration. + public IEnumerator GetEnumerator() + { + Prefix(); + + while (enumerator.MoveNext()) + { + var item = enumerator.Current; + var returnItem = PreItem(item); + var transformedItem = TransformItem(item); + + if (returnItem) + yield return transformedItem; + + PostItem(item, transformedItem, returnItem); + } + + Postfix(); + } + + /// + /// Injects into and transforms the input enumeration without a generic type. + /// + /// The injected and transformed enumeration without a generic type. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// + /// Provides the ability to inject actions into the execution of an enumeration without transforming it.

+ /// This example shows how to apply the when patching a function.
+ /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. + /// + /// static void Postfix<T>(ref IEnumerable<T> __result) + /// { + /// __result = new EnumerableInjector<T>(__result) + /// { + /// Prefix = () => Msg("Before the first item is returned"), + /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, + /// TransformItem = item => { Msg("Change what the item is exactly"); return item; }, + /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), + /// Postfix = () => Msg("When the generator stopped returning items") + /// }; + /// } + /// + ///
+ /// The type of the enumeration's items. + public class EnumerableInjector : EnumerableInjector + { + /// + /// Creates a new instance of the class using the supplied input enumerable. + /// + /// The enumerable to inject into. + public EnumerableInjector(IEnumerable enumerable) + : this(enumerable.GetEnumerator()) + { } + + /// + /// Creates a new instance of the class using the supplied input enumerator. + /// + /// The enumerator to inject into. + public EnumerableInjector(IEnumerator enumerator) + : base(enumerator, item => item) + { } + } }