using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; using UnityEditor.Build.Player; //using Newtonsoft.Json; using System.Runtime.CompilerServices; using System.Linq; using UnityEditor.Build.Content; //using Base; #if UNITY_EDITOR using HybridCLR.Editor; using HybridCLR.Editor.Commands; #endif public class HybridCLRBuild { static string sPlatform = "pc"; static Dictionary m_mapResNameDic = new Dictionary(); static List maps = new List(); static List paths = new List(); static List files = new List(); static Dictionary m_dicFilelist = new Dictionary(); static string m_strListFileMD5; static string m_strDllFileMD5; static string m_strResFileMD5; static string m_strOldVersion = ""; static string m_aesPassword = "#@pOrhgf2026091211325890vciu%c-X"; #if ENV_INTRANET public static string UpdatePath = $"\\\\{GetLocal()}\\g\\App\\CDNRes\\public\\{BuildSettings.GameResVersion}\\"; #elif ENV_EXTRANET || ENV_PRODUCTION public static string UpdatePath = Application.dataPath + "/../../../CDNRes/public/"+ BuildSettings.GameResVersion + "/"; #else public static string UpdatePath = $"../XDevNode/Data/";//XWorld/ #endif public static string GetUpdatePath() { return UpdatePath; } static string AppDataPath { get { return Application.dataPath.ToLower(); }//Assets目录 } public static byte[] ChangeBytes(byte[] src, byte xor = 0xb4) { for (int i = 0; i < src.Length; i++) { src[i] = (byte)(src[i] ^ xor); } return src; } private static bool IsUnityAssetBundleData(byte[] data) { if (data == null || data.Length < 5) return false; return data[0] == (byte)'U' && data[1] == (byte)'n' && data[2] == (byte)'i' && data[3] == (byte)'t' && data[4] == (byte)'y'; } private static string GetDataHead(byte[] data) { if (data == null || data.Length == 0) return ""; int len = Math.Min(16, data.Length); StringBuilder sb = new StringBuilder(len * 3); for (int i = 0; i < len; i++) { byte b = data[i]; sb.Append(b >= 32 && b <= 126 ? (char)b : '.'); } sb.Append(" / "); for (int i = 0; i < len; i++) { if (i > 0) sb.Append(' '); sb.Append(data[i].ToString("X2")); } return sb.ToString(); } public static void ClearDir(string path, bool bDeleteSelf = false) { if (!Directory.Exists(path)) return; if (!bDeleteSelf) { System.IO.DirectoryInfo di = new DirectoryInfo(path); foreach (FileInfo file in di.EnumerateFiles()) { file.Delete(); } foreach (DirectoryInfo dir in di.EnumerateDirectories()) { dir.Delete(true); } } else { if (Directory.Exists(path)) { Directory.Delete(path, true); path = path + ".meta"; File.Delete(path); } } } static string packageVer = "G7"; //static URLConfig uconfig; public static string GetLocal() { //if (uconfig == null) //{ // string configPath = Application.streamingAssetsPath + "/config.json"; // string configJson = File.ReadAllText(configPath); // uconfig = JsonConvert.DeserializeObject(configJson); //} //if (uconfig != null) //{ // return uconfig?.local; //} return "127.0.0.1"; } [MenuItem("HybridCLR/测试IP", false, 2001)] public static void TestDebug() { Debug.Log($"更新地址:{GetUpdatePath()}"); } //需要导出的aot文件,运行的时候会全部更新 public static List AOTMetaAssemblyNames { get; } = new List() { "mscorlib.dll", "System.dll", "System.Core.dll", "ZString.dll",//解决泛型问题 "System.Runtime.CompilerServices.Unsafe.dll", "UnityEngine.AndroidJNIModule.dll" //"UnityEngine.UnityWebRequestModule.dll", }; [MenuItem("HybridCLR/GenAll(+xlua)", false, 2000)] public static void GenLuaAndHybridCLRAll() { //xlua wrap文件 CSObjectWrapEditor.Generator.GenAll(); //hybricCLR 生成 PrebuildCommand.GenerateAll(); //CSObjectWrapEditor.Generator.ClearAll(); } [MenuItem("HybridCLR/更新dll(PC)", false, 2001)] public static void BuildDllUpdatePC() { BuildHotFix(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64, "Client/Client.exe"); } [MenuItem("HybridCLR/更新dll(Android)", false, 2002)] public static void BuildDllUpdateAndroid() { BuildHotFix(BuildTargetGroup.Android, BuildTarget.Android); } //[MenuItem("HybridCLR/更新dll(iOS)", false, 2003)] //public static void BuildDllUpdateiOS() //{ // BuildHotFix(BuildTargetGroup.iOS, BuildTarget.iOS); //} [MenuItem("HybridCLR/更新dll(iOS)", false, 2003)] public static void BuildDllUpdateiOS() { BuildHotFix(BuildTargetGroup.iOS, BuildTarget.iOS); } [MenuItem("HybridCLR/更新dll(WebGL)", false, 2004)] public static void BuildDllUpdateWebGL() { BuildHotFix(BuildTargetGroup.WebGL, BuildTarget.WebGL); } [MenuItem("打包/生成底包/微信小游戏底包", false, 1030)] public static void BuildWX() {//彻底更新 //xlua wrap文件 //CSObjectWrapEditor.Generator.GenAll(); //hybricCLR 生成 //PrebuildCommand.GenerateAll(); //微信小游戏 Debug.Log($"BuildTarget : {EditorUserBuildSettings.activeBuildTarget}"); WeChatWASM.WXProjectConf config = WeChatWASM.WXConvertCore.config.ProjectConf; Debug.Log($"WXPConfig projectName : {config.projectName}"); Debug.Log($"WXPConfig Appid : {config.Appid}"); Debug.Log($"WXPConfig CDN : {config.CDN}"); Debug.Log($"WXPConfig assetLoadType : {config.assetLoadType}"); Debug.Log($"WXPConfig compressDataPackage : {config.compressDataPackage}"); Debug.Log($"WXPConfig VideoUrl : {config.VideoUrl}"); Debug.Log($"WXPConfig Orientation : {config.Orientation}"); WeChatWASM.CompileOptions options = WeChatWASM.WXConvertCore.config.CompileOptions; Debug.Log($"WXOp DevelopBuild : {options.DevelopBuild}"); Debug.Log($"WXOp AutoProfile : {options.AutoProfile}"); Debug.Log($"WXOp ScriptOnly : {options.ScriptOnly}"); Debug.Log($"WXOp Il2CppOptimizeSize : {options.Il2CppOptimizeSize}"); Debug.Log($"WXOp profilingFuncs : {options.profilingFuncs}"); Debug.Log($"WXOp ProfilingMemory : {options.ProfilingMemory}"); Debug.Log($"WXOp DeleteStreamingAssets : {options.DeleteStreamingAssets}"); Debug.Log($"WXOp CleanBuild : {options.CleanBuild}"); Debug.Log($"WXOp CustomNodePath : {options.CustomNodePath}"); Debug.Log($"WXOp Webgl2 : {options.Webgl2}"); Debug.Log($"WXOp fbslim : {options.fbslim}"); WeChatWASM.WXConvertCore.DoExport(); //生成底包版本号base.version string basever = md5file($"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}/webgl/Build/webgl.data"); basever = $"var ver=\"{basever}\""; File.WriteAllText($"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}/webgl/ver.js", basever); //复制底包内容到服务器上 string dst = GetUpdatePath();// "\\\\192.168.9.231\\g\\App\\G7\\"; dst = dst.Substring(0, dst.Length - 16); string[] args = Environment.GetCommandLineArgs(); string dstpath = GetCommandLineValue(args, "-dstpath"); if (dstpath != null && dstpath != "") { dst = dstpath; } bool bCopyHtml = false; if (!File.Exists($"{dst}index.html")) { bCopyHtml = true; } string src = $"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}\\webgl\\"; src = src.Replace("\\", "/"); //删除原文件 string[] names = Directory.GetFiles(dst); foreach (string file in names) { if (file.Contains("index.html")) continue; File.Delete(file); } //复制 names = Directory.GetFiles(src, "*.*", SearchOption.AllDirectories); foreach (string file in names) { if (file.Contains("StreamingAssets.unity3d") || (!bCopyHtml && file.Contains("index.html"))) continue; string dstfile = file.Replace(src, ""); dstfile = $"{dst}{dstfile}"; if (!Directory.Exists(Path.GetDirectoryName(dstfile))) Directory.CreateDirectory(Path.GetDirectoryName(dstfile)); if (File.Exists(dstfile)) File.Delete(dstfile); File.Copy(file, dstfile, true); } } public static void BuildHotFix(BuildTargetGroup targetGroup, BuildTarget target, string targetName = "", bool bUpdateDll = true) { //bool bLuaPackage = false; string[] EncryptFile = { "a.unity3d", "h.unity3d","xhotfix/base.unity3d", "StreamingAssets.unity3d", "bs.unity3d" };// if (EditorUserBuildSettings.activeBuildTarget != target) { EditorUserBuildSettings.SwitchActiveBuildTarget(targetGroup, target); } if (target == BuildTarget.Android) { sPlatform = "android"; } else if (target == BuildTarget.iOS) { sPlatform = "ios"; } else if (target == BuildTarget.WebGL) { sPlatform = "webgl"; //bLuaPackage = true; } else { sPlatform = "pc"; } maps.Clear(); //基础更新(大厅/基础资源)产物输出到仓库 CDN/XWorld//, //由 Server/Cdn.Runner 以 --root <仓库>/CDN 提供,客户端经 DevDiscovery 拿 CDN 根后从 <根>/XWorld// 增量下载。 //(CWD = Unity 工程根 Client/,故 ../CDN = 仓库根 CDN)。产物已是 md5 命名,匹配 XResLoader.LoadManifest("XWorld")。 UpdatePath = $"../CDN/XWorld/{sPlatform}/"; //if (bUpdateDll) //{//EncryptFile剔除其中的aot.unity,现在一般不更新aot.all,只打到底包 // EncryptFile = EncryptFile.Where(x => !x.Equals("aot.unity3d")).ToArray(); //} string outPath = $"{AppDataPath}/StreamingAssets/";//这个不要直接清 还有其他文件 string hotfixDir = $"{outPath}xhotfix"; string aotDir = $"{outPath}xaot"; ClearDir(hotfixDir); ClearDir(aotDir); if (!Directory.Exists(outPath)) Directory.CreateDirectory(outPath); if (!Directory.Exists(hotfixDir)) Directory.CreateDirectory(hotfixDir); if (!Directory.Exists(aotDir)) Directory.CreateDirectory(aotDir); foreach (var f in EncryptFile) {//unity需要删除旧文件,不然这个会不更新,还是用老的加密ab文件,后面就会重复加密。 string file = $"{AppDataPath}/StreamingAssets/{f}"; if (File.Exists(file)) { File.Delete(file); } file = $"{AppDataPath}/StreamingAssets/{f}.manifest"; if (File.Exists(file)) { File.Delete(file); } } //生成一次临时包,以便正确生成dll //生成xlua的wrap代码 CSObjectWrapEditor.Generator.GenAll(); //runtime 编译 ScriptCompilationSettings scriptCompilationSettings = default; scriptCompilationSettings.group = targetGroup; scriptCompilationSettings.target = target; PlayerSettings.stripEngineCode = true; PlayerSettings.allowUnsafeCode = true; string BasePath = System.Environment.CurrentDirectory; PlayerSettings.SetScriptingBackend(targetGroup, ScriptingImplementation.IL2CPP); ScriptCompilationResult scriptCompilationResult = PlayerBuildInterface.CompilePlayerScripts(scriptCompilationSettings, $"{BasePath}/client/"); //string[] SCENES = FindHotfixScenes(); //BuildPipeline.BuildPlayer(SCENES, targetName, target, BuildOptions.None); //打入更新的资源,dll string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target); if (!File.Exists($"{aotAssembliesSrcDir}/mscorlib.dll")) {//aot dll 检查 StripAOTDllCommand.GenerateStripedAOTDlls(); AssetDatabase.Refresh(); } //HybridCLR dll update BuildAssetsCommand.BuildAndCopyABAOTHotUpdateDlls(); AssetDatabase.Refresh(); //lua 打包 BuildLuaAB("bs.unity3d"); PackDllFile("h.unity3d", $"{AppDataPath}/StreamingAssets/xhotfix/"); //if (!bUpdateDll) //PackDllFile("a.unity3d", $"{AppDataPath}/StreamingAssets/xaot/"); //打包基础代码入口资源,这个为挂载GameLauncher脚本的预设 AddBuildMap($"xhotfix/base.unity3d", "*.*", $"Assets/base/Prefab"); AssetDatabase.Refresh(); //渲染shader ab //shader也跟下面的资源一起打吧 //PackAllPathFile("game_shader.unity3d", $"Assets/Game/Shader", "Shadervariants"); AddBuildMap("game_shader.unity3d", "*.*", "Assets/Game/Shader"); //资源 ab string[] HotfixRes = { "Game/Art" };//热更资源目录, 按目录打资源 string namePre = $"Game_Art"; foreach (string respath in HotfixRes) { string tempPath = $"{AppDataPath}/{respath}/"; string buildPath = $"Assets/{respath}/";//路径必须以Assets开始 string[] dirs = Directory.GetDirectories(tempPath, "*", SearchOption.AllDirectories); for (int i = 0; i < dirs.Length; i++) { string path = dirs[i]; if (path.Contains(".svn")) continue; string pathname = dirs[i].Replace(tempPath, string.Empty); string name = pathname.Replace('\\', '_').Replace('/', '_');//Lua ab文件使用.替换目录分隔符 name = name.ToLower() + ".unity3d"; AddBuildMap($"{namePre}_{name}", "*.*", buildPath + pathname); //AssetDatabase.Refresh(); } AddBuildMap($"{namePre}_{respath}.unity3d", "*.*", $"Assets/{respath}/"); //AssetDatabase.Refresh(); } //打ab包 BuildPipeline.BuildAssetBundles(outPath, maps.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression/*| BuildAssetBundleOptions.DisableWriteTypeTree*/, target); //AssetDatabase.Refresh(); //改名StreamingAssets,加个后缀 if (File.Exists($"{outPath}StreamingAssets")) { File.Move($"{outPath}StreamingAssets", $"{outPath}StreamingAssets.unity3d"); } //生成更新列表 //BuildFileIndex(sPlatform);//StreamingAssets 里的除了\\xaot\\ 和 \\xhotfix (dll目录)外所有文件 //添加dll更新列表 m_dicFilelist.Clear(); m_strDllFileMD5 = ""; m_strResFileMD5 = ""; //资源列表 BuildFileIndex("");//暂时不分平台 //dll列表 BuildDllsFileIndex("", bUpdateDll); SaveUpdateVer(""); string update_xaot_dir = $"{GetUpdatePath()}xaot"; string update_xhotfix_dir = $"{GetUpdatePath()}xhotfix"; string update_dllfiles = $"{GetUpdatePath()}dllfiles.txt"; string update_files = $"{GetUpdatePath()}files.txt"; string update_ver = $"{GetUpdatePath()}updatever.txt"; //复制到目标地址 //先清理之前的 if (Directory.Exists(GetUpdatePath())) { ClearDir(update_xaot_dir); ClearDir(update_xhotfix_dir); //string [] files = Directory.GetFiles(GetUpdatePath(), "*.*"); //for (int i = 0; i < files.Length; i++) //{ // if (files[i].Contains("basepack.bytes") || files[i].Contains("config.json")) // continue; // File.Delete(files[i]); //} } string OldVerString = ""; if (File.Exists(update_ver)) { OldVerString = File.ReadAllText(update_ver); } //CopyPath(outPath, GetUpdatePath()); //CopyPath($"{outPath}xaot", update_xaot_dir); CopyPath($"{outPath}xhotfix", update_xhotfix_dir);//这里面有个base.unity3d File.Copy($"{outPath}dllfiles.txt", update_dllfiles, true); File.Copy($"{outPath}files.txt", $"{GetUpdatePath()}files.txt", true); File.Copy($"{outPath}updatever.txt", update_ver, true); int copyCount = CopyUpdateFilesFromIndex(outPath, GetUpdatePath()); Debug.Log($"Copy update resource files: {copyCount}"); //复制资源 //加密 byte[] w = System.Text.Encoding.UTF8.GetBytes($"{m_aesPassword}"); AESManager.Set(ChangeBytes(w)); foreach (string f in EncryptFile) { if (!File.Exists($"{AppDataPath}/StreamingAssets/{f}")) continue; //if (!f.Contains("bs.unity3d")) //if (target != BuildTarget.WebGL) { byte[] srcdata = File.ReadAllBytes($"{AppDataPath}/StreamingAssets/{f}"); byte[] encryptdata = AESManager.Encrypt(srcdata); //byte[] retdata = AESManager.Decrypt(encryptdata); File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}", encryptdata); //File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}_Encrypt", encryptdata); } string file = $"{GetUpdatePath()}{f}"; if (File.Exists(file)) { File.Delete(file); } //File.Move($"{AppDataPath}/StreamingAssets/{f}_Encrypt", file); File.Copy($"{AppDataPath}/StreamingAssets/{f}", file, true); } //xlua处理 //if (target == BuildTarget.WebGL && bLuaPackage) //{ // File.Copy($"{outPath}bs.unity3d", $"{GetUpdatePath()}bs.unity3d", true); //} //以后修复 //GYPackager.BuildServerListMd5(target); Debug.Log($"Dest Path : {GetUpdatePath()}"); //if (target == BuildTarget.WebGL) {//给文件加上md5后缀,以便方便webgl小程序使用UnityWebRequest自动缓存 //先删除旧的 string finally_update_dllfiles; string finally_update_files; if (OldVerString != "") { string[] vers = OldVerString.Split(','); vers[0] = GetFileMd5Name($"{GetUpdatePath()}dllfiles.txt",vers[0]); vers[1] = GetFileMd5Name($"{GetUpdatePath()}files.txt",vers[1]); //foreach (var ver in vers) { //删除旧的更新文件 if (File.Exists(vers[0])) { string dllString = File.ReadAllText(vers[0]); foreach (string f in EncryptFile) { int pos = dllString.IndexOf(f); if (pos != -1) { string md5 = dllString.Substring(pos + f.Length + 1, 8); if (md5 != null && md5.Length == 8) { string ab = GetFileMd5Name($"{GetUpdatePath()}{f}", GetFixedMd5(md5)); if (File.Exists(ab)) File.Delete(ab); } } } //删除旧的dllfiles.txt文件 File.Delete(vers[0]); } if (File.Exists(vers[1])) { IEnumerable lines = File.ReadLines(vers[1]); foreach (var line in lines) { string[] items = line.Split(","); if (items.Length > 2) { string path = items[0].Substring(1);//路径去掉左边的‘{’符号 if (items[1] != null && items[1].Length == 8)//第二个是md5码 { string ab = GetFileMd5Name($"{GetUpdatePath()}{path}",GetFixedMd5(items[1])); if (File.Exists(ab)) File.Delete(ab); } } } //删除旧的dllfiles.txt文件 File.Delete(vers[1]); } } } foreach (var file in m_dicFilelist) { string destFile = GetFileMd5Name($"{GetUpdatePath()}{file.Key}", GetFixedMd5(file.Value)); string srcFile = $"{GetUpdatePath()}{file.Key}"; if (!File.Exists(srcFile)) continue; if (!File.Exists(destFile)) File.Move(srcFile, destFile); } //新的文件MD5, m_strDllFileMD5 finally_update_dllfiles = GetFileMd5Name($"{GetUpdatePath()}dllfiles.txt", GetFixedMd5(m_strDllFileMD5)); if(!File.Exists(finally_update_dllfiles)) File.Move(update_dllfiles, finally_update_dllfiles); finally_update_files = GetFileMd5Name($"{GetUpdatePath()}files.txt", GetFixedMd5(m_strResFileMD5)); if (!File.Exists(finally_update_files)) File.Move(update_files, finally_update_files); } //可靠产出最终 updatever.txt(明文):它是 XResLoader.LoadManifest("XWorld") 的入口文件,须与 //files_.txt / dllfiles_.txt 配套。中途的 File.Copy 在重定向到 CDN 后偶发不落盘, //故在所有改名之后用已算出的 md5 显式重写一遍,保证存在且写在最后不被后续步骤清掉。 if (!string.IsNullOrEmpty(m_strDllFileMD5) && !string.IsNullOrEmpty(m_strResFileMD5)) { Directory.CreateDirectory(GetUpdatePath()); File.WriteAllText($"{GetUpdatePath()}updatever.txt", $"{m_strDllFileMD5},{m_strResFileMD5}"); Debug.Log($"updatever.txt => {GetUpdatePath()}updatever.txt ({m_strDllFileMD5},{m_strResFileMD5})"); } else { Debug.LogError("updatever.txt 未生成:dll/res 清单 md5 为空(SaveUpdateVer 可能提前返回)"); } //清理outPath //。。。 Debug.Log($"构建完成{DateTime.Now.ToString()}"); //清理中间打包目录 string temppath = $"{AppDataPath}/a"; ClearDir(temppath, true); temppath = $"{AppDataPath}/h"; ClearDir(temppath, true); //Lua string luapath = $"{AppDataPath}/l"; ClearDir(luapath, true); //xlua相关 //CSObjectWrapEditor.Generator.ClearAll(); } static int CopyUpdateFilesFromIndex(string srcRoot, string dstRoot) { if (string.IsNullOrEmpty(srcRoot) || string.IsNullOrEmpty(dstRoot)) return 0; if (!srcRoot.EndsWith("/") && !srcRoot.EndsWith("\\")) srcRoot += "/"; if (!dstRoot.EndsWith("/") && !dstRoot.EndsWith("\\")) dstRoot += "/"; int count = 0; foreach (var item in m_dicFilelist) { string relativePath = item.Key; if (string.IsNullOrEmpty(relativePath)) continue; relativePath = relativePath.Replace('\\', '/'); if (relativePath.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) || relativePath.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase) || relativePath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) { continue; } string srcFile = Path.Combine(srcRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); if (!File.Exists(srcFile)) { Debug.LogWarning($"Update file listed but missing: {srcFile}"); continue; } string dstFile = Path.Combine(dstRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); string dstDir = Path.GetDirectoryName(dstFile); if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir); File.Copy(srcFile, dstFile, true); count++; } return count; } static void CopyPath(string srcPath, string outPath) { if (!Directory.Exists(outPath)) { Directory.CreateDirectory(outPath); } string[] luaPaths = { srcPath };//AppDataPath + "/../../Scripts_c/" }; for (int i = 0; i < luaPaths.Length; i++) { paths.Clear(); files.Clear(); string luaDataPath = luaPaths[i].ToLower(); Recursive(luaDataPath); int n = 0; foreach (string f in files) { if (f.EndsWith(".meta") || f.EndsWith(".manifest")) continue; string newfile = f.Replace(luaDataPath, ""); string newpath = outPath + newfile; string path = Path.GetDirectoryName(newpath); if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (File.Exists(newpath)) { File.Delete(newpath); } File.Copy(f, newpath, true); UpdateProgress(n++, files.Count, newpath); } } EditorUtility.ClearProgressBar(); } /// /// 遍历目录及其子目录 /// static void Recursive(string path) { if (path.Contains("\\xaot\\") || path.Contains("\\xhotfix\\") || path.Contains("/xaot/") || path.Contains("/xhotfix/")) {//忽略 return; } string[] names = Directory.GetFiles(path); string[] dirs = Directory.GetDirectories(path); foreach (string filename in names) { string ext = Path.GetExtension(filename); if (ext.Equals(".meta")) continue; files.Add(filename.Replace('\\', '/')); } foreach (string dir in dirs) { if (dir.IndexOf(".svn") >= 0) { continue; } paths.Add(dir.Replace('\\', '/')); Recursive(dir); } } private static string[] FindHotfixScenes() {//只导出基于热更的基础场景 List LauncherSceneNames = new List(); LauncherSceneNames.Insert(0, "Assets/Launcher/Launcher_Hotfix.unity"); return LauncherSceneNames.ToArray(); } static void AddBuildMap(string bundleName, string pattern, string path, bool bDirPack = true) { var files = Array.FindAll(Directory.GetFiles(path, pattern), x => !x.EndsWith(".meta")); if (files.Length == 0) return; for (int i = 0; i < files.Length; i++) { files[i] = files[i].Replace('\\', '/'); } if (bDirPack) { AssetBundleBuild build = new AssetBundleBuild(); build.assetBundleName = bundleName; build.assetNames = files; maps.Add(build); Debug.LogWarning($"AddBuildMap(dir) : {build.assetBundleName}({files.Length})"); } else { string[] singlefile; string abname = bundleName.Substring(0, bundleName.IndexOf(".unity3d")); abname += "_"; for (int i = 0; i < files.Length; i++) { AssetBundleBuild build = new AssetBundleBuild(); singlefile = new string[] { files[i] }; build.assetBundleName = abname + Path.GetFileNameWithoutExtension(files[i]).ToLower() + ".unity3d";// bundleName; build.assetNames = singlefile; maps.Add(build); Debug.LogWarning($"AddBuildMap : {build.assetBundleName}({files.Length})"); } } } public static bool PackAotFile(BuildTargetGroup targetGroup, BuildTarget target, string bundleName, string dllpath) { maps.Clear(); paths.Clear(); files.Clear(); if (target == BuildTarget.Android) { sPlatform = "android"; } else if (target == BuildTarget.iOS) { sPlatform = "ios"; } else if (target == BuildTarget.WebGL) { sPlatform = "webgl"; } else { sPlatform = "pc"; } UpdatePath = $"../CDN/XWorld/{sPlatform}/"; ScriptCompilationSettings scriptCompilationSettings = default; scriptCompilationSettings.group = targetGroup; scriptCompilationSettings.target = target; PlayerSettings.stripEngineCode = true; PlayerSettings.allowUnsafeCode = true; string BasePath = System.Environment.CurrentDirectory; PlayerSettings.SetScriptingBackend(targetGroup, ScriptingImplementation.IL2CPP); var inputPath = $"{BasePath}/client/"; var outputPath = $"{BasePath}/clientObfuz/"; //打aot不需要这个 //ScriptCompilationResult scriptCompilationResult = PlayerBuildInterface.CompilePlayerScripts(scriptCompilationSettings, inputPath); //AssetDatabase.Refresh(); string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target); if (!File.Exists($"{aotAssembliesSrcDir}/mscorlib.dll")) {//aot dll 检查 StripAOTDllCommand.GenerateStripedAOTDlls(); AssetDatabase.Refresh(); } HybridCLR.Editor.BuildAssetsCommand.CopyAOTAssembliesToStreamingAssets(); AssetDatabase.Refresh(); PackDllFile(bundleName, dllpath); string outPath = $"{AppDataPath}/StreamingAssets/"; string bundlePath = $"{outPath}{bundleName}"; if (File.Exists(bundlePath)) File.Delete(bundlePath); if (File.Exists($"{bundlePath}.manifest")) File.Delete($"{bundlePath}.manifest"); var manifest = BuildPipeline.BuildAssetBundles(outPath, maps.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression/*| BuildAssetBundleOptions.DisableWriteTypeTree*/, target); if (manifest == null) { Debug.LogError("BuildAssetBundles 返回 null!原因通常是:1.资源重复打包 2.资源路径不存在 3.有残留的.meta错误。请查看控制台前一条报错。"); return false; } AssetDatabase.Refresh(); //string outBundleName = $"{outPath}/{bundlePath}"; if (!File.Exists(bundlePath)) { Debug.LogError($"BuildAssetBundles 失败!{bundlePath} 不存在。请查看控制台前一条报错。"); return false; } Debug.Log($"BuildAssetBundles {bundlePath}成功"); //encrypt string f = "a.unity3d"; byte[] w = System.Text.Encoding.UTF8.GetBytes($"{m_aesPassword}"); AESManager.Set(ChangeBytes(w)); byte[] srcdata = File.ReadAllBytes($"{AppDataPath}/StreamingAssets/{f}"); if (!IsUnityAssetBundleData(srcdata)) { Debug.LogError($"{f} before encrypt is not a Unity AssetBundle. head:{GetDataHead(srcdata)}"); return false; } byte[] encryptdata = AESManager.Encrypt(srcdata); byte[] retdata = AESManager.Decrypt(encryptdata); if (!IsUnityAssetBundleData(retdata)) { Debug.LogError($"{f} encrypt/decrypt check failed. head:{GetDataHead(retdata)}"); return false; } File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}", encryptdata); //清理临时目录 string temppath = $"{AppDataPath}/aot"; if (Directory.Exists(temppath)) { Directory.Delete(temppath, true); temppath = temppath + ".meta"; File.Delete(temppath); } temppath = $"{AppDataPath}/StreamingAssets/res"; if (Directory.Exists(temppath)) { Directory.Delete(temppath, true); temppath = temppath + ".meta"; File.Delete(temppath); } temppath = $"{AppDataPath}/StreamingAssets/xaot"; if (Directory.Exists(temppath)) { Directory.Delete(temppath, true); temppath = temppath + ".meta"; File.Delete(temppath); } temppath = $"{AppDataPath}/hotfix"; if (Directory.Exists(temppath)) { Directory.Delete(temppath, true); temppath = temppath + ".meta"; File.Delete(temppath); } SaveMd5UpdateFiles(); return true; } static void SaveMd5UpdateFiles() { //处理生成带md5文件 Debug.Log($"处理生成带md5文件"); string dllmd5 = File.ReadAllText($"{GetUpdatePath()}updatever.txt"); if (dllmd5 != m_strOldVersion) { Debug.Log($"处理生成带md5文件有更新"); } else { Debug.Log($"处理生成带md5文件没有有更新"); } {//有改变 string new_dllfiles = $"{GetUpdatePath()}dllfiles_{dllmd5}.txt"; File.Copy($"{AppDataPath}/StreamingAssets/dllfiles.txt", $"{GetUpdatePath()}dllfiles_{dllmd5}.txt", true); string[] dllString = File.ReadAllLines(new_dllfiles); //检查原version文件,删除旧的dllfiles.txt_md5文件和加密的dll文件 if (m_strOldVersion.Length >= 8) { string old_dllfiles = $"{GetUpdatePath()}dllfiles_{m_strOldVersion}.txt"; if (File.Exists(old_dllfiles)) { //查找原来的文件,删除 string[] dlls = File.ReadAllLines(old_dllfiles); //比较dlls和dllString的不同,删除旧的 var filesToDelete = dlls.Except(dllString); foreach (string f in filesToDelete) { string trimmedString = f.Trim('{', '}'); string[] line = trimmedString.Split(','); string fn = GetFileMd5Name(line[0], line[1]); string file = $"{GetUpdatePath()}{fn}"; if (File.Exists(file)) { File.Delete(file); } } File.Delete(old_dllfiles); } } //生成新的带md5文件 foreach (string f in dllString) { string trimmedString = f.Trim('{', '}'); string[] line = trimmedString.Split(','); string fn = GetFileMd5Name(line[0], line[1]); string srcfile = $"{GetUpdatePath()}{line[0]}"; string destfile = $"{GetUpdatePath()}{fn}"; if (!File.Exists(destfile)) { File.Copy(srcfile, destfile, true); } } } /*/生成版本文件(代码md5,YooAssets版本号,资源版本号) string updateShowVer = File.ReadAllText($"{GetUpdatePath()}updateShowVer.txt"); string serverinfo_file = $"{GetUpdatePath()}serverinfo.txt"; ServerConfig serverConfig = new ServerConfig(); if (BuildSettings.GameEnvironment == EBaseGameEnv.Local) { serverConfig.cdn = "http://192.168.130.30/cdn/"; serverConfig.resver = BuildSettings.GameResVersion; //"10000101";//先测试用101 serverConfig.resver_update = updateShowVer;//updateShowVer.txt serverConfig.codemd5 = dllmd5;//updatever.txt serverConfig.yooasset_ver = $"{yoopack2md5},{yoopack3md5}";//PatchManifest_Pack2.version 和PatchManifest_Pack3.version serverConfig.login = "172.25.50.43:3001"; serverConfig.login_backup = "172.25.50.43:3001"; serverConfig.notice = "http://192.168.130.30/cdn/"; serverConfig.date = "2025082610220612"; serverConfig.gateway = new string[]{ "114.207.53.71", "114.207.53.72", }; } else { serverConfig.cdn = "http://fish.funturegame.com:8081/download/cdn/"; serverConfig.resver = BuildSettings.GameResVersion;// "10000000"; serverConfig.resver_update = updateShowVer;//updateShowVer.txt serverConfig.codemd5 = dllmd5;//updatever.txt serverConfig.yooasset_ver = $"{yoopack2md5},{yoopack3md5}";//PatchManifest_Pack2.version 和PatchManifest_Pack3.version serverConfig.login = "fish.funturegame.com:3001"; serverConfig.login_backup = "114.207.53.82:3001"; serverConfig.notice = "https://version_t1.funturio.com/"; serverConfig.date = "2025082610220612"; serverConfig.gateway = new string[]{ "114.207.53.71", "114.207.53.72", }; serverConfig.ext = new Dictionary(){ {"xconsole","100"}, {"logserver","http://fish.funturegame.com:8089/api/v1/logs/batch"} }; } string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(serverConfig); File.WriteAllText(serverinfo_file, jsonString); //*/ } static void PackDllFile(string bundleName, string dllpath) { //string resPath = $"{AppDataPath}/StreamingAssets/xhotfix/"; List files = new List(); files.AddRange(Directory.GetFiles(dllpath)); //files.AddRange(Directory.GetFiles($"{AppDataPath}/StreamingAssets/xaot/")); //int nPos = resPath.IndexOf("assets/StreamingAssets/", StringComparison.OrdinalIgnoreCase); int nPos = bundleName.IndexOf('.'); //加密 List DllFiles = new List(); string dirname = bundleName.Substring(0, nPos); string temppath = $"{AppDataPath}/{dirname}"; if (!Directory.Exists(temppath)) { Directory.CreateDirectory(temppath); } else { ClearDir(temppath); } //byte[] buffer, aesdata; foreach (var file in files) { if (file.EndsWith(".dll")) { //buffer = File.ReadAllBytes(file); //aesdata = buffer;// AESManager.Encrypt(buffer); string name = Path.GetFileNameWithoutExtension(file);//file.Replace(".dll", ".bytes"); string filepath = $"{temppath}/{name}.bytes"; //File.WriteAllBytes(filepath, aesdata); File.Move(file, filepath); name = $"assets/{dirname}/{name}.bytes"; DllFiles.Add(name); } } AssetBundleBuild build = new AssetBundleBuild(); build.assetBundleName = bundleName; build.assetNames = DllFiles.ToArray();// filelist; maps.Add(build); } static void BuildLuaAB(string bundleName) { string luapath = $"{Application.dataPath}/l/"; if(Directory.Exists(luapath)) ClearDir(luapath); //Assets下的lua目录 CopyScriptsFile($"{Application.dataPath}/lua/", luapath, "bytes"); AssetDatabase.Refresh();//新增文件需要刷新 var files = Directory.GetFiles($"Assets/l", "*.bytes", SearchOption.AllDirectories); for (int i = 0; i < files.Length; i++) { files[i] = files[i].Replace('\\', '/'); } AssetBundleBuild build = new AssetBundleBuild(); build.assetBundleName = bundleName; build.assetNames = files; maps.Add(build); } static void CopyScriptsFile(string srcPath, string outPath, string fixname) { if (!Directory.Exists(outPath)) { Directory.CreateDirectory(outPath); } string[] luaPaths = { srcPath };//AppDataPath + "/../../Scripts_c/" }; for (int i = 0; i < luaPaths.Length; i++) { paths.Clear(); files.Clear(); string luaDataPath = luaPaths[i].ToLower(); Recursive(luaDataPath); int n = 0; foreach (string f in files) { if (f.EndsWith(".meta")) continue; string newfile = f.Replace(luaDataPath, ""); string newpath; if (newfile.IndexOf(".lua") < 0)//转2进制文件 { newpath = outPath + newfile + ".bytes"; } else { newpath = outPath + newfile.Replace(".lua", "." + fixname); } newpath = newpath.ToLower(); string path = Path.GetDirectoryName(newpath); if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (File.Exists(newpath)) { File.Delete(newpath); } File.Copy(f, newpath, true); UpdateProgress(n++, files.Count, newpath); } } EditorUtility.ClearProgressBar(); } static void UpdateProgress(int progress, int progressMax, string desc) { string title = "Processing...[" + progress + " - " + progressMax + "]"; float value = (float)progress / (float)progressMax; EditorUtility.DisplayProgressBar(title, desc, value); } static void BuildDllsFileIndex(string dir = "", bool bUpdateDll = true) { List files = new List(); string resPath = $"{AppDataPath}/StreamingAssets/{dir}/"; ///----------------------创建文件列表----------------------- string newFilePath = resPath + "dllfiles.txt"; if (File.Exists(newFilePath)) File.Delete(newFilePath); if (!Directory.Exists($"{resPath}xaot/")) Directory.CreateDirectory($"{resPath}xaot/"); if (!Directory.Exists($"{resPath}xhotfix/")) Directory.CreateDirectory($"{resPath}xhotfix/"); //files.AddRange(Directory.GetFiles($"{resPath}xaot/")); files.AddRange(Directory.GetFiles($"{resPath}xhotfix/")); FileStream fs = new FileStream(newFilePath, FileMode.CreateNew); StreamWriter sw = new StreamWriter(fs); foreach (var dllfile in files) { if (!dllfile.EndsWith(".dll") && !dllfile.EndsWith(".unity3d")) continue; long size = 0; string md5 = GetFixedMd5(md5file(dllfile, out size)); string value = dllfile.Replace(resPath, string.Empty); sw.WriteLine($"{{{value},{md5},{size}}}"); m_dicFilelist[value] = md5; } //记录基础 ab string[] BaseABFile = { "a.unity3d", "h.unity3d"};//, "bs.unity3d", "StreamingAssets" foreach (var f in BaseABFile) { if (f == "a.unity3d" && bUpdateDll) continue; string file = $"{AppDataPath}/StreamingAssets/{f}"; if (File.Exists(file)) { long size = 0; string md5 = GetFixedMd5(md5file(file, out size)); string value = file.Replace($"{AppDataPath}/StreamingAssets/", string.Empty); sw.WriteLine($"{{{value},{md5},{size}}}"); m_dicFilelist[value] = md5; } } sw.Close(); fs.Close(); } static void BuildFileIndex(string dir = "") { string resPath = $"{AppDataPath}/StreamingAssets/{dir}/"; ///----------------------创建文件列表----------------------- string newFilePath = resPath + "files.txt"; if (File.Exists(newFilePath)) File.Delete(newFilePath); paths.Clear(); files.Clear(); Recursive(resPath);//此处去掉了dll热更相关的目录 FileStream fs = new FileStream(newFilePath, FileMode.CreateNew); StreamWriter sw = new StreamWriter(fs); for (int i = 0; i < files.Count; i++) { string file = files[i]; if (file.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) continue; string ext = Path.GetExtension(file); if (file.EndsWith(".meta") || file.Contains(".DS_Store") || file.EndsWith(".manifest")) continue; long size = 0; string md5 = GetFixedMd5(md5file(file, out size)); string value = file.Replace(resPath, string.Empty); //if (!value.Contains("/") && value != "StreamingAssets.unity3d" && value != "bs.unity3d")//去除根路径文件 if(value == "h.unity3d")//剔除热更 continue; sw.WriteLine($"{{{value},{md5},{size}}}"); m_dicFilelist[value] = md5; } sw.Close(); fs.Close(); //版本文件ver.txt //string vermd5 = md5file(newFilePath); //File.WriteAllText($"{resPath}/updatever.txt", vermd5); } static void SaveUpdateVer(string dir = "") { string resPath = $"{AppDataPath}/StreamingAssets/{dir}/"; string resfilelist = $"{resPath}files.txt"; string dllfilelist = $"{resPath}dllfiles.txt"; if (!File.Exists(dllfilelist) || !File.Exists(resfilelist)) { UnityEngine.Debug.LogError("需要先生成更新列表文件"); return;//需要先 } string resmd5 = md5file(resfilelist); string dllmd5 = md5file(dllfilelist); m_strResFileMD5 = GetFixedMd5(resmd5); m_strDllFileMD5 = GetFixedMd5(dllmd5); File.WriteAllText($"{resPath}/updatever.txt", $"{m_strDllFileMD5},{m_strResFileMD5}"); } public static string md5file(string file) { long size = 0; return md5file(file, out size); } public static string md5file(string file, out long size) { try { FileStream fs = new FileStream(file, FileMode.Open); System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); byte[] retVal = md5.ComputeHash(fs); size = fs.Length; fs.Close(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < retVal.Length; i++) { sb.Append(retVal[i].ToString("x2")); } return sb.ToString(); } catch (Exception ex) { throw new Exception("md5file() fail, error:" + ex.Message); } } public static string GetPlatformString(BuildTarget target) { string sPlatform; if (target == BuildTarget.Android) { sPlatform = "android"; } else if (target == BuildTarget.iOS) { sPlatform = "ios"; } else if (target == BuildTarget.WebGL) { sPlatform = "webgl"; } else { sPlatform = "pc"; } return sPlatform; } private static string GetVersionResDir(BuildTarget target) { string dir; dir = Application.dataPath + $"/../../../CDNRes/public/G7/{GetPlatformString(target)}/"; //if (BuildSettings.GameEnvironment == EGameEnvType.Intranet) //{ // dir = $"\\\\{GetLocal()}\\g\\App\\CDNRes\\public\\{BuildSettings.GameResVersion}\\{GetPlatformString(target)}\\"; //} //else //{ // dir = Application.dataPath + $"/../../../CDNRes/public/{BuildSettings.GameResVersion}/{GetPlatformString(target)}/"; //} return dir; } public static void BuildServerListMd5(BuildTarget target, string packageName = "", string OutputPackageDir = "") { string dir = GetVersionResDir(target); if (!Directory.Exists(dir)) { Debug.LogError($"目录不存在:{dir}"); return; } StringBuilder sb = new StringBuilder(); //将三个包的Hash for (int i = 1; i <= 3; i++) { string packName = $"Pack{i}"; string currentDir = dir; if (packName == packageName) { currentDir = OutputPackageDir; } string verFile = $"{currentDir}PatchManifest_{packName}.version"; if (File.Exists(verFile)) { string verName = File.ReadAllText(verFile); string hashFile = $"{currentDir}PatchManifest_{packName}_{verName}.hash"; sb.AppendLine(File.ReadAllText(hashFile)); } } string package = $"{dir}{packageVer}"; if (File.Exists(package)) { sb.AppendLine(File.ReadAllText(package)); } string updatever = $"{dir}updatever.txt"; if (File.Exists(updatever)) { sb.AppendLine(File.ReadAllText(updatever)); } Debug.Log($"源hash:\n{sb}"); string md5 = Packager.md5(sb.ToString()); string md5File = $"{dir}server_res_list_md5.txt"; if (File.Exists(md5File)) { File.Delete(md5File); } byte[] bytes = Encoding.UTF8.GetBytes(md5); using (FileStream fs = File.Create(md5File)) { fs.Write(bytes, 0, bytes.Length); fs.Flush(); fs.Close(); } Debug.Log($"md5生成成功:{md5} {md5File}"); } static void PackAllPathFile(string bundleName, string path, string nopath = null) { paths.Clear(); files.Clear(); Recursive(path); string[] filelist = files.ToArray(); List total = new List(filelist); foreach (var file in filelist) { if (!m_mapResNameDic.ContainsKey(file)) { m_mapResNameDic[file] = "1"; } } //依赖文件 foreach (var file in filelist) { List depends = GetAllDependencies(file); foreach (var fd in depends) { if (fd.EndsWith(".hlsl")) continue; if (!m_mapResNameDic.ContainsKey(fd)) { m_mapResNameDic[fd] = "1"; total.Add(fd); } } } if (nopath != null) { for (int i = total.Count - 1; i >= 0; i--) { var file = total[i]; if (file.Contains(nopath)) { string[] pathParts = file.Replace('\\', '/').Split('/'); foreach (var p in pathParts) { if (string.Equals(p, nopath, StringComparison.OrdinalIgnoreCase))//if (p == nopath) { //删除不合理的 total.RemoveAt(i); break; } } } } } AssetBundleBuild build = new AssetBundleBuild(); build.assetBundleName = bundleName; build.assetNames = total.ToArray();// filelist; maps.Add(build); UnityEngine.Debug.Log($"ab:{bundleName} has {filelist.Length} file"); } static private List GetAllDependencies(string mainAssetPath) { List result = new List(); string[] depends = AssetDatabase.GetDependencies(mainAssetPath, true); foreach (string assetPath in depends) { // 注意:排除主资源对象 if (assetPath != mainAssetPath) result.Add(assetPath); } return result; } static string GetFixedMd5(string md5) { return md5.Substring(0, 8);//只取8位 } static string GetFileMd5Name(string filename, string md5) { int pos = filename.LastIndexOf('.'); if (pos != -1) { return $"{filename.Substring(0, pos)}_{md5}{filename.Substring(pos)}"; } else return $"{filename}_{md5}"; } public static string GetCommandLineValue(string[] array, string key) { foreach (var item in array) { if (item.Contains(key)) { string value = item.Split('=')[1]; return value; } } return ""; } }