diff --git a/NeosModLoader/AssemblyHider.cs b/NeosModLoader/AssemblyHider.cs new file mode 100644 index 0000000..ad4cf77 --- /dev/null +++ b/NeosModLoader/AssemblyHider.cs @@ -0,0 +1,76 @@ +using BaseX; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace NeosModLoader +{ + internal static class AssemblyHider + { + private static HashSet? neosAssemblies; + private static HashSet? modAssemblies; + + /// + /// Patch Neos's type lookup code to not see mod-related types. This is needed, because users can pass + /// arbitrary strings to TypeHelper.FindType(), which can be used to detect if someone is running mods. + /// + /// Our NML harmony instance + /// Assemblies that were loaded when NML first started + internal static void PatchNeos(Harmony harmony, HashSet initialAssemblies) + { + if (ModLoaderConfiguration.Get().HideModTypes) + { + neosAssemblies = GetNeosAssemblies(initialAssemblies); + modAssemblies = GetModAssemblies(); + MethodInfo target = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType)); + MethodInfo patch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); + harmony.Patch(target, postfix: new HarmonyMethod(patch)); + } + } + + private static HashSet GetNeosAssemblies(HashSet initialAssemblies) + { + // Remove NML itself, as its types should be hidden but it's guaranteed to be loaded. + initialAssemblies.Remove(Assembly.GetExecutingAssembly()); + + // Remove Harmony, as users who aren't using nml_libs will already have it loaded. + initialAssemblies.Remove(typeof(Harmony).Assembly); + + return initialAssemblies; + } + + private static HashSet GetModAssemblies() + { + // start with ALL assemblies + HashSet assemblies = AppDomain.CurrentDomain.GetAssemblies().ToHashSet(); + + // remove assemblies that already existed before NML loaded + assemblies.ExceptWith(neosAssemblies); + + return assemblies; + } + + private static void FindTypePostfix(ref Type? __result) + { + if (__result != null && !neosAssemblies!.Contains(__result.Assembly)) + { + if (!modAssemblies!.Contains(__result.Assembly)) + { + // an assembly was in neither neosAssemblies nor modAssemblies + // this implies someone late-loaded an assembly after NML, and it was later used in-game + // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. + Logger.WarnInternal($"The \"{__result}\" type does not appear to part of Neos or a mod. It is unclear whether it should be hidden or not."); + } + else + { + Type type = __result; + Logger.DebugFuncInternal(() => $"Hid type \"{type}\" from Neos"); + } + + // Pretend the type doesn't exist + __result = null; + } + } + } +} diff --git a/NeosModLoader/ExecutionHook.cs b/NeosModLoader/ExecutionHook.cs index 08ff293..0dda0a7 100644 --- a/NeosModLoader/ExecutionHook.cs +++ b/NeosModLoader/ExecutionHook.cs @@ -1,6 +1,8 @@ using FrooxEngine; using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace NeosModLoader { @@ -17,8 +19,11 @@ static ExecutionHook() { try { + HashSet initialAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToHashSet(); SplashChanger.SetCustom("Loading libraries"); AssemblyFile[] loadedAssemblies = AssemblyLoader.LoadAssembliesFromDir("nml_libs"); + // note that harmony may not be loaded until this point, so this class cannot directly inport HarmonyLib. + if (loadedAssemblies.Length != 0) { string loadedAssemblyList = string.Join("\n", loadedAssemblies.Select(a => a.Assembly.FullName + " Sha256=" + a.Sha256)); @@ -28,7 +33,7 @@ static ExecutionHook() SplashChanger.SetCustom("Initializing"); DebugInfo.Log(); NeosVersionReset.Initialize(); - ModLoader.LoadMods(); + HarmonyWorker.LoadModsAndHideModAssemblies(initialAssemblies); SplashChanger.SetCustom("Loaded"); } catch (Exception e) // it's important that this doesn't send exceptions back to Neos diff --git a/NeosModLoader/HarmonyWorker.cs b/NeosModLoader/HarmonyWorker.cs new file mode 100644 index 0000000..2f8fe7e --- /dev/null +++ b/NeosModLoader/HarmonyWorker.cs @@ -0,0 +1,18 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; + +namespace NeosModLoader +{ + // this class does all the harmony-related NML work. + // this is needed to avoid importing harmony in ExecutionHook, where it may not be loaded yet. + internal class HarmonyWorker + { + internal static void LoadModsAndHideModAssemblies(HashSet initialAssemblies) + { + Harmony harmony = new("com.neosmodloader"); + ModLoader.LoadMods(harmony); + AssemblyHider.PatchNeos(harmony, initialAssemblies); + } + } +} diff --git a/NeosModLoader/ModLoader.cs b/NeosModLoader/ModLoader.cs index 23b69f8..28d3048 100644 --- a/NeosModLoader/ModLoader.cs +++ b/NeosModLoader/ModLoader.cs @@ -31,7 +31,7 @@ public static IEnumerable Mods() .ToList(); } - internal static void LoadMods() + internal static void LoadMods(Harmony harmony) { ModLoaderConfiguration config = ModLoaderConfiguration.Get(); if (config.NoMods) @@ -92,7 +92,6 @@ internal static void LoadMods() } SplashChanger.SetCustom("Hooking big fish"); - Harmony harmony = new("net.michaelripley.neosmodloader"); ModConfiguration.RegisterShutdownHook(harmony); foreach (LoadedNeosMod mod in LoadedMods) diff --git a/NeosModLoader/ModLoaderConfiguration.cs b/NeosModLoader/ModLoaderConfiguration.cs index 8d416a2..5c0e254 100644 --- a/NeosModLoader/ModLoaderConfiguration.cs +++ b/NeosModLoader/ModLoaderConfiguration.cs @@ -59,6 +59,10 @@ internal static ModLoaderConfiguration Get() { _configuration.LogConflicts = false; } + else if ("hidemodtypes".Equals(key) && "false".Equals(value)) + { + _configuration.HideModTypes = false; + } } } } @@ -96,5 +100,6 @@ private static string GetAssemblyDirectory() public bool NoLibraries { get; private set; } = false; public bool AdvertiseVersion { get; private set; } = false; public bool LogConflicts { get; private set; } = true; + public bool HideModTypes { get; private set; } = true; } } diff --git a/doc/modloader_config.md b/doc/modloader_config.md index 595ae0a..6999d2e 100644 --- a/doc/modloader_config.md +++ b/doc/modloader_config.md @@ -18,3 +18,4 @@ Not all keys are required to be present. Missing keys will use the defaults outl | `advertiseversion` | `false` | If `false`, your version will be spoofed and will resemble `2021.8.29.1240`. If `true`, your version will be left unaltered and will resemble `2021.8.29.1240+NeosModLoader.dll`. This version string is visible to other players under certain circumstances. | | `unsafe` | `false` | If `true`, the version spoofing safety check is disabled and it will still work even if you have other Neos plugins. DO NOT load plugin components in multiplayer sessions, as it will break things and cause crashes. Plugin components should only be used in your local home or user space. | | `logconflicts` | `true` | If `false`, conflict logging will be disabled. If `true`, potential mod conflicts will be logged. If `debug` is also `true` this will be more verbose. | +| `hidemodtypes` | `true` | If `true`, mod-related types will be hidden in-game. If `false`, no types will be hidden, which makes NML detectable in-game. |