#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using HybridCLR.Editor.Installer; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using WeChatWASM; namespace AOT.Editor.WeChat { /// /// 导出微信小游戏基础包:只含 Boot 壳。首包资源走 CDN,玩法与美术仍由 YooAsset 热更。 /// public static class ExportWeChatBasePackage { const string BootScene = "Assets/Scenes/Boot.unity"; const string RelativeDst = "Build/WeChat/Base"; const string DataCdn = "https://www.xworld.ren/game/Friends/webgl/"; const int ShellHeapMb = 496; const string ResumeKey = "AIGame.WeChat.ExportBase.Resume"; [MenuItem("AIGame/WeChat/Export Base Package")] public static string Export() { if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.WebGL, BuildTarget.WebGL)) return Fail("未安装 Unity WebGL 模块"); var installer = new InstallerController(); if (!installer.HasInstalledHybridCLR()) return Fail("HybridCLR 未初始化,先执行菜单 HybridCLR/Installer"); if (installer.PackageVersion != installer.InstalledLibil2cppVersion) return Fail("HybridCLR 安装版本与包版本不一致,先重新执行 HybridCLR/Installer"); string sceneError = ValidateBootOnly(); if (sceneError != null) return Fail(sceneError); string configError = ApplyBaseConfig(); if (configError != null) return Fail(configError); if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL) { SessionState.SetBool(ResumeKey, true); bool switched = EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.WebGL, BuildTarget.WebGL); if (!switched) { SessionState.EraseBool(ResumeKey); return Fail("无法切换到 WebGL"); } Debug.Log("[AIGame] 正在切换到 WebGL,域重载后继续导出基础包"); return "ok switching target=WebGL"; } return ExportCore(); } [InitializeOnLoadMethod] static void ResumeAfterTargetSwitch() { EditorApplication.delayCall += ResumeIfRequested; } static void ResumeIfRequested() { if (!SessionState.GetBool(ResumeKey, false)) return; if (EditorApplication.isCompiling || EditorApplication.isUpdating) { EditorApplication.delayCall += ResumeIfRequested; return; } if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL) { SessionState.EraseBool(ResumeKey); Debug.LogError("[AIGame] fail: 切换 WebGL 后目标不是 WebGL"); return; } SessionState.EraseBool(ResumeKey); ExportCore(); } static string ExportCore() { string configError = ApplyBaseConfig(); if (configError != null) return Fail(configError); var error = WXConvertCore.DoExport(true); if (error != WXConvertCore.WXExportError.SUCCEED) return Fail("微信转换失败 " + error + ",详见 Console"); string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst)); string projectError = PrepareDevToolsProject(dst); if (projectError != null) return Fail(projectError); string summary = WriteReport(dst); string minigame = Path.Combine(dst, "minigame"); string result = "ok minigame=" + minigame + " " + summary; Debug.Log("[AIGame] " + result); return result; } [MenuItem("AIGame/WeChat/Repair Exported DevTools Project")] public static string RepairExportedProject() { string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst)); string error = PrepareDevToolsProject(dst); if (error != null) return Fail(error); string result = "ok devtools=" + dst + " source=minigame/ " + WriteReport(dst); Debug.Log("[AIGame] " + result); return result; } [MenuItem("AIGame/WeChat/Refresh Exported Config (No Build)")] public static string RefreshExportedConfig() { string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst)); string gamePath = Path.Combine(dst, "minigame", "game.js"); if (!File.Exists(gamePath)) return Fail("尚未导出小游戏,请先执行 Export Base Package"); string game = File.ReadAllText(gamePath); var cdnPattern = new Regex(@"\bDATA_CDN\s*:\s*(['""])[^'""]*\1"); if (cdnPattern.Matches(game).Count != 1) return Fail("无法识别导出 game.js 的 DATA_CDN,未修改配置"); string error = ApplyBaseConfig(); if (error != null) return Fail(error); error = PrepareDevToolsProject(dst); if (error != null) return Fail(error); string updated = cdnPattern.Replace(game, _ => "DATA_CDN: " + JsonConvert.SerializeObject(DataCdn), 1); if (updated != game) File.WriteAllText(gamePath, updated, new UTF8Encoding(false)); string result = "ok config refreshed (no WebGL build) " + WriteReport(dst); Debug.Log("[AIGame] " + result); return result; } [MenuItem("AIGame/WeChat/Open Exported MiniGame Folder")] public static void OpenExportedMiniGameFolder() { string game = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst, "minigame", "game.json")); if (!File.Exists(game)) { Fail("尚未导出小游戏,请先执行 Export Base Package"); return; } EditorUtility.RevealInFinder(game); } /// Allow importing either the export container or its minigame directory. public static string PrepareDevToolsProject(string dst) { string mini = Path.Combine(dst, "minigame"); foreach (string required in new[] { "game.json", "game.js", "project.config.json" }) if (!File.Exists(Path.Combine(mini, required))) return "小游戏导出不完整,缺少 " + Path.Combine(mini, required); try { JObject.Parse(File.ReadAllText(Path.Combine(mini, "game.json"))); string miniConfigPath = Path.Combine(mini, "project.config.json"); string rootConfigPath = Path.Combine(dst, "project.config.json"); JObject miniConfig = JObject.Parse(File.ReadAllText(miniConfigPath)); JObject rootConfig = File.Exists(rootConfigPath) ? JObject.Parse(File.ReadAllText(rootConfigPath)) : new JObject(); string appid = (string)miniConfig["appid"]; if (string.IsNullOrWhiteSpace(appid)) appid = (string)rootConfig["appid"]; // Carry SDK compiler options and symbol exclusions to the parent entry. // Keep other existing user fields and combine package ignore rules. rootConfig.Merge(miniConfig, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Union }); rootConfig["compileType"] = "game"; rootConfig["miniprogramRoot"] = "minigame/"; if (!string.IsNullOrWhiteSpace(appid)) { rootConfig["appid"] = appid; if (string.IsNullOrWhiteSpace((string)miniConfig["appid"])) { miniConfig["appid"] = appid; File.WriteAllText(miniConfigPath, miniConfig.ToString(Formatting.Indented), new UTF8Encoding(false)); } } string rootJson = rootConfig.ToString(Formatting.Indented); if (!File.Exists(rootConfigPath) || File.ReadAllText(rootConfigPath) != rootJson) File.WriteAllText(rootConfigPath, rootJson, new UTF8Encoding(false)); return null; } catch (Exception exception) { return "小游戏项目配置失败: " + exception.Message; } } static string ValidateBootOnly() { if (!File.Exists(BootScene)) return "缺少场景 " + BootScene; var enabled = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path).ToArray(); if (enabled.Length != 1 || enabled[0] != BootScene) return "基础包只能包含 " + BootScene + ",当前启用场景: " + string.Join(", ", enabled); return null; } static string ApplyBaseConfig() { var config = WXConvertCore.config; if (config == null) return "找不到 MiniGameConfig"; string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst)); var so = new SerializedObject(config); var project = so.FindProperty("ProjectConf"); var compile = so.FindProperty("CompileOptions"); var sdk = so.FindProperty("SDKOptions"); if (project == null || compile == null || sdk == null) return "MiniGameConfig 字段不完整"; project.FindPropertyRelative("projectName").stringValue = PlayerSettings.productName; project.FindPropertyRelative("relativeDST").stringValue = RelativeDst; project.FindPropertyRelative("DST").stringValue = dst; project.FindPropertyRelative("CDN").stringValue = DataCdn; project.FindPropertyRelative("assetLoadType").intValue = 0; project.FindPropertyRelative("Orientation").enumValueIndex = 0; project.FindPropertyRelative("MemorySize").intValue = ShellHeapMb; project.FindPropertyRelative("compressDataPackage").boolValue = false; compile.FindPropertyRelative("DevelopBuild").boolValue = false; compile.FindPropertyRelative("Il2CppOptimizeSize").boolValue = false; compile.FindPropertyRelative("Webgl2").boolValue = true; compile.FindPropertyRelative("fbslim").boolValue = true; compile.FindPropertyRelative("DeleteStreamingAssets").boolValue = false; sdk.FindPropertyRelative("UseFriendRelation").boolValue = false; so.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(config); AssetDatabase.SaveAssets(); if (PlayerSettings.defaultInterfaceOrientation != UIOrientation.Portrait) { PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait; AssetDatabase.SaveAssets(); } return null; } static string WriteReport(string dst) { string mini = Path.Combine(dst, "minigame"); string data = Path.Combine(dst, "webgl", "Build", "webgl.data"); long miniBytes = Directory.Exists(mini) ? DirectorySize(mini) : 0; long dataBytes = File.Exists(data) ? new FileInfo(data).Length : 0; var text = new StringBuilder(); text.AppendLine("AIGame WeChat base package"); text.AppendLine("scenes: " + BootScene); text.AppendLine("dataCdn: " + DataCdn); text.AppendLine("unityHeapMB: " + ShellHeapMb); text.AppendLine("minigameBytes: " + miniBytes); text.AppendLine("dataBytes: " + dataBytes); text.AppendLine("devtools: " + mini); text.AppendLine("devtoolsContainer: " + dst + " (miniprogramRoot=minigame/)"); text.AppendLine("uploadSource: " + Path.Combine(dst, "webgl")); text.AppendLine("uploadContentsTo: " + DataCdn); text.AppendLine("uploadNote: copy the CONTENTS of webgl, preserving relative paths; export does not upload to the server."); string webgl = Path.Combine(dst, "webgl"); if (Directory.Exists(webgl)) foreach (string resource in Directory.GetFiles(webgl, "*.webgl.data.unityweb.bin.*")) text.AppendLine("startupData: " + Path.GetFileName(resource) + " -> " + DataCdn + Path.GetFileName(resource)); if (Directory.Exists(mini)) { text.AppendLine("largest:"); foreach (var file in new DirectoryInfo(mini).GetFiles("*", SearchOption.AllDirectories) .OrderByDescending(item => item.Length) .Take(12)) { text.AppendLine(file.Length + "\t" + file.FullName.Substring(mini.Length).TrimStart('\\', '/')); } } Directory.CreateDirectory(dst); string report = Path.Combine(dst, "base-package-report.txt"); File.WriteAllText(report, text.ToString()); return "minigameBytes=" + miniBytes + " dataBytes=" + dataBytes + " report=" + report; } static long DirectorySize(string path) { long total = 0; foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) total += new FileInfo(file).Length; return total; } static string Fail(string reason) { Debug.LogError("[AIGame] " + reason); return "fail: " + reason; } } } #endif