TH1/Unity/Assets/Scripts/TH1_Logic/Editor/HybridCLR/TH1HybridCLRBuildTools.cs
2026-06-12 23:35:09 +08:00

875 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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());
}
}
}
}