306 lines
14 KiB
C#
306 lines
14 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// 导出微信小游戏基础包:只含 Boot 壳。首包资源走 CDN,玩法与美术仍由 YooAsset 热更。
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Allow importing either the export container or its minigame directory.</summary>
|
|
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
|