diff --git a/RecodeItLib/Models/AppSettingsModel.cs b/RecodeItLib/Models/AppSettingsModel.cs index 380d71a..a5f39a8 100644 --- a/RecodeItLib/Models/AppSettingsModel.cs +++ b/RecodeItLib/Models/AppSettingsModel.cs @@ -424,7 +424,7 @@ public class MappingSettings get { return _publicize; } set { - _unseal = value; + _publicize = value; Save(); } } diff --git a/RecodeItLib/Remapper/Publicizer.cs b/RecodeItLib/Remapper/Publicizer.cs index f6dbe8c..22f2fec 100644 --- a/RecodeItLib/Remapper/Publicizer.cs +++ b/RecodeItLib/Remapper/Publicizer.cs @@ -1,4 +1,7 @@ -using ReCodeIt.Utils; +using Mono.Cecil; +using Mono.Cecil.Rocks; +using ReCodeIt.Utils; +using System.Runtime.CompilerServices; namespace ReCodeIt.ReMapper; @@ -49,4 +52,143 @@ public static class Publicizer if (type.IsSealed) { type.IsSealed = false; } } } +} + +internal static class SPTPublicizer +{ + private static ModuleDefinition MainModule; + + public static void PublicizeClasses(ModuleDefinition definition) + { + var types = definition.GetAllTypes(); + + foreach (var type in types) + { + if (type.IsNested) continue; // Nested types are handled when publicizing the parent type + + PublicizeType(type); + } + } + + private static void PublicizeType(TypeDefinition type) + { + // if (type.CustomAttributes.Any(a => a.AttributeType.Name == + // nameof(CompilerGeneratedAttribute))) { return; } + + if (!type.IsNested && !type.IsPublic || type.IsNested && !type.IsNestedPublic) + { + type.Attributes &= ~TypeAttributes.VisibilityMask; // Remove all visibility mask attributes + type.Attributes |= type.IsNested ? TypeAttributes.NestedPublic : TypeAttributes.Public; // Apply a public visibility attribute + } + + if (type.IsSealed) + { + type.Attributes &= ~TypeAttributes.Sealed; // Remove the Sealed attribute if it exists + } + + foreach (var method in type.Methods) + { + PublicizeMethod(method); + } + + foreach (var property in type.Properties) + { + if (property.GetMethod != null) PublicizeMethod(property.GetMethod); + if (property.SetMethod != null) PublicizeMethod(property.SetMethod); + } + + // var eventNames = new HashSet(type.Events.Select(e => e.Name)); foreach (var field + // in type.Fields) { if (eventNames.Contains(field.Name)) { continue; } + // + // // if (type.Name.StartsWith("GClass") || !type.Namespace.Contains("EFT") || + // !type.Namespace.Contains("UI") || !string.IsNullOrWhiteSpace(type.Namespace)) // if + // (type.Namespace.Length > 0 && type.Namespace[0] > 'E') PublicizeField(field); } + + var nestedTypesToPublicize = type.NestedTypes.ToArray(); + + // Workaround to not publicize some nested types that cannot be patched easily and cause + // issues Specifically, we want to find any type that implements the "IHealthController" + // interface and make sure none of it's nested types that implement "IEffect" are changed + if (GetFlattenedInterfacesRecursive(type).Any(i => i.InterfaceType.Name == "IHealthController")) + { + // Specifically, any type that implements the IHealthController interface needs to not + // publicize any nested types that implement the IEffect interface + nestedTypesToPublicize = type.NestedTypes.Where(t => t.IsAbstract || t.Interfaces.All(i => i.InterfaceType.Name != "IEffect")).ToArray(); + } + + foreach (var nestedType in nestedTypesToPublicize) + { + PublicizeType(nestedType); + } + } + + // Don't publicize methods that implement interfaces not belonging to the current assembly + // Unused - sometimes some ambiguous reference errors appear due to this, but the pros outweigh + // the cons at the moment + private static bool CanPublicizeMethod(MethodDefinition method) + { + return !method.HasOverrides && method.GetBaseMethod().Equals(method) && !method.IsVirtual; + } + + private static void PublicizeMethod(MethodDefinition method) + { + if (method.IsCompilerControlled /*|| method.CustomAttributes.Any(a => a.AttributeType.Name == nameof(CompilerGeneratedAttribute))*/) + { + return; + } + + if (method.IsPublic) return; + + // if (!CanPublicizeMethod(method)) return; + + // Workaround to not publicize a specific method so the game doesn't crash + if (method.Name == "TryGetScreen") return; + + method.Attributes &= ~MethodAttributes.MemberAccessMask; + method.Attributes |= MethodAttributes.Public; + } + + // Unused for now - publicizing fields is tricky, as it often creates MonoBehaviour loading + // errors and prevents scenes from loading, most notably breaking the initial game loader scene + // and causing the game to CTD right after starting + private static void PublicizeField(FieldDefinition field) + { + if (field.CustomAttributes.Any(a => a.AttributeType.Name == nameof(CompilerGeneratedAttribute)) + // || field.HasCustomAttributes + || field.Name.StartsWith("delegate") + || field.Name.Contains("__BackingField")) + { + return; + } + + if (field.IsPublic || field.IsCompilerControlled || field.IsLiteral || field.IsStatic || field.IsInitOnly) return; + + field.Attributes &= ~FieldAttributes.FieldAccessMask; + field.Attributes |= FieldAttributes.Public; + } + + private static List GetFlattenedInterfacesRecursive(TypeDefinition type) + { + var interfaces = new List(); + + if (type is null) return interfaces; + + if (type.Interfaces.Any()) + { + interfaces.AddRange(type.Interfaces); + } + + if (type.BaseType != null && !type.BaseType.Name.Contains("Object")) + { + var baseTypeDefinition = MainModule?.ImportReference(type.BaseType)?.Resolve(); + var baseTypeInterfaces = GetFlattenedInterfacesRecursive(baseTypeDefinition); + + if (baseTypeInterfaces.Any()) + { + interfaces.AddRange(baseTypeInterfaces); + } + } + + return interfaces; + } } \ No newline at end of file diff --git a/RecodeItLib/Remapper/ReCodeItRemapper.cs b/RecodeItLib/Remapper/ReCodeItRemapper.cs index 6ece109..13c41a5 100644 --- a/RecodeItLib/Remapper/ReCodeItRemapper.cs +++ b/RecodeItLib/Remapper/ReCodeItRemapper.cs @@ -1,4 +1,6 @@ using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; using ReCodeIt.CrossCompiler; using ReCodeIt.Enums; using ReCodeIt.Models; @@ -66,15 +68,19 @@ public class ReCodeItRemapper ChooseBestMatches(); - // Dont publicize and unseal until after the remapping so we can use those as search parameters + // Don't publicize and unseal until after the remapping, so we can use those as search parameters if (Settings.MappingSettings.Publicize) { - Publicizer.Publicize(); + //Publicizer.Publicize(); + + Logger.Log("Publicizing classes...", ConsoleColor.Green); + + SPTPublicizer.PublicizeClasses(DataProvider.ModuleDefinition); } if (Settings.MappingSettings.Unseal) { - Publicizer.Unseal(); + //Publicizer.Unseal(); } // We are done, write the assembly @@ -297,10 +303,26 @@ public class ReCodeItRemapper /// private void WriteAssembly() { + if (!OutPath.EndsWith(".dll")) + { + var moduleName = DataProvider.AssemblyDefinition.MainModule.Name; + moduleName = moduleName.Replace(".dll", "-Remapped.dll"); + + OutPath = Path.Combine(OutPath, moduleName); + } + var path = DataProvider.WriteAssemblyDefinition(OutPath); + Logger.Log("-----------------------------------------------", ConsoleColor.Green); + Hollow(); + + var hollowedDir = Path.GetDirectoryName(OutPath); + var hollowedPath = Path.Combine(hollowedDir, "Hollowed.dll"); + DataProvider.WriteAssemblyDefinition(hollowedPath); + Logger.Log("-----------------------------------------------", ConsoleColor.Green); Logger.Log($"Complete: Assembly written to `{path}`", ConsoleColor.Green); + Logger.Log($"Complete: Hollowed written to `{hollowedPath}`", ConsoleColor.Green); Logger.Log("Original type names updated on mapping file.", ConsoleColor.Green); Logger.Log($"Remap took {Stopwatch.Elapsed.TotalSeconds:F1} seconds", ConsoleColor.Green); Logger.Log("-----------------------------------------------", ConsoleColor.Green); @@ -311,4 +333,43 @@ public class ReCodeItRemapper IsRunning = false; OnComplete?.Invoke(); } + + /// + /// Hollows out all logic from the dll + /// + private void Hollow() + { + foreach (var type in DataProvider.ModuleDefinition.GetAllTypes()) + { + foreach (var method in type.Methods.Where(m => m.HasBody)) + { + var ilProcessor = method.Body.GetILProcessor(); + + // Remove existing instructions + ilProcessor.Clear(); + + if (method.ReturnType.FullName != "System.Void") + { + // Return appropriate default value based on return type + if (method.ReturnType.IsValueType) + { + // Load 0 onto the stack (works for most value types) + ilProcessor.Emit(OpCodes.Ldc_I4_0); + + // Convert to Int64 if needed + if (method.ReturnType.FullName == "System.Int64") + ilProcessor.Emit(OpCodes.Conv_I8); + } + else + { + // Load null for reference types + ilProcessor.Emit(OpCodes.Ldnull); + } + } + + // Add a return instruction + ilProcessor.Emit(OpCodes.Ret); + } + } + } } \ No newline at end of file