Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ResoniteModLoader/DelegateExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<Exception>();

foreach (var handler in del.GetInvocationList()) {
Expand Down
15 changes: 10 additions & 5 deletions ResoniteModLoader/ExecutionHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace ResoniteModLoader;

/// <summary>
/// Contains hooks for starting ResoniteModLoader initialization via <see cref="ModLoaderInit.Initialize()"/>.
/// Primarily uses <see cref="IPlatformConnector"/> to be called by the platform,
/// Additionally has a fallback <see cref="ModuleInitializerAttribute"/> in case the connector is not called.
/// </summary>
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() { }
Expand All @@ -32,7 +37,7 @@ public async Task<bool> Initialize(PlatformInterface platformInterface) {

#pragma warning disable CA2255
/// <summary>
/// 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.
/// </summary>
[ModuleInitializer]
public static void Init() {
Expand Down
2 changes: 1 addition & 1 deletion ResoniteModLoader/JsonConverters/EnumConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
5 changes: 3 additions & 2 deletions ResoniteModLoader/ModConfigurationDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion ResoniteModLoader/ModConfigurationKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal ModConfigurationKey(string name, string? description, bool internalAcce
/// </summary>
/// <param name="obj">The other object to compare against.</param>
/// <returns><c>true</c> if the other object is equal to this.</returns>
public override bool Equals(object obj) {
public override bool Equals(object? obj) {
return obj is ModConfigurationKey key && Name == key.Name;
}

Expand Down
8 changes: 4 additions & 4 deletions ResoniteModLoader/ModLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type> types;
IEnumerable<Type?> types;
try {
types = a.GetTypes();
} catch (ReflectionTypeLoadException e) {
Expand Down Expand Up @@ -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 ");
Expand Down Expand Up @@ -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}\"");
}
}
Expand Down
32 changes: 28 additions & 4 deletions ResoniteModLoader/ModLoaderConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Action<string>> keyActions = new() {
Expand All @@ -29,7 +29,7 @@ internal static ModLoaderConfiguration Get() {
string key = line[..splitIdx];
string value = line[(splitIdx + 1)..];

if (keyActions.TryGetValue(key, out Action<string> action)) {
if (keyActions.TryGetValue(key, out Action<string>? action)) {
try {
action(value);
} catch (Exception) {
Expand All @@ -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);
}

/// <summary>
/// When enabled, messages logged with <see cref="ResoniteMod.Debug(object)"/> or internal Debug methods will be added to the log file.
/// Additonally, mods may enable their own debug functionality via <see cref="ResoniteMod.IsDebugEnabled()"/> that isn't limited to logging.
/// </summary>
public bool Debug { get; internal set; }

/// <summary>
/// No mods will be loaded if this option is set.
/// </summary>
public bool NoMods { get; internal set; }

/// <summary>
/// No loading progress subphase names will be set if this option is active.
/// </summary>
public bool HideVisuals { get; internal set; }

/// <summary>
/// Currently this option does nothing. Previously, this would enable showing the unmodified version string.
/// </summary>
public bool AdvertiseVersion { get; internal set; }

/// <summary>
/// Whether to log warnings if one method is patched by multiple mods. Enabled by default.
/// </summary>
public bool LogConflicts { get; internal set; } = true;
}
20 changes: 20 additions & 0 deletions ResoniteModLoader/Settings/ModLoaderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,46 @@

namespace ResoniteModLoader;

/// <summary>
/// Settings UI for the mod loader itself.
/// </summary>
[AutoRegisterSetting]
[SettingCategory("ResoniteModLoader")]
public sealed class ModLoaderSettings : SettingComponent<ModLoaderSettings> {
/// <inheritdoc/>
public override bool UserspaceOnly => true;

#pragma warning disable CS8618 // Initializer generated by Resonite

/// <summary>How count of loaded mods.</summary>
[SettingIndicatorProperty]
public readonly Sync<string> LoadedMods;

/// <summary>The version of RML. Corresponds to <see cref="ModLoader.VERSION"/>.</summary>
[SettingIndicatorProperty]
public readonly Sync<string> ModLoaderVersion;

/// <summary>Corresponds to <see cref="ModLoaderConfiguration.Debug"/>.</summary>
[SettingProperty]
public readonly Sync<bool> DebugMode;

/// <summary>Corresponds to <see cref="ModLoaderConfiguration.HideVisuals"/>.</summary>
[SettingProperty]
public readonly Sync<bool> HideVisuals;

/// <summary>
/// Link to the RML GitHub repository at https://github.com/resonite-modding-group/ResoniteModLoader.
/// </summary>
//TODO make clickable link in UI
[SettingIndicatorProperty]
public readonly Sync<string> ProjectLink;

#pragma warning restore CS8618

/// <summary>
/// Since the Project Link indicator is not clickable,
/// this button provides this functionality instead.
/// </summary>
[SettingProperty("Open Github Repo", "")]
[SyncMethod(typeof(Action), [])]
public void OpenGithubRepo() {
Expand All @@ -50,6 +68,8 @@ protected override void OnChanges() {
ModLoaderVersion.Value = ModLoader.VERSION;
LoadedMods.Value = ModLoader.Mods().Count().ToString(CultureInfo.InvariantCulture);
}

/// <inheritdoc/>
protected override void OnStart() {
base.OnStart();
ModLoaderVersion.Value = ModLoader.VERSION;
Expand Down
4 changes: 4 additions & 0 deletions ResoniteModLoader/Settings/ModSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace ResoniteModLoader;

/// <summary>
/// TODO: Settings UI for loaded mods.
/// </summary>
[AutoRegisterSetting]
[SettingCategory("ResoniteModLoader")]
public sealed class ModSettings : SettingComponent<ModSettings> {
/// <inheritdoc/>
public override bool UserspaceOnly => true;
}
7 changes: 7 additions & 0 deletions ResoniteModLoader/Settings/SettingCategoryDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@

using ResoniteModLoader.Assets;

/// <summary>
/// Definitions for settings categories in the UI, contains <see cref="SettingCategoryInfo"/>.
/// </summary>
[DataModelType]
[SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Needs to exist in global namespace to be used")]
public static class SettingCategoryDefinitions {

/// <summary>
/// The RML settings category in the settings tab.
/// </summary>
[SettingCategory("ResoniteModLoader")]
public static SettingCategoryInfo ResoniteModLoader => new(RMLAssets.Icon, 0L);
}
8 changes: 5 additions & 3 deletions ResoniteModLoader/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -77,7 +77,9 @@ internal static IEnumerable<Type> 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<Type>(); // filter out null
} catch (Exception e) {
Logger.ErrorInternal($"Unhandled exception when processing loadable types: {e}");
return [];
Expand All @@ -86,7 +88,7 @@ internal static IEnumerable<Type> 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<Type> predicate) {
private static bool CheckType(Type? type, Predicate<Type> predicate) {
if (type == null) {
Logger.DebugInternal($"Passed in type was null");
return false;
Expand Down