diff --git a/ResoniteModLoader/DelegateExtensions.cs b/ResoniteModLoader/DelegateExtensions.cs index 131b10e..516a74a 100644 --- a/ResoniteModLoader/DelegateExtensions.cs +++ b/ResoniteModLoader/DelegateExtensions.cs @@ -1,7 +1,7 @@ namespace ResoniteModLoader; 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()) { diff --git a/ResoniteModLoader/ExecutionHook.cs b/ResoniteModLoader/ExecutionHook.cs index a17f0b7..8e3cceb 100644 --- a/ResoniteModLoader/ExecutionHook.cs +++ b/ResoniteModLoader/ExecutionHook.cs @@ -4,14 +4,19 @@ namespace ResoniteModLoader; +/// +/// Contains hooks for starting ResoniteModLoader initialization via . +/// Primarily uses to be called by the platform, +/// Additionally has a fallback in case the connector is not called. +/// public class ExecutionHook : IPlatformConnector { -#pragma warning disable CS1591 - public PlatformInterface Platform { get; private set; } +#pragma warning disable CS1591 // IPlatformConnector has no (official) documentation. + public PlatformInterface Platform { get; private set; } = null!; public int Priority => -10; public string PlatformName => "ResoniteModLoader"; - public string Username => null; - public string PlatformUserId => null; + public string Username => null!; + public string PlatformUserId => null!; public bool IsPlatformNameUnique => false; public void SetCurrentStatus(World world, bool isPrivate, int totalWorldCount) { } public void ClearCurrentStatus() { } @@ -32,7 +37,7 @@ public async Task Initialize(PlatformInterface platformInterface) { #pragma warning disable CA2255 /// - /// One method that can start the static constructor of the mod loader. + /// A fallback method that can start the static constructor of the mod loader. /// [ModuleInitializer] public static void Init() { diff --git a/ResoniteModLoader/JsonConverters/EnumConverter.cs b/ResoniteModLoader/JsonConverters/EnumConverter.cs index d24954f..dc65fe0 100644 --- a/ResoniteModLoader/JsonConverters/EnumConverter.cs +++ b/ResoniteModLoader/JsonConverters/EnumConverter.cs @@ -25,7 +25,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - string serialized = Enum.GetName(value!.GetType(), value); + string? serialized = Enum.GetName(value!.GetType(), value); writer.WriteValue(serialized); } diff --git a/ResoniteModLoader/JsonConverters/ResonitePrimitiveConverter.cs b/ResoniteModLoader/JsonConverters/ResonitePrimitiveConverter.cs index 4ad47bc..3c79b97 100644 --- a/ResoniteModLoader/JsonConverters/ResonitePrimitiveConverter.cs +++ b/ResoniteModLoader/JsonConverters/ResonitePrimitiveConverter.cs @@ -15,14 +15,14 @@ public override bool CanConvert(Type objectType) { public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader.Value is string serialized) { // use Resonite's built-in decoding if the value was serialized as a string - return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString").Invoke(null, [serialized]); + return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString")!.Invoke(null, [serialized])!; } throw new ArgumentException($"Could not deserialize a Core Element 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, [value]); + string serialized = (string)typeof(Coder<>).MakeGenericType(value!.GetType()).GetMethod("EncodeToString")!.Invoke(null, [value])!; writer.WriteValue(serialized); } } diff --git a/ResoniteModLoader/ModConfigurationDefinitionBuilder.cs b/ResoniteModLoader/ModConfigurationDefinitionBuilder.cs index 99351c9..061fe3c 100644 --- a/ResoniteModLoader/ModConfigurationDefinitionBuilder.cs +++ b/ResoniteModLoader/ModConfigurationDefinitionBuilder.cs @@ -66,8 +66,9 @@ private void ProcessField(FieldInfo field) { return; } - ModConfigurationKey fieldValue = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : Owner); - Keys.Add(fieldValue); + if (field.GetValue(field.IsStatic ? null : Owner) is ModConfigurationKey fieldValue) { + Keys.Add(fieldValue); + } } internal ModConfigurationDefinition? Build() { diff --git a/ResoniteModLoader/ModConfigurationKey.cs b/ResoniteModLoader/ModConfigurationKey.cs index a9cc262..2b588f4 100644 --- a/ResoniteModLoader/ModConfigurationKey.cs +++ b/ResoniteModLoader/ModConfigurationKey.cs @@ -62,7 +62,7 @@ internal ModConfigurationKey(string name, string? description, bool internalAcce /// /// The other object to compare against. /// true if the other object is equal to this. - public override bool Equals(object obj) { + public override bool Equals(object? obj) { return obj is ModConfigurationKey key && Name == key.Name; } diff --git a/ResoniteModLoader/ModLoader.cs b/ResoniteModLoader/ModLoader.cs index 6ac83b2..d86e9c8 100644 --- a/ResoniteModLoader/ModLoader.cs +++ b/ResoniteModLoader/ModLoader.cs @@ -23,7 +23,7 @@ public sealed class ModLoader { public static bool IsHeadless { // Extremely thorough, but doesn't rely on any specific class to check for headless presence get { return _isHeadless ??= AppDomain.CurrentDomain.GetAssemblies().Any(a => { - IEnumerable types; + IEnumerable types; try { types = a.GetTypes(); } catch (ReflectionTypeLoadException e) { @@ -82,8 +82,8 @@ internal static void LoadMods() { // 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}"); + 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 "); @@ -118,7 +118,7 @@ internal static void LoadMods() { Logger.WarnInternal($" \"{owner}\" ({TypesForOwner(patches, owner)})"); } } else if (config.Debug) { - string owner = owners.FirstOrDefault(); + string? owner = owners.FirstOrDefault(); Logger.DebugFuncInternal(() => $"Method \"{patchedMethod.FullDescription()}\" has been patched by \"{owner}\""); } } diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index b16a11d..def87db 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -8,7 +8,7 @@ internal sealed class ModLoaderConfiguration { 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); + string path = GetConfigPath(); _configuration = new ModLoaderConfiguration(); Dictionary> keyActions = new() { @@ -29,7 +29,7 @@ internal static ModLoaderConfiguration Get() { string key = line[..splitIdx]; string value = line[(splitIdx + 1)..]; - if (keyActions.TryGetValue(key, out Action action)) { + if (keyActions.TryGetValue(key, out Action? action)) { try { action(value); } catch (Exception) { @@ -51,13 +51,37 @@ internal static ModLoaderConfiguration Get() { return _configuration; } - private static string GetAssemblyDirectory() { - return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + private static string GetConfigPath() { + var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (dir == null) { + return CONFIG_FILENAME; + } + return Path.Combine(dir, CONFIG_FILENAME); } + /// + /// When enabled, messages logged with or internal Debug methods will be added to the log file. + /// Additonally, mods may enable their own debug functionality via that isn't limited to logging. + /// public bool Debug { get; internal set; } + + /// + /// No mods will be loaded if this option is set. + /// public bool NoMods { get; internal set; } + + /// + /// No loading progress subphase names will be set if this option is active. + /// public bool HideVisuals { get; internal set; } + + /// + /// Currently this option does nothing. Previously, this would enable showing the unmodified version string. + /// public bool AdvertiseVersion { get; internal set; } + + /// + /// Whether to log warnings if one method is patched by multiple mods. Enabled by default. + /// public bool LogConflicts { get; internal set; } = true; } diff --git a/ResoniteModLoader/Settings/ModLoaderSettings.cs b/ResoniteModLoader/Settings/ModLoaderSettings.cs index 66ef870..89263d0 100644 --- a/ResoniteModLoader/Settings/ModLoaderSettings.cs +++ b/ResoniteModLoader/Settings/ModLoaderSettings.cs @@ -4,28 +4,46 @@ namespace ResoniteModLoader; +/// +/// Settings UI for the mod loader itself. +/// [AutoRegisterSetting] [SettingCategory("ResoniteModLoader")] public sealed class ModLoaderSettings : SettingComponent { /// public override bool UserspaceOnly => true; +#pragma warning disable CS8618 // Initializer generated by Resonite + + /// How count of loaded mods. [SettingIndicatorProperty] public readonly Sync LoadedMods; + /// The version of RML. Corresponds to . [SettingIndicatorProperty] public readonly Sync ModLoaderVersion; + /// Corresponds to . [SettingProperty] public readonly Sync DebugMode; + /// Corresponds to . [SettingProperty] public readonly Sync HideVisuals; + /// + /// Link to the RML GitHub repository at https://github.com/resonite-modding-group/ResoniteModLoader. + /// //TODO make clickable link in UI [SettingIndicatorProperty] public readonly Sync ProjectLink; +#pragma warning restore CS8618 + + /// + /// Since the Project Link indicator is not clickable, + /// this button provides this functionality instead. + /// [SettingProperty("Open Github Repo", "")] [SyncMethod(typeof(Action), [])] public void OpenGithubRepo() { @@ -50,6 +68,8 @@ protected override void OnChanges() { ModLoaderVersion.Value = ModLoader.VERSION; LoadedMods.Value = ModLoader.Mods().Count().ToString(CultureInfo.InvariantCulture); } + + /// protected override void OnStart() { base.OnStart(); ModLoaderVersion.Value = ModLoader.VERSION; diff --git a/ResoniteModLoader/Settings/ModSettings.cs b/ResoniteModLoader/Settings/ModSettings.cs index 18188ca..2fcec72 100644 --- a/ResoniteModLoader/Settings/ModSettings.cs +++ b/ResoniteModLoader/Settings/ModSettings.cs @@ -2,8 +2,12 @@ namespace ResoniteModLoader; +/// +/// TODO: Settings UI for loaded mods. +/// [AutoRegisterSetting] [SettingCategory("ResoniteModLoader")] public sealed class ModSettings : SettingComponent { + /// public override bool UserspaceOnly => true; } diff --git a/ResoniteModLoader/Settings/SettingCategoryDefinitions.cs b/ResoniteModLoader/Settings/SettingCategoryDefinitions.cs index 1fdec4e..d60b68e 100644 --- a/ResoniteModLoader/Settings/SettingCategoryDefinitions.cs +++ b/ResoniteModLoader/Settings/SettingCategoryDefinitions.cs @@ -4,9 +4,16 @@ using ResoniteModLoader.Assets; +/// +/// Definitions for settings categories in the UI, contains . +/// [DataModelType] [SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Needs to exist in global namespace to be used")] public static class SettingCategoryDefinitions { + + /// + /// The RML settings category in the settings tab. + /// [SettingCategory("ResoniteModLoader")] public static SettingCategoryInfo ResoniteModLoader => new(RMLAssets.Icon, 0L); } diff --git a/ResoniteModLoader/Util.cs b/ResoniteModLoader/Util.cs index 5998906..4a5536e 100644 --- a/ResoniteModLoader/Util.cs +++ b/ResoniteModLoader/Util.cs @@ -13,7 +13,7 @@ internal static class Util { internal static ResoniteMod? ExecutingMod(StackTrace stackTrace) { for (int i = 0; i < stackTrace.FrameCount; i++) { Assembly? assembly = stackTrace.GetFrame(i)?.GetMethod()?.DeclaringType?.Assembly; - if (assembly != null && ModLoader.AssemblyLookupMap.TryGetValue(assembly, out ResoniteMod mod)) { + if (assembly != null && ModLoader.AssemblyLookupMap.TryGetValue(assembly, out ResoniteMod? mod)) { return mod; } } @@ -77,7 +77,9 @@ internal static IEnumerable GetLoadableTypes(this Assembly assembly, Predi return assembly.GetTypes().Where(type => CheckType(type, predicate)); } catch (ReflectionTypeLoadException e) { Logger.ErrorInternal(e); - return e.Types.Where(type => CheckType(type, predicate)); + return e.Types + .Where(type => CheckType(type, predicate)) + .OfType(); // filter out null } catch (Exception e) { Logger.ErrorInternal($"Unhandled exception when processing loadable types: {e}"); return []; @@ -86,7 +88,7 @@ internal static IEnumerable GetLoadableTypes(this Assembly assembly, Predi // Check a potentially unloadable type to see if it is (A) loadable and (B) satisfies a predicate without throwing an exception // this does a series of increasingly aggressive checks to see if the type is unsafe to touch - private static bool CheckType(Type type, Predicate predicate) { + private static bool CheckType(Type? type, Predicate predicate) { if (type == null) { Logger.DebugInternal($"Passed in type was null"); return false;