875 lines
35 KiB
C#
875 lines
35 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Text.RegularExpressions;
|
||
using OPS.Mono.Cecil;
|
||
using TH1_Logic.Hotfix;
|
||
using UnityEditor;
|
||
using UnityEditor.Build;
|
||
using UnityEngine;
|
||
|
||
namespace TH1_Logic.Editor.HybridCLR
|
||
{
|
||
public static class TH1HybridCLRBuildTools
|
||
{
|
||
private const string HybridClrInstallerMenu = "HybridCLR/Installer...";
|
||
private const string HybridClrGenerateAllMenu = "HybridCLR/Generate/All";
|
||
private const string OpsObfuscatorSettingsPath = "Assets/OPS/Obfuscator/Settings/Obfuscator_Settings.json";
|
||
private static readonly Regex OpsObfuscationRegex = new Regex(
|
||
"(?<prefix>\"Key\"\\s*:\\s*\"Global_Enable_Obfuscation\"\\s*,\\s*\"Value\"\\s*:\\s*\")(?<value>True|False)(?<suffix>\")",
|
||
RegexOptions.Compiled);
|
||
|
||
public const long MinimumHotfixDllSize = 64 * 1024;
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/1. Run HybridCLR Installer")]
|
||
public static void RunHybridClrInstaller()
|
||
{
|
||
ExecuteHybridClrMenu(HybridClrInstallerMenu);
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/2. Configure TH1 Hotfix Settings")]
|
||
public static void ConfigureHotfixSettings()
|
||
{
|
||
var settingsType = FindType("HybridCLR.Editor.Settings.HybridCLRSettings");
|
||
if (settingsType == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] HybridCLR package is not available yet. Let Unity resolve packages, then run this menu again.");
|
||
return;
|
||
}
|
||
|
||
var instance = settingsType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
||
if (instance == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] HybridCLRSettings.Instance is unavailable.");
|
||
return;
|
||
}
|
||
|
||
SetBoolField(settingsType, instance, "enable", true);
|
||
AddStringToArrayField(settingsType, instance, "hotUpdateAssemblies", HotfixManifest.HotfixAssemblyName);
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "mscorlib");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System.Core");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "MemoryPack");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System.Runtime.CompilerServices.Unsafe");
|
||
|
||
settingsType.GetMethod("Save", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
|
||
Debug.Log("[TH1.HybridCLR] Configured HybridCLRSettings for TH1.Hotfix.");
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/3. Generate All")]
|
||
public static void GenerateAll()
|
||
{
|
||
using (TH1MigrationBuildValidationGate.Suppress())
|
||
{
|
||
EnsureHybridClrInstalled();
|
||
InvokeHybridClrGenerateAll();
|
||
EnsureAotMetadataSourceFiles(EditorUserBuildSettings.activeBuildTarget);
|
||
}
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/4. Compile Hotfix Dll")]
|
||
public static void CompileHotfixDll()
|
||
{
|
||
TryCompileHotfixDll(EditorUserBuildSettings.development);
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/5. Copy Hotfix Artifacts To StreamingAssets")]
|
||
public static void CopyHotfixArtifactsToStreamingAssets()
|
||
{
|
||
BuildAndCopyHotfixArtifacts(EditorUserBuildSettings.development);
|
||
}
|
||
|
||
public static bool BuildAndCopyHotfixArtifacts(bool developmentBuild)
|
||
{
|
||
return BuildAndCopyHotfixArtifacts(developmentBuild, IsHotfixObfuscationEnabled());
|
||
}
|
||
|
||
public static bool BuildAndCopyHotfixArtifacts(bool developmentBuild, bool obfuscateHotfixDll)
|
||
{
|
||
if (!TryCompileHotfixDll(developmentBuild))
|
||
{
|
||
Debug.LogError("[TH1.HybridCLR] Compile hotfix dll failed. Skip copying hotfix artifacts.");
|
||
return false;
|
||
}
|
||
|
||
if (!TryPrepareHotfixDllForCopy(obfuscateHotfixDll, out var hotfixDllPath))
|
||
{
|
||
Debug.LogError("[TH1.HybridCLR] Prepare hotfix dll failed. Skip copying hotfix artifacts.");
|
||
return false;
|
||
}
|
||
|
||
var copiedHotfixDll = CopyHotfixDll(hotfixDllPath);
|
||
var copiedAotMetadata = CopyAotMetadataDlls();
|
||
AssetDatabase.Refresh();
|
||
return copiedHotfixDll && copiedAotMetadata;
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/6. Test Load StreamingAssets Hotfix In Editor")]
|
||
public static void TestLoadHotfixInEditor()
|
||
{
|
||
var loaded = HotfixBootstrap.InitializeFromStreamingAssets(true);
|
||
Debug.Log($"[TH1.HybridCLR] Test load result: {loaded}, assembly: {HotfixBootstrap.LoadedAssemblyFullName}");
|
||
}
|
||
|
||
public static string GetHotfixDllSourcePath(BuildTarget buildTarget)
|
||
{
|
||
return Path.Combine("HybridCLRData", "HotUpdateDlls", buildTarget.ToString(), HotfixManifest.HotfixAssemblyName + ".dll");
|
||
}
|
||
|
||
public static string GetObfuscatedHotfixDllPath(BuildTarget buildTarget)
|
||
{
|
||
return Path.Combine("HybridCLRData", "ObfuscatedHotUpdateDlls", buildTarget.ToString(), HotfixManifest.HotfixAssemblyName + ".dll");
|
||
}
|
||
|
||
public static string GetStreamingHotfixDllPath()
|
||
{
|
||
return Path.Combine(
|
||
Application.streamingAssetsPath,
|
||
HotfixManifest.RootFolderName,
|
||
HotfixManifest.HotfixDllFolderName,
|
||
HotfixManifest.HotfixAssemblyFileName);
|
||
}
|
||
|
||
public static string GetAotMetadataSourceDir(BuildTarget buildTarget)
|
||
{
|
||
return Path.Combine("HybridCLRData", "AssembliesPostIl2CppStrip", buildTarget.ToString());
|
||
}
|
||
|
||
public static string GetStreamingAotMetadataDir()
|
||
{
|
||
return Path.Combine(Application.streamingAssetsPath, HotfixManifest.RootFolderName, HotfixManifest.AotMetadataFolderName);
|
||
}
|
||
|
||
public static bool IsUsableHotfixDll(string path, out string message)
|
||
{
|
||
if (!File.Exists(path))
|
||
{
|
||
message = $"缺失: {path}";
|
||
return false;
|
||
}
|
||
|
||
var file = new FileInfo(path);
|
||
if (file.Length < MinimumHotfixDllSize)
|
||
{
|
||
message = $"文件过小,通常表示还是旧的空壳 DLL({FormatBytes(file.Length)}): {path}";
|
||
return false;
|
||
}
|
||
|
||
message = $"OK({FormatBytes(file.Length)},{file.LastWriteTime:yyyy-MM-dd HH:mm:ss})";
|
||
return true;
|
||
}
|
||
|
||
private static bool TryPrepareHotfixDllForCopy(bool obfuscateHotfixDll, out string hotfixDllPath)
|
||
{
|
||
var source = GetHotfixDllSourcePath(EditorUserBuildSettings.activeBuildTarget);
|
||
hotfixDllPath = source;
|
||
|
||
if (!IsUsableHotfixDll(source, out var message))
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] hotfix dll is not usable before obfuscation: {message}");
|
||
return false;
|
||
}
|
||
|
||
if (!obfuscateHotfixDll)
|
||
{
|
||
Debug.Log("[TH1.HybridCLR] OPS obfuscation is disabled. Copy raw hotfix dll.");
|
||
return true;
|
||
}
|
||
|
||
return TryObfuscateHotfixDll(source, out hotfixDllPath);
|
||
}
|
||
|
||
private static bool CopyHotfixDll(string source)
|
||
{
|
||
if (File.Exists(source) && new FileInfo(source).Length < MinimumHotfixDllSize)
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] hotfix dll is too small and looks stale: {source}");
|
||
return false;
|
||
}
|
||
|
||
var destination = GetStreamingHotfixDllPath();
|
||
|
||
return CopyFile(source, destination, "hotfix dll");
|
||
}
|
||
|
||
public static bool IsHotfixObfuscationEnabled()
|
||
{
|
||
var path = GetProjectPath(OpsObfuscatorSettingsPath);
|
||
if (!File.Exists(path))
|
||
{
|
||
Debug.LogWarning($"[TH1.HybridCLR] OPS settings missing. Hotfix dll will not be obfuscated: {OpsObfuscatorSettingsPath}");
|
||
return false;
|
||
}
|
||
|
||
if (!TryReadOpsObfuscationEnabled(out var enabled))
|
||
{
|
||
Debug.LogWarning($"[TH1.HybridCLR] Cannot read Global_Enable_Obfuscation from {OpsObfuscatorSettingsPath}. Hotfix dll will not be obfuscated.");
|
||
return false;
|
||
}
|
||
|
||
return enabled;
|
||
}
|
||
|
||
private static bool TryObfuscateHotfixDll(string source, out string obfuscatedPath)
|
||
{
|
||
var targetPath = GetObfuscatedHotfixDllPath(EditorUserBuildSettings.activeBuildTarget);
|
||
obfuscatedPath = targetPath;
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
|
||
File.Copy(source, targetPath, true);
|
||
|
||
RunWithOpsObfuscationTemporarilyEnabled(() => RunOpsObfuscationForHotfixDll(targetPath));
|
||
|
||
if (!IsUsableHotfixDll(targetPath, out var message))
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] obfuscated hotfix dll is not usable: {message}");
|
||
return false;
|
||
}
|
||
|
||
if (!VerifyHotfixObfuscationKeepRules(targetPath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
Debug.Log($"[TH1.HybridCLR] Obfuscated hotfix dll: {targetPath}");
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] OPS obfuscate hotfix dll failed:\n{e}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static void RunWithOpsObfuscationTemporarilyEnabled(System.Action action)
|
||
{
|
||
var canRestore = TryReadOpsObfuscationEnabled(out var previousEnabled);
|
||
if (canRestore && !previousEnabled)
|
||
{
|
||
SetOpsObfuscationEnabled(true);
|
||
}
|
||
|
||
try
|
||
{
|
||
action();
|
||
}
|
||
finally
|
||
{
|
||
if (canRestore)
|
||
{
|
||
SetOpsObfuscationEnabled(previousEnabled);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static bool TryReadOpsObfuscationEnabled(out bool enabled)
|
||
{
|
||
enabled = false;
|
||
var path = GetProjectPath(OpsObfuscatorSettingsPath);
|
||
if (!File.Exists(path))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var json = File.ReadAllText(path);
|
||
var matches = OpsObfuscationRegex.Matches(json);
|
||
if (matches.Count != 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
enabled = string.Equals(matches[0].Groups["value"].Value, "True", StringComparison.OrdinalIgnoreCase);
|
||
return true;
|
||
}
|
||
|
||
public static void SetOpsObfuscationEnabled(bool enabled)
|
||
{
|
||
var path = GetProjectPath(OpsObfuscatorSettingsPath);
|
||
if (!File.Exists(path))
|
||
{
|
||
throw new FileNotFoundException($"OPS obfuscator settings not found: {OpsObfuscatorSettingsPath}", path);
|
||
}
|
||
|
||
var json = File.ReadAllText(path);
|
||
var matches = OpsObfuscationRegex.Matches(json);
|
||
if (matches.Count != 1)
|
||
{
|
||
throw new InvalidOperationException($"Cannot find a unique Global_Enable_Obfuscation entry in {OpsObfuscatorSettingsPath}.");
|
||
}
|
||
|
||
var updated = OpsObfuscationRegex.Replace(json, match =>
|
||
$"{match.Groups["prefix"].Value}{(enabled ? "True" : "False")}{match.Groups["suffix"].Value}", 1);
|
||
|
||
if (updated == json) return;
|
||
|
||
File.WriteAllText(path, updated);
|
||
AssetDatabase.ImportAsset(OpsObfuscatorSettingsPath);
|
||
}
|
||
|
||
private static void RunOpsObfuscationForHotfixDll(string hotfixDllPath)
|
||
{
|
||
var editorSettingsType = RequireType("OPS.Obfuscator.Editor.Settings.Unity.Editor.EditorSettings");
|
||
var buildSettingsType = RequireType("OPS.Obfuscator.Editor.Settings.Unity.Build.BuildSettings");
|
||
var assemblyLoadInfoType = RequireType("OPS.Obfuscator.Editor.Assembly.AssemblyLoadInfo");
|
||
var obfuscatorType = RequireType("OPS.Obfuscator.Editor.Obfuscator");
|
||
|
||
var editorSettings = CreateInstance(editorSettingsType);
|
||
var buildSettings = CreateInstance(buildSettingsType);
|
||
|
||
SetMember(buildSettings, "IsDevelopmentBuild", EditorUserBuildSettings.development);
|
||
SetMember(buildSettings, "BuildTarget", EditorUserBuildSettings.activeBuildTarget);
|
||
SetMember(buildSettings, "BuildTargetGroup", EditorUserBuildSettings.selectedBuildTargetGroup);
|
||
SetMember(buildSettings, "UnityBuildReport", null);
|
||
SetMember(buildSettings, "IsIL2CPPBuild", PlayerSettings.GetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup) == ScriptingImplementation.IL2CPP);
|
||
var compression = GetOpsCompressionValue(buildSettingsType);
|
||
if (compression != null)
|
||
{
|
||
TrySetMember(buildSettings, "Compression", compression);
|
||
}
|
||
SetMember(buildSettings, "BuildIntoProject", false);
|
||
SetMember(buildSettings, "AssemblyDependencyDirectoryPathList", CreateStringList(GetOpsDependencyDirectories(hotfixDllPath)));
|
||
SetMember(buildSettings, "AssemblyLoadInfoList", CreateAssemblyLoadInfoList(assemblyLoadInfoType, hotfixDllPath));
|
||
|
||
obfuscatorType.GetMethod("Init", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
|
||
var singleton = obfuscatorType.GetProperty("Singleton", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
||
if (singleton == null)
|
||
{
|
||
throw new InvalidOperationException("OPS Obfuscator singleton is unavailable.");
|
||
}
|
||
|
||
EditorApplication.LockReloadAssemblies();
|
||
try
|
||
{
|
||
var method = obfuscatorType.GetMethod("PostAssemblyBuild", BindingFlags.Public | BindingFlags.Instance);
|
||
if (method == null)
|
||
{
|
||
throw new MissingMethodException(obfuscatorType.FullName, "PostAssemblyBuild");
|
||
}
|
||
|
||
method.Invoke(singleton, new[] { editorSettings, buildSettings });
|
||
}
|
||
catch (TargetInvocationException e)
|
||
{
|
||
throw e.InnerException ?? e;
|
||
}
|
||
finally
|
||
{
|
||
EditorApplication.UnlockReloadAssemblies();
|
||
}
|
||
}
|
||
|
||
private static IList CreateAssemblyLoadInfoList(Type assemblyLoadInfoType, string hotfixDllPath)
|
||
{
|
||
var listType = typeof(List<>).MakeGenericType(assemblyLoadInfoType);
|
||
var list = (IList)Activator.CreateInstance(listType);
|
||
list.Add(CreateAssemblyLoadInfo(assemblyLoadInfoType, hotfixDllPath, true, false));
|
||
return list;
|
||
}
|
||
|
||
private static object CreateAssemblyLoadInfo(Type assemblyLoadInfoType, string filePath, bool obfuscate, bool helper)
|
||
{
|
||
var loadInfo = CreateInstance(assemblyLoadInfoType);
|
||
SetMember(loadInfo, "FilePath", Path.GetFullPath(filePath));
|
||
SetMember(loadInfo, "Obfuscate", obfuscate);
|
||
TrySetMember(loadInfo, "IsUnityAssembly", !helper);
|
||
TrySetMember(loadInfo, "IsThirdPartyAssembly", helper);
|
||
TrySetMember(loadInfo, "IsThirdParty", helper);
|
||
TrySetMember(loadInfo, "IsHelperAssembly", helper);
|
||
TrySetMember(loadInfo, "IsHelper", helper);
|
||
return loadInfo;
|
||
}
|
||
|
||
private static List<string> CreateStringList(IEnumerable<string> values)
|
||
{
|
||
return values
|
||
.Where(path => !string.IsNullOrWhiteSpace(path))
|
||
.Select(Path.GetFullPath)
|
||
.Where(Directory.Exists)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
}
|
||
|
||
private static IEnumerable<string> GetOpsDependencyDirectories(string hotfixDllPath)
|
||
{
|
||
yield return Path.GetDirectoryName(hotfixDllPath);
|
||
yield return GetProjectPath("Library/ScriptAssemblies");
|
||
yield return GetProjectPath("Assets/Plugins");
|
||
yield return GetProjectPath("Assets/OPS/Plugins");
|
||
yield return GetProjectPath("Assets/OPS/Obfuscator/Plugins");
|
||
yield return GetProjectPath("Assets/OPS/Obfuscator/Editor/Plugins");
|
||
|
||
var unityEditorDir = EditorApplication.applicationContentsPath;
|
||
yield return Path.Combine(unityEditorDir, "Managed");
|
||
yield return Path.Combine(unityEditorDir, "Managed", "UnityEngine");
|
||
yield return Path.Combine(unityEditorDir, "UnityReferenceAssemblies", "unity-4.8-api");
|
||
yield return Path.Combine(unityEditorDir, "UnityReferenceAssemblies", "unity-4.8-api", "Facades");
|
||
|
||
var packageCache = GetProjectPath("Library/PackageCache");
|
||
if (Directory.Exists(packageCache))
|
||
{
|
||
foreach (var dll in Directory.EnumerateFiles(packageCache, "*.dll", SearchOption.AllDirectories))
|
||
{
|
||
yield return Path.GetDirectoryName(dll);
|
||
}
|
||
}
|
||
|
||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||
{
|
||
if (assembly.IsDynamic) continue;
|
||
var location = string.Empty;
|
||
try
|
||
{
|
||
location = assembly.Location;
|
||
}
|
||
catch
|
||
{
|
||
// Some editor assemblies do not expose a stable file location.
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(location))
|
||
{
|
||
yield return Path.GetDirectoryName(location);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static bool VerifyHotfixObfuscationKeepRules(string hotfixDllPath)
|
||
{
|
||
try
|
||
{
|
||
using var assembly = AssemblyDefinition.ReadAssembly(hotfixDllPath);
|
||
var missing = new List<string>();
|
||
|
||
var entryType = FindCecilType(assembly.MainModule, HotfixManifest.EntryTypeName);
|
||
if (entryType == null)
|
||
{
|
||
missing.Add(HotfixManifest.EntryTypeName);
|
||
}
|
||
else if (!entryType.Methods.Any(method =>
|
||
method.Name == HotfixManifest.EntryMethodName &&
|
||
method.IsPublic &&
|
||
method.IsStatic))
|
||
{
|
||
missing.Add($"{HotfixManifest.EntryTypeName}.{HotfixManifest.EntryMethodName}");
|
||
}
|
||
|
||
var mainType = FindCecilType(assembly.MainModule, HotfixManifest.RuntimeMainTypeName);
|
||
if (mainType == null)
|
||
{
|
||
missing.Add(HotfixManifest.RuntimeMainTypeName);
|
||
}
|
||
else
|
||
{
|
||
foreach (var fieldName in RequiredRuntimeMainFieldNames)
|
||
{
|
||
if (!mainType.Fields.Any(field => field.Name == fieldName))
|
||
{
|
||
missing.Add($"{HotfixManifest.RuntimeMainTypeName}.{fieldName}");
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var typeName in RequiredAchievementConditionTypeNames)
|
||
{
|
||
if (FindCecilType(assembly.MainModule, typeName) == null)
|
||
{
|
||
missing.Add(typeName);
|
||
}
|
||
}
|
||
|
||
if (missing.Count <= 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
Debug.LogError($"[TH1.HybridCLR] obfuscated hotfix dll is missing required reflection metadata: {string.Join(", ", missing)}");
|
||
return false;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] verify obfuscated hotfix dll failed: {hotfixDllPath}\n{e}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static readonly string[] RequiredRuntimeMainFieldNames =
|
||
{
|
||
"NoAI",
|
||
"FullSight",
|
||
"AIActionTime",
|
||
"AIAllTech",
|
||
"AIMoreMoney",
|
||
"LandThreshold",
|
||
"AnimationSpeed",
|
||
"DebugMode",
|
||
"DebugHideCenterMessage",
|
||
"cityCount",
|
||
"unitCount",
|
||
"turn",
|
||
"renko",
|
||
"IsNetActionExecuting",
|
||
"ROMapRenderer",
|
||
"URPDefaultMat",
|
||
};
|
||
|
||
private static readonly string[] RequiredAchievementConditionTypeNames =
|
||
{
|
||
"Logic.Achievement.AchievementConditionBase",
|
||
"Logic.Achievement.TrainGiantCondition",
|
||
"Logic.Achievement.BuildWonderConditionCondition",
|
||
"Logic.Achievement.WonderInCityConditionCondition",
|
||
"Logic.Achievement.UnitOnWonderConditionCondition",
|
||
"Logic.Achievement.AroundBuildingsConditionCondition",
|
||
"Logic.Achievement.AroundWondersConditionCondition",
|
||
"Logic.Achievement.AroundEnemyUnitsConditionCondition",
|
||
"Logic.Achievement.AroundSelfUnitsConditionCondition",
|
||
"Logic.Achievement.AroundCityGridsConditionCondition",
|
||
};
|
||
|
||
private static TypeDefinition FindCecilType(ModuleDefinition module, string fullName)
|
||
{
|
||
return EnumerateCecilTypes(module.Types)
|
||
.FirstOrDefault(type => type.FullName == fullName);
|
||
}
|
||
|
||
private static IEnumerable<TypeDefinition> EnumerateCecilTypes(IEnumerable<TypeDefinition> types)
|
||
{
|
||
foreach (var type in types)
|
||
{
|
||
yield return type;
|
||
|
||
foreach (var nestedType in EnumerateCecilTypes(type.NestedTypes))
|
||
{
|
||
yield return nestedType;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static Type RequireType(string fullName)
|
||
{
|
||
var type = FindType(fullName);
|
||
if (type == null)
|
||
{
|
||
throw new InvalidOperationException($"Required type not found: {fullName}");
|
||
}
|
||
|
||
return type;
|
||
}
|
||
|
||
private static object CreateInstance(Type type)
|
||
{
|
||
var defaultCtor = type.GetConstructor(
|
||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||
null,
|
||
Type.EmptyTypes,
|
||
null);
|
||
if (defaultCtor != null)
|
||
{
|
||
return defaultCtor.Invoke(null);
|
||
}
|
||
|
||
var ctor = type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
|
||
.OrderBy(item => item.GetParameters().Length)
|
||
.FirstOrDefault();
|
||
if (ctor == null)
|
||
{
|
||
throw new MissingMethodException(type.FullName, ".ctor");
|
||
}
|
||
|
||
var parameters = ctor.GetParameters();
|
||
var args = parameters
|
||
.Select(parameter => parameter.HasDefaultValue ? parameter.DefaultValue : GetDefaultValue(parameter.ParameterType))
|
||
.ToArray();
|
||
return ctor.Invoke(args);
|
||
}
|
||
|
||
private static object GetDefaultValue(Type type)
|
||
{
|
||
return type.IsValueType ? Activator.CreateInstance(type) : null;
|
||
}
|
||
|
||
private static object GetOpsCompressionValue(Type buildSettingsType)
|
||
{
|
||
var memberType = GetMemberType(buildSettingsType, "Compression");
|
||
var method = typeof(EditorUserBuildSettings).GetMethod(
|
||
"GetCompressionType",
|
||
BindingFlags.NonPublic | BindingFlags.Static);
|
||
if (memberType == null || method == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var unityCompression = method.Invoke(null, new object[] { EditorUserBuildSettings.selectedBuildTargetGroup });
|
||
var value = Convert.ToInt32(unityCompression);
|
||
return Enum.ToObject(memberType, value);
|
||
}
|
||
|
||
private static Type GetMemberType(Type type, string memberName)
|
||
{
|
||
var property = type.GetProperty(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (property != null)
|
||
{
|
||
return property.PropertyType;
|
||
}
|
||
|
||
var field = type.GetField(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||
return field?.FieldType;
|
||
}
|
||
|
||
private static void SetMember(object instance, string memberName, object value)
|
||
{
|
||
if (instance == null) throw new ArgumentNullException(nameof(instance));
|
||
|
||
var type = instance.GetType();
|
||
var property = type.GetProperty(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (property != null)
|
||
{
|
||
var setter = property.GetSetMethod(true);
|
||
if (setter != null)
|
||
{
|
||
setter.Invoke(instance, new[] { value });
|
||
return;
|
||
}
|
||
}
|
||
|
||
var field = type.GetField(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (field != null)
|
||
{
|
||
field.SetValue(instance, value);
|
||
return;
|
||
}
|
||
|
||
throw new MissingMemberException(type.FullName, memberName);
|
||
}
|
||
|
||
private static bool TrySetMember(object instance, string memberName, object value)
|
||
{
|
||
try
|
||
{
|
||
SetMember(instance, memberName, value);
|
||
return true;
|
||
}
|
||
catch (MissingMemberException)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool CopyAotMetadataDlls()
|
||
{
|
||
var sourceDir = GetAotMetadataSourceDir(EditorUserBuildSettings.activeBuildTarget);
|
||
var destinationDir = GetStreamingAotMetadataDir();
|
||
var group = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
|
||
var requiresAotMetadata = PlayerSettings.GetScriptingBackend(group) == ScriptingImplementation.IL2CPP;
|
||
|
||
if (!Directory.Exists(sourceDir))
|
||
{
|
||
var message = $"[TH1.HybridCLR] AOT metadata source missing: {sourceDir}. Run HybridCLR Installer, then HybridCLR/Generate/All after an IL2CPP target is selected.";
|
||
if (requiresAotMetadata)
|
||
{
|
||
Debug.LogError(message);
|
||
return false;
|
||
}
|
||
|
||
Debug.LogWarning(message + " Current target uses Mono, so continue without AOT metadata.");
|
||
return true;
|
||
}
|
||
|
||
var copiedAll = true;
|
||
Directory.CreateDirectory(destinationDir);
|
||
foreach (var fileName in HotfixManifest.AotMetadataAssemblyFileNames)
|
||
{
|
||
var source = Path.Combine(sourceDir, fileName.Replace(".bytes", ""));
|
||
var destination = Path.Combine(destinationDir, fileName);
|
||
if (!File.Exists(source))
|
||
{
|
||
copiedAll = false;
|
||
var message = $"[TH1.HybridCLR] AOT metadata dll missing: {source}";
|
||
if (requiresAotMetadata)
|
||
{
|
||
Debug.LogError(message);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning(message + " Current target uses Mono, so continue without this AOT metadata file.");
|
||
}
|
||
continue;
|
||
}
|
||
|
||
File.Copy(source, destination, true);
|
||
Debug.Log($"[TH1.HybridCLR] Copied AOT metadata: {destination}");
|
||
}
|
||
|
||
return requiresAotMetadata ? copiedAll : true;
|
||
}
|
||
|
||
private static bool ExecuteHybridClrMenu(string menuPath)
|
||
{
|
||
if (EditorApplication.ExecuteMenuItem(menuPath))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
Debug.LogWarning($"[TH1.HybridCLR] Unity menu not found: {menuPath}. Make sure the HybridCLR package has been resolved.");
|
||
return false;
|
||
}
|
||
|
||
private static void EnsureHybridClrInstalled()
|
||
{
|
||
var installerType = FindType("HybridCLR.Editor.Installer.InstallerController");
|
||
if (installerType == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] InstallerController was not found. Make sure the HybridCLR package has been resolved.");
|
||
}
|
||
|
||
var controller = Activator.CreateInstance(installerType);
|
||
var method = installerType.GetMethod("HasInstalledHybridCLR", BindingFlags.Public | BindingFlags.Instance);
|
||
if (method == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] InstallerController.HasInstalledHybridCLR was not found.");
|
||
}
|
||
|
||
var installed = method.Invoke(controller, null) is bool value && value;
|
||
if (!installed)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] HybridCLR is not initialized. Open Tools/TH1/iOS Migration/HybridCLR/1. Run HybridCLR Installer, click Install in the installer window, then run Prepare Player Build again.");
|
||
}
|
||
}
|
||
|
||
private static void InvokeHybridClrGenerateAll()
|
||
{
|
||
var commandType = FindType("HybridCLR.Editor.Commands.PrebuildCommand");
|
||
if (commandType == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] PrebuildCommand was not found. Make sure the HybridCLR package has been resolved.");
|
||
}
|
||
|
||
var method = commandType.GetMethod("GenerateAll", BindingFlags.Public | BindingFlags.Static);
|
||
if (method == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] PrebuildCommand.GenerateAll was not found.");
|
||
}
|
||
|
||
try
|
||
{
|
||
method.Invoke(null, null);
|
||
}
|
||
catch (TargetInvocationException e)
|
||
{
|
||
var root = e.InnerException ?? e;
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All failed: {root.Message}\n{root}");
|
||
}
|
||
}
|
||
|
||
private static void EnsureAotMetadataSourceFiles(BuildTarget buildTarget)
|
||
{
|
||
var sourceDir = GetAotMetadataSourceDir(buildTarget);
|
||
if (!Directory.Exists(sourceDir))
|
||
{
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All did not create AOT metadata directory: {sourceDir}");
|
||
}
|
||
|
||
var missing = HotfixManifest.AotMetadataAssemblyFileNames
|
||
.Select(fileName => fileName.Replace(".bytes", ""))
|
||
.Where(fileName => !File.Exists(Path.Combine(sourceDir, fileName)))
|
||
.ToArray();
|
||
|
||
if (missing.Length > 0)
|
||
{
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All did not create required AOT metadata files under {sourceDir}: {string.Join(", ", missing)}");
|
||
}
|
||
}
|
||
|
||
private static bool CopyFile(string source, string destination, string label)
|
||
{
|
||
if (!File.Exists(source))
|
||
{
|
||
Debug.LogWarning($"[TH1.HybridCLR] {label} source missing: {source}");
|
||
return false;
|
||
}
|
||
|
||
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
||
File.Copy(source, destination, true);
|
||
Debug.Log($"[TH1.HybridCLR] Copied {label}: {destination}");
|
||
return true;
|
||
}
|
||
|
||
private static bool TryCompileHotfixDll(bool developmentBuild)
|
||
{
|
||
var compileCommandType = FindType("HybridCLR.Editor.Commands.CompileDllCommand");
|
||
if (compileCommandType == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] CompileDllCommand was not found. Make sure the HybridCLR package has been resolved.");
|
||
return false;
|
||
}
|
||
|
||
var compileMethod = compileCommandType.GetMethod(
|
||
"CompileDll",
|
||
BindingFlags.Public | BindingFlags.Static,
|
||
null,
|
||
new[] { typeof(BuildTarget), typeof(bool) },
|
||
null);
|
||
if (compileMethod == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] CompileDll(BuildTarget, bool) was not found.");
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
compileMethod.Invoke(null, new object[] { EditorUserBuildSettings.activeBuildTarget, developmentBuild });
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
var root = e is TargetInvocationException && e.InnerException != null ? e.InnerException : e;
|
||
Debug.LogError($"[TH1.HybridCLR] Compile hotfix dll failed:\n{root}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string FormatBytes(long length)
|
||
{
|
||
if (length >= 1024 * 1024) return $"{length / 1024f / 1024f:F2} MB";
|
||
if (length >= 1024) return $"{length / 1024f:F1} KB";
|
||
return $"{length} B";
|
||
}
|
||
|
||
private static string GetProjectPath(string relativePath)
|
||
{
|
||
var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath;
|
||
return Path.Combine(projectRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||
}
|
||
|
||
private static Type FindType(string fullName)
|
||
{
|
||
return AppDomain.CurrentDomain.GetAssemblies()
|
||
.Select(assembly => assembly.GetType(fullName, false))
|
||
.FirstOrDefault(type => type != null);
|
||
}
|
||
|
||
private static void SetBoolField(Type type, object instance, string fieldName, bool value)
|
||
{
|
||
type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance)?.SetValue(instance, value);
|
||
}
|
||
|
||
private static void AddStringToArrayField(Type type, object instance, string fieldName, string value)
|
||
{
|
||
var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
|
||
if (field == null) return;
|
||
|
||
var values = ((string[])field.GetValue(instance) ?? Array.Empty<string>()).ToList();
|
||
if (!values.Contains(value))
|
||
{
|
||
values.Add(value);
|
||
field.SetValue(instance, values.ToArray());
|
||
}
|
||
}
|
||
}
|
||
}
|