2506 lines
96 KiB
C#
2506 lines
96 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Globalization;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using System.Diagnostics;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using System.Linq;
|
||
|
||
public class XWorldUtil
|
||
{
|
||
private const string MAIN_SCENE = "Assets/Scenes/Main.unity";
|
||
private const string UI_PREFAB_DIR = "Assets/Game/Art/UI/Prefab";
|
||
private const string DEFAULT_HTML_DIR = "Assets/Doc/Output";
|
||
private const string AUTO_CONVERT_JSON_ON_FOCUS_PREF_KEY = "XWorld.UI.AutoConvertJsonOnFocus";
|
||
private const string AUTO_CONVERT_HTML_ON_FOCUS_PREF_KEY = "XWorld.UI.AutoConvertHtmlOnFocus";
|
||
private const string LOCAL_SERVER_EXE = "../../Server/build-gateway-local3/Debug/AIProjectServer.exe";
|
||
private const string LOCAL_SERVER_DLL_DIR = "../../Server/build-local-sqlite/vcpkg_installed/x64-windows/debug/bin";
|
||
private const string LOCAL_SERVER_LOG_DIR = "../../Server/logs";
|
||
private const string MINI_GAME_GATEWAY_RUNNER_DLL = "../../Server/Gateway.Runner/bin/Debug/net10.0/XWorld.Server.Gateway.Runner.dll";
|
||
private const string MINI_GAME_SERVER_GAMES_ROOT = "../../deploy/minigame";
|
||
private const string MINI_GAME_GATEWAY_PID_FILE = "pc-minigame-gateway.pid";
|
||
private const string MINI_GAME_CDN_RUNNER_DLL = "../../Server/Cdn.Runner/bin/Debug/net10.0/XWorld.Server.Cdn.Runner.dll";
|
||
private const string MINI_GAME_CDN_ROOT = "../../CDN";
|
||
private const string MINI_GAME_CDN_PID_FILE = "pc-cdn.pid";
|
||
private const int MINI_GAME_CDN_PORT = 15081;
|
||
private const string DEV_DISCOVERY_TOKEN = "xw-dev"; // 须与客户端 GlobalData.LanDiscoveryToken 一致
|
||
private const int LOCAL_GATEWAY_PORT = 7777;
|
||
private const int LOCAL_INTERNAL_GATEWAY_PORT = 7000;
|
||
private const int LOCAL_INTERNAL_LOGIN_PORT = 7001;
|
||
private const int LOCAL_INTERNAL_WORLD_PORT = 7002;
|
||
private const int LOCAL_INTERNAL_GAME_PORT = 7003;
|
||
private const int MINI_GAME_GATEWAY_PORT = 5005;
|
||
private const int MINI_GAME_TICK_MS = 100;
|
||
private const int MINI_GAME_MATCH_TIMEOUT_TICKS = 150;
|
||
|
||
private static readonly List<Process> LocalServerProcesses = new List<Process>();
|
||
private static readonly object LocalServerLogLock = new object();
|
||
|
||
static int MAX_FILE_HASH = 8;
|
||
|
||
static string m_sPlatform = "pc";
|
||
//导出到../../XDevNode/data
|
||
static string m_XWOutputPath = "../XDevNode/data";
|
||
static string m_CurID = "local";
|
||
|
||
|
||
static List<AssetBundleBuild> maps = new List<AssetBundleBuild>();
|
||
static List<string> paths = new List<string>();
|
||
static List<string> files = new List<string>();
|
||
|
||
static Dictionary<string, string> m_mapResNameDic = new Dictionary<string, string>();
|
||
static Dictionary<string, string> m_mapDepResNameDic = new Dictionary<string, string>();
|
||
static Dictionary<string, string> m_dicFilelist = new Dictionary<string, string>();
|
||
static string m_strResFileMD5;
|
||
|
||
public static void SetOutputPath(string path) { m_XWOutputPath = path; }
|
||
public static void SetCurID(string b64XID) { m_CurID = b64XID; }
|
||
|
||
static string AppDataPath
|
||
{
|
||
get { return Application.dataPath.ToLower(); }//Assets目录
|
||
}
|
||
|
||
public static string GetUpdatePath()
|
||
{
|
||
return $"{m_XWOutputPath}/{m_sPlatform}/{m_CurID}/";
|
||
}
|
||
|
||
//[MenuItem("XWorld/导出", false, 801)]
|
||
static public void Export()
|
||
{
|
||
//WebGL
|
||
BuildHotFix("game", BuildTargetGroup.WebGL, BuildTarget.WebGL);
|
||
//Android
|
||
//iOS
|
||
//pc
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/HTML 转 Prefab", false, 701)]
|
||
static public void ConvertHtmlToPrefabMenu()
|
||
{
|
||
string htmlFullPath = EditorUtility.OpenFilePanel("选择 UI HTML 设计稿", Path.GetFullPath(DEFAULT_HTML_DIR), "html");
|
||
if (string.IsNullOrEmpty(htmlFullPath))
|
||
return;
|
||
|
||
try
|
||
{
|
||
string prefabPath = ConvertHtmlToPrefab(htmlFullPath);
|
||
EditorUtility.DisplayDialog("HTML 转 Prefab", $"生成完成:\n{prefabPath}", "OK");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UnityEngine.Debug.LogException(ex);
|
||
EditorUtility.DisplayDialog("HTML 转 Prefab 失败", ex.Message, "OK");
|
||
}
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/Regenerate Auth Login Prefab", false, 702)]
|
||
static public void ConvertAuthRegisterLoginHtmlToPrefab()
|
||
{
|
||
string htmlFullPath = Path.Combine(Application.dataPath, "Doc/Output/AuthRegisterLogin.html");
|
||
string prefabPath = ConvertHtmlToPrefab(htmlFullPath);
|
||
UnityEngine.Debug.Log($"Generated auth login prefab: {prefabPath}");
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/Regenerate Match Waiting Prefab", false, 703)]
|
||
static public void ConvertMatchWaitingHtmlToPrefab()
|
||
{
|
||
string htmlFullPath = Path.Combine(Application.dataPath, "Doc/Output/MatchWaiting.html");
|
||
string prefabPath = ConvertHtmlToPrefab(htmlFullPath);
|
||
PostProcessConvertedHtmlPrefab(htmlFullPath, prefabPath);
|
||
UnityEngine.Debug.Log($"Generated match waiting prefab: {prefabPath}");
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/Convert Changed JSON Prefabs Now", false, 704)]
|
||
static public void ConvertChangedJsonPrefabsNow()
|
||
{
|
||
ConvertChangedJsonPrefabs(false, "manual");
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/Auto Convert JSON On Focus", false, 705)]
|
||
static public void ToggleAutoConvertJsonOnFocus()
|
||
{
|
||
bool enabled = !IsAutoConvertJsonOnFocusEnabled();
|
||
EditorPrefs.SetBool(AUTO_CONVERT_JSON_ON_FOCUS_PREF_KEY, enabled);
|
||
Menu.SetChecked("XWorld/UI/Auto Convert JSON On Focus", enabled);
|
||
UnityEngine.Debug.Log($"Auto convert JSON on Unity focus: {(enabled ? "enabled" : "disabled")}");
|
||
}
|
||
|
||
[MenuItem("XWorld/UI/Auto Convert JSON On Focus", true)]
|
||
static public bool ToggleAutoConvertJsonOnFocusValidate()
|
||
{
|
||
Menu.SetChecked("XWorld/UI/Auto Convert JSON On Focus", IsAutoConvertJsonOnFocusEnabled());
|
||
return true;
|
||
}
|
||
|
||
static public void AutoConvertChangedJsonPrefabsOnEditorFocus()
|
||
{
|
||
if (!IsAutoConvertJsonOnFocusEnabled())
|
||
return;
|
||
ConvertChangedJsonPrefabs(false, "focus");
|
||
}
|
||
|
||
static bool IsAutoConvertJsonOnFocusEnabled()
|
||
{
|
||
if (EditorPrefs.HasKey(AUTO_CONVERT_JSON_ON_FOCUS_PREF_KEY))
|
||
return EditorPrefs.GetBool(AUTO_CONVERT_JSON_ON_FOCUS_PREF_KEY, true);
|
||
return EditorPrefs.GetBool(AUTO_CONVERT_HTML_ON_FOCUS_PREF_KEY, true);
|
||
}
|
||
|
||
static bool s_IsAutoConvertingJsonPrefabs;
|
||
static double s_LastAutoConvertJsonPrefabTime;
|
||
|
||
static int ConvertChangedJsonPrefabs(bool force, string reason)
|
||
{
|
||
if (s_IsAutoConvertingJsonPrefabs)
|
||
return 0;
|
||
if (!force && EditorApplication.timeSinceStartup - s_LastAutoConvertJsonPrefabTime < 1.0)
|
||
return 0;
|
||
if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode)
|
||
return 0;
|
||
|
||
s_IsAutoConvertingJsonPrefabs = true;
|
||
s_LastAutoConvertJsonPrefabTime = EditorApplication.timeSinceStartup;
|
||
try
|
||
{
|
||
string jsonRoot = XGame.EditorTools.JsonUIPrefabBuilder.ResolveDefaultJsonRoot();
|
||
return XGame.EditorTools.JsonUIPrefabBuilder.BuildChanged(jsonRoot, reason, force).Count;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UnityEngine.Debug.LogException(ex);
|
||
return 0;
|
||
}
|
||
finally
|
||
{
|
||
s_IsAutoConvertingJsonPrefabs = false;
|
||
}
|
||
}
|
||
|
||
static bool s_IsAutoConvertingHtmlPrefabs;
|
||
static double s_LastAutoConvertHtmlPrefabTime;
|
||
|
||
static int ConvertChangedHtmlPrefabs(bool force, string reason)
|
||
{
|
||
if (s_IsAutoConvertingHtmlPrefabs)
|
||
return 0;
|
||
if (!force && EditorApplication.timeSinceStartup - s_LastAutoConvertHtmlPrefabTime < 1.0)
|
||
return 0;
|
||
if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode)
|
||
return 0;
|
||
|
||
string htmlDir = Path.Combine(Application.dataPath, DEFAULT_HTML_DIR.Substring("Assets/".Length));
|
||
if (!Directory.Exists(htmlDir))
|
||
return 0;
|
||
|
||
int converted = 0;
|
||
s_IsAutoConvertingHtmlPrefabs = true;
|
||
s_LastAutoConvertHtmlPrefabTime = EditorApplication.timeSinceStartup;
|
||
try
|
||
{
|
||
foreach (string htmlFullPath in Directory.GetFiles(htmlDir, "*.html", SearchOption.TopDirectoryOnly))
|
||
{
|
||
string prefabPath = GetHtmlOutputPrefabAssetPath(htmlFullPath);
|
||
if (string.IsNullOrEmpty(prefabPath))
|
||
continue;
|
||
if (!force && !IsHtmlNewerThanPrefab(htmlFullPath, prefabPath))
|
||
continue;
|
||
|
||
string generatedPrefabPath = ConvertHtmlToPrefab(htmlFullPath);
|
||
PostProcessConvertedHtmlPrefab(htmlFullPath, generatedPrefabPath);
|
||
converted++;
|
||
UnityEngine.Debug.Log($"[HTML2Prefab:{reason}] {Path.GetFileName(htmlFullPath)} -> {generatedPrefabPath}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UnityEngine.Debug.LogException(ex);
|
||
}
|
||
finally
|
||
{
|
||
s_IsAutoConvertingHtmlPrefabs = false;
|
||
}
|
||
return converted;
|
||
}
|
||
|
||
static string GetHtmlOutputPrefabAssetPath(string htmlFullPath)
|
||
{
|
||
if (!File.Exists(htmlFullPath))
|
||
return null;
|
||
string html = File.ReadAllText(htmlFullPath, Encoding.UTF8);
|
||
Dictionary<string, string> meta = ParseUnityPrefabMeta(html);
|
||
string outputPrefabPath = GetMetaValue(meta, "outputPrefabPath");
|
||
string prefabName = GetMetaValue(meta, "prefabName");
|
||
string outputFileName = string.IsNullOrEmpty(outputPrefabPath) ? $"{prefabName}.prefab" : Path.GetFileName(outputPrefabPath);
|
||
if (string.IsNullOrEmpty(outputFileName) || outputFileName == ".prefab")
|
||
outputFileName = $"{Path.GetFileNameWithoutExtension(htmlFullPath)}.prefab";
|
||
return NormalizeAssetPath($"{UI_PREFAB_DIR}/{outputFileName}");
|
||
}
|
||
|
||
static bool IsHtmlNewerThanPrefab(string htmlFullPath, string prefabAssetPath)
|
||
{
|
||
string prefabFullPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, NormalizeAssetPath(prefabAssetPath));
|
||
if (!File.Exists(prefabFullPath))
|
||
return true;
|
||
return File.GetLastWriteTimeUtc(htmlFullPath) > File.GetLastWriteTimeUtc(prefabFullPath).AddMilliseconds(100);
|
||
}
|
||
|
||
static void PostProcessConvertedHtmlPrefab(string htmlFullPath, string prefabPath)
|
||
{
|
||
string fileName = Path.GetFileName(htmlFullPath);
|
||
if (string.Equals(fileName, "MatchWaiting.html", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
EnsurePrefabRootComponent(prefabPath, "XGame.MatchWaitingController");
|
||
}
|
||
}
|
||
|
||
static void EnsurePrefabRootComponent(string prefabPath, string componentTypeName)
|
||
{
|
||
Type componentType = FindType(componentTypeName);
|
||
if (componentType == null || !typeof(Component).IsAssignableFrom(componentType))
|
||
{
|
||
UnityEngine.Debug.LogWarning($"Component type not found: {componentTypeName}");
|
||
return;
|
||
}
|
||
|
||
GameObject root = PrefabUtility.LoadPrefabContents(prefabPath);
|
||
try
|
||
{
|
||
if (root.GetComponent(componentType) == null)
|
||
{
|
||
root.AddComponent(componentType);
|
||
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
PrefabUtility.UnloadPrefabContents(root);
|
||
}
|
||
}
|
||
|
||
static public string ConvertHtmlToPrefab(string htmlFullPath)
|
||
{
|
||
if (!File.Exists(htmlFullPath))
|
||
throw new FileNotFoundException("HTML 文件不存在", htmlFullPath);
|
||
|
||
string html = File.ReadAllText(htmlFullPath, Encoding.UTF8);
|
||
HtmlDocumentData document = ParseHtmlDocument(html);
|
||
HtmlNode rootNode = FindUnityRoot(document.Root);
|
||
if (rootNode == null)
|
||
throw new Exception("HTML 中没有找到 data-unity-root=\"true\" 的根节点。");
|
||
|
||
Dictionary<string, string> meta = ParseUnityPrefabMeta(html);
|
||
string prefabName = GetMetaValue(meta, "prefabName");
|
||
if (string.IsNullOrEmpty(prefabName))
|
||
prefabName = GetAttr(rootNode, "data-unity-name", Path.GetFileNameWithoutExtension(htmlFullPath));
|
||
|
||
string outputPrefabPath = GetMetaValue(meta, "outputPrefabPath");
|
||
string outputFileName = string.IsNullOrEmpty(outputPrefabPath) ? $"{prefabName}.prefab" : Path.GetFileName(outputPrefabPath);
|
||
if (string.IsNullOrEmpty(outputFileName))
|
||
outputFileName = $"{prefabName}.prefab";
|
||
outputPrefabPath = NormalizeAssetPath($"{UI_PREFAB_DIR}/{outputFileName}");
|
||
|
||
EnsureAssetDirectory(Path.GetDirectoryName(outputPrefabPath));
|
||
|
||
GameObject prefabRoot = BuildUnityNode(rootNode, null, document.CssRules, true);
|
||
prefabRoot.name = prefabName;
|
||
|
||
PrefabUtility.SaveAsPrefabAsset(prefabRoot, outputPrefabPath);
|
||
UnityEngine.Object.DestroyImmediate(prefabRoot);
|
||
AssetDatabase.Refresh();
|
||
return outputPrefabPath;
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Open Local Server (All)", false, 601)]
|
||
static public void OpenLocalServerAll()
|
||
{
|
||
StopExitedLocalServerProcesses();
|
||
SetLocalServerPrefs();
|
||
// 内网 CDN(HTTP 静态文件)独立于网关,先确保起来(端口已开则跳过)
|
||
StartMiniGameCdnRunnerProcess("XWorld PC CDN");
|
||
if (IsVisibleMiniGameGatewayConsoleRunning())
|
||
{
|
||
UnityEngine.Debug.Log("PC MiniGame Gateway console is already running.");
|
||
return;
|
||
}
|
||
|
||
if (IsMiniGameGatewayRunnerRunning())
|
||
{
|
||
UnityEngine.Debug.Log("PC MiniGame Gateway is running without a tracked console; restarting it with a visible console.");
|
||
StopMiniGameGatewayRunnerFromPidFile();
|
||
WaitForMiniGameGatewayPortToClose(3000);
|
||
if (IsTcpPortOpen("127.0.0.1", MINI_GAME_GATEWAY_PORT))
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server",
|
||
"PC MiniGame Gateway port " + MINI_GAME_GATEWAY_PORT + " is already in use, but the process is not tracked by Unity.\n\n" +
|
||
"Please stop the existing Gateway.Runner/dotnet process, then press Play again.",
|
||
"OK");
|
||
return;
|
||
}
|
||
}
|
||
|
||
StartMiniGameGatewayRunnerProcess("XWorld PC minigame gateway");
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Open Local Server (Separate)", false, 602)]
|
||
static public void OpenLocalServerSeparate()
|
||
{
|
||
UnityEngine.Debug.Log("Separate legacy server startup is disabled for the PC minigame flow; starting Gateway.Runner instead.");
|
||
OpenLocalServerAll();
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Stop Local Server", false, 603)]
|
||
static public void StopLocalServer()
|
||
{
|
||
StopExitedLocalServerProcesses();
|
||
foreach (Process process in LocalServerProcesses.ToArray())
|
||
{
|
||
TryStopProcess(process);
|
||
}
|
||
LocalServerProcesses.Clear();
|
||
|
||
foreach (Process process in Process.GetProcessesByName("AIProjectServer"))
|
||
{
|
||
TryStopProcess(process);
|
||
}
|
||
StopMiniGameGatewayRunnerFromPidFile();
|
||
|
||
EditorUtility.DisplayDialog("XWorld Server", "Local server stopped.", "OK");
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Status", false, 604)]
|
||
static public void LocalServerStatus()
|
||
{
|
||
StopExitedLocalServerProcesses();
|
||
Process[] processes = Process.GetProcessesByName("AIProjectServer");
|
||
int pid = ReadMiniGameGatewayPid();
|
||
EditorUtility.DisplayDialog(
|
||
"XWorld Server",
|
||
"Tracked processes: " + LocalServerProcesses.Count +
|
||
"\nRunning AIProjectServer processes: " + processes.Length +
|
||
"\nPC MiniGame Gateway pid: " + (pid > 0 && IsProcessAlive(pid) ? pid.ToString() : "not running") +
|
||
"\nPC MiniGame Gateway port " + MINI_GAME_GATEWAY_PORT + ": " + (IsTcpPortOpen("127.0.0.1", MINI_GAME_GATEWAY_PORT) ? "open" : "closed"),
|
||
"OK");
|
||
}
|
||
|
||
static public bool IsPcMiniGameGatewayReady()
|
||
{
|
||
return IsMiniGameGatewayRunnerRunning();
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Open Latest Log", false, 605)]
|
||
static public void OpenLatestLocalServerLog()
|
||
{
|
||
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
||
if (!Directory.Exists(logDir))
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server", "Log directory not found:\n" + logDir, "OK");
|
||
return;
|
||
}
|
||
|
||
FileInfo latest = new DirectoryInfo(logDir)
|
||
.GetFiles("*.log")
|
||
.OrderByDescending(file => file.LastWriteTimeUtc)
|
||
.FirstOrDefault();
|
||
if (latest == null)
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server", "No server log found:\n" + logDir, "OK");
|
||
return;
|
||
}
|
||
|
||
EditorUtility.OpenWithDefaultApp(latest.FullName);
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Open Local Server (All)", true)]
|
||
[MenuItem("XWorld/Server/Open Local Server (Separate)", true)]
|
||
static public bool CanOpenLocalServer()
|
||
{
|
||
return File.Exists(GetMiniGameGatewayRunnerPath());
|
||
}
|
||
|
||
[MenuItem("XWorld/Server/Stop Local Server", true)]
|
||
static public bool CanStopLocalServer()
|
||
{
|
||
int pid = ReadMiniGameGatewayPid();
|
||
return LocalServerProcesses.Count > 0 || IsProcessRunning("AIProjectServer") || (pid > 0 && IsProcessAlive(pid));
|
||
}
|
||
|
||
[MenuItem("XWorld/Start Game", false, 801)]
|
||
static public void Run()
|
||
{
|
||
//传输文件,
|
||
//启动运行节点
|
||
if (IsProcessRunning("XWorldNode"))
|
||
{
|
||
RunExecutable("../XWorld/XWorldNode.exe");
|
||
}
|
||
|
||
//运行游戏
|
||
if (EditorApplication.isPlaying)
|
||
{
|
||
UnityEditor.EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||
EditorApplication.ExitPlaymode();
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.OpenScene(MAIN_SCENE);
|
||
EditorApplication.EnterPlaymode();
|
||
}
|
||
}
|
||
static private void OnPlayModeStateChanged(PlayModeStateChange state)
|
||
{
|
||
if (state == PlayModeStateChange.EnteredEditMode)
|
||
{
|
||
UnityEditor.EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||
EditorSceneManager.OpenScene(MAIN_SCENE);
|
||
EditorApplication.EnterPlaymode();
|
||
}
|
||
}
|
||
|
||
static private string GetProjectRoot()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, "../.."));
|
||
}
|
||
|
||
static private string GetLocalServerExePath()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_EXE));
|
||
}
|
||
|
||
static private string GetLocalServerDllPath()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_DLL_DIR));
|
||
}
|
||
|
||
static private string GetMiniGameGatewayRunnerPath()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_GATEWAY_RUNNER_DLL));
|
||
}
|
||
|
||
static private string GetMiniGameServerGamesRoot()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_SERVER_GAMES_ROOT));
|
||
}
|
||
|
||
static private string GetMiniGameGatewayPidPath()
|
||
{
|
||
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
||
Directory.CreateDirectory(logDir);
|
||
return Path.Combine(logDir, MINI_GAME_GATEWAY_PID_FILE);
|
||
}
|
||
|
||
static private string GetMiniGameCdnRunnerPath()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_RUNNER_DLL));
|
||
}
|
||
|
||
static private string GetMiniGameCdnRoot()
|
||
{
|
||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_ROOT));
|
||
}
|
||
|
||
static private string GetMiniGameCdnPidPath()
|
||
{
|
||
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
||
Directory.CreateDirectory(logDir);
|
||
return Path.Combine(logDir, MINI_GAME_CDN_PID_FILE);
|
||
}
|
||
|
||
static private string GetLocalServerLogPath(string title)
|
||
{
|
||
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
||
Directory.CreateDirectory(logDir);
|
||
string safeTitle = Regex.Replace(title, "[^a-zA-Z0-9_-]+", "-").Trim('-');
|
||
if (string.IsNullOrEmpty(safeTitle))
|
||
safeTitle = "server";
|
||
return Path.Combine(logDir, safeTitle + "-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".log");
|
||
}
|
||
|
||
static private void StartServerProcess(string arguments, string title)
|
||
{
|
||
string exePath = GetLocalServerExePath();
|
||
if (!File.Exists(exePath))
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server", "Server executable not found:\n" + exePath + "\n\nBuild Server first.", "OK");
|
||
return;
|
||
}
|
||
|
||
string dllPath = GetLocalServerDllPath();
|
||
if (!Directory.Exists(dllPath))
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server", "Server DLL directory not found:\n" + dllPath + "\n\nBuild Server dependencies first.", "OK");
|
||
return;
|
||
}
|
||
|
||
string logPath = GetLocalServerLogPath(title);
|
||
string scriptPath = Path.Combine(Path.GetDirectoryName(logPath), "run-" + Path.GetFileNameWithoutExtension(logPath) + ".ps1");
|
||
string script =
|
||
"$ErrorActionPreference = 'Continue'\r\n" +
|
||
"$env:PATH = '" + EscapePowerShellSingleQuoted(dllPath) + ";' + $env:PATH\r\n" +
|
||
"Set-Location -LiteralPath '" + EscapePowerShellSingleQuoted(GetProjectRoot()) + "'\r\n" +
|
||
"Write-Host 'Starting " + EscapePowerShellSingleQuoted(title) + "'\r\n" +
|
||
"Write-Host 'Exe: " + EscapePowerShellSingleQuoted(exePath) + "'\r\n" +
|
||
"Write-Host 'Args: " + EscapePowerShellSingleQuoted(arguments) + "'\r\n" +
|
||
"Write-Host 'Log: " + EscapePowerShellSingleQuoted(logPath) + "'\r\n" +
|
||
"& '" + EscapePowerShellSingleQuoted(exePath) + "' " + arguments + " *>&1 | Tee-Object -FilePath '" + EscapePowerShellSingleQuoted(logPath) + "'\r\n" +
|
||
"Write-Host ''\r\n" +
|
||
"Write-Host 'Server process exited. Press Enter to close this window.'\r\n" +
|
||
"Read-Host\r\n";
|
||
File.WriteAllText(scriptPath, script, new UTF8Encoding(false));
|
||
|
||
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||
startInfo.FileName = "powershell.exe";
|
||
startInfo.Arguments = "-ExecutionPolicy Bypass -File \"" + scriptPath + "\"";
|
||
startInfo.WorkingDirectory = GetProjectRoot();
|
||
startInfo.UseShellExecute = true;
|
||
startInfo.CreateNoWindow = false;
|
||
|
||
Process process = Process.Start(startInfo);
|
||
if (process != null)
|
||
{
|
||
LocalServerProcesses.Add(process);
|
||
UnityEngine.Debug.Log(title + " started. pid=" + process.Id + " args=" + arguments + " log=" + logPath);
|
||
}
|
||
}
|
||
|
||
static private void StartMiniGameGatewayRunnerProcess(string title)
|
||
{
|
||
string runnerPath = GetMiniGameGatewayRunnerPath();
|
||
if (!File.Exists(runnerPath))
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server",
|
||
"Gateway runner not found:\n" + runnerPath + "\n\nBuild Server/Gateway.Runner first.", "OK");
|
||
return;
|
||
}
|
||
|
||
// 不做小游戏包存在性检测:网关支持运行中热更(BuildGameList 每请求重扫、建房按需加载),
|
||
// 空目录照常启动,之后用菜单 [XWorld/生成小游戏更新] 发布即生效。
|
||
string gamesRoot = GetMiniGameServerGamesRoot();
|
||
Directory.CreateDirectory(gamesRoot);
|
||
if (Directory.GetFiles(gamesRoot, "game.json", SearchOption.AllDirectories).Length == 0)
|
||
{
|
||
UnityEngine.Debug.Log("PC MiniGame Gateway: games root is empty (" + gamesRoot +
|
||
"). Publish anytime via menu [XWorld/生成小游戏更新]; it takes effect while the gateway is running.");
|
||
}
|
||
|
||
if (IsTcpPortOpen("127.0.0.1", MINI_GAME_GATEWAY_PORT))
|
||
{
|
||
UnityEngine.Debug.Log("PC MiniGame Gateway port is already open: " + MINI_GAME_GATEWAY_PORT);
|
||
return;
|
||
}
|
||
|
||
string logPath = GetLocalServerLogPath(title);
|
||
string scriptPath = Path.Combine(Path.GetDirectoryName(logPath), "run-" + Path.GetFileNameWithoutExtension(logPath) + ".ps1");
|
||
string script =
|
||
"$ErrorActionPreference = 'Continue'\r\n" +
|
||
"Set-Location -LiteralPath '" + EscapePowerShellSingleQuoted(GetProjectRoot()) + "'\r\n" +
|
||
"$runner = '" + EscapePowerShellSingleQuoted(runnerPath) + "'\r\n" +
|
||
"$gamesRoot = '" + EscapePowerShellSingleQuoted(gamesRoot) + "'\r\n" +
|
||
"$logPath = '" + EscapePowerShellSingleQuoted(logPath) + "'\r\n" +
|
||
"Write-Host 'Starting " + EscapePowerShellSingleQuoted(title) + "'\r\n" +
|
||
"Write-Host ('Runner: ' + $runner)\r\n" +
|
||
"Write-Host ('Games: ' + $gamesRoot)\r\n" +
|
||
"Write-Host 'WS: ws://127.0.0.1:" + MINI_GAME_GATEWAY_PORT + "/ws?pid=1'\r\n" +
|
||
"Write-Host ('Log: ' + $logPath)\r\n" +
|
||
"Write-Host ''\r\n" +
|
||
"& dotnet $runner --gamesRoot $gamesRoot --port " + MINI_GAME_GATEWAY_PORT +
|
||
" --tickMs " + MINI_GAME_TICK_MS +
|
||
" --matchTimeoutTicks " + MINI_GAME_MATCH_TIMEOUT_TICKS +
|
||
" --lan --devToken " + DEV_DISCOVERY_TOKEN + // 开 DevDiscovery(UDP48923) 响应 + 广播 LAN IP;CDN 地址由 --lan 兜底为 http://<lanip>:15081/
|
||
" *>&1 | Tee-Object -FilePath $logPath\r\n" +
|
||
"Write-Host ''\r\n" +
|
||
"Write-Host 'Gateway process exited. Press Enter to close this window.'\r\n" +
|
||
"Read-Host\r\n";
|
||
File.WriteAllText(scriptPath, script, new UTF8Encoding(false));
|
||
|
||
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||
startInfo.FileName = "powershell.exe";
|
||
startInfo.Arguments = "-ExecutionPolicy Bypass -File \"" + scriptPath + "\"";
|
||
startInfo.WorkingDirectory = GetProjectRoot();
|
||
startInfo.UseShellExecute = true;
|
||
startInfo.CreateNoWindow = false;
|
||
|
||
Process process = Process.Start(startInfo);
|
||
if (process == null)
|
||
{
|
||
EditorUtility.DisplayDialog("XWorld Server", "Failed to start Gateway.Runner.", "OK");
|
||
return;
|
||
}
|
||
|
||
LocalServerProcesses.Add(process);
|
||
File.WriteAllText(GetMiniGameGatewayPidPath(), process.Id.ToString(), Encoding.UTF8);
|
||
AppendLocalServerLog(logPath, "Started " + title + " pid=" + process.Id);
|
||
AppendLocalServerLog(logPath, "powershell " + startInfo.Arguments);
|
||
UnityEngine.Debug.Log(title + " started. pid=" + process.Id + " ws=ws://127.0.0.1:" + MINI_GAME_GATEWAY_PORT + "/ws?pid=1 log=" + logPath);
|
||
}
|
||
|
||
// 内网 CDN(HTTP 静态文件):服务仓库 CDN/ 目录,含 XWorld/<platform>/(旧全量热更)与 minigame/<id>/<ver>/。
|
||
static private void StartMiniGameCdnRunnerProcess(string title)
|
||
{
|
||
string runnerPath = GetMiniGameCdnRunnerPath();
|
||
if (!File.Exists(runnerPath))
|
||
{
|
||
UnityEngine.Debug.LogWarning("[CDN] 未找到 Cdn.Runner:" + runnerPath + "\n请先构建 Server/Cdn.Runner(dotnet build Server/Server.sln)。");
|
||
return;
|
||
}
|
||
|
||
// 防重复:pid 文件在 Process.Start 时即写(先于 dotnet 绑端口),挡住多次 OpenLocalServerAll 之间的冷启动竞速
|
||
int existingPid = ReadMiniGameCdnPid();
|
||
if (existingPid > 0 && IsProcessAlive(existingPid))
|
||
{
|
||
UnityEngine.Debug.Log("PC CDN console already running (pid " + existingPid + ").");
|
||
return;
|
||
}
|
||
|
||
if (IsTcpPortOpen("127.0.0.1", MINI_GAME_CDN_PORT))
|
||
{
|
||
UnityEngine.Debug.Log("PC CDN port is already open: " + MINI_GAME_CDN_PORT);
|
||
return;
|
||
}
|
||
|
||
string cdnRoot = GetMiniGameCdnRoot();
|
||
Directory.CreateDirectory(cdnRoot);
|
||
|
||
string logPath = GetLocalServerLogPath(title);
|
||
string scriptPath = Path.Combine(Path.GetDirectoryName(logPath), "run-" + Path.GetFileNameWithoutExtension(logPath) + ".ps1");
|
||
string script =
|
||
"$ErrorActionPreference = 'Continue'\r\n" +
|
||
"Set-Location -LiteralPath '" + EscapePowerShellSingleQuoted(GetProjectRoot()) + "'\r\n" +
|
||
"$runner = '" + EscapePowerShellSingleQuoted(runnerPath) + "'\r\n" +
|
||
"$cdnRoot = '" + EscapePowerShellSingleQuoted(cdnRoot) + "'\r\n" +
|
||
"$logPath = '" + EscapePowerShellSingleQuoted(logPath) + "'\r\n" +
|
||
"Write-Host 'Starting " + EscapePowerShellSingleQuoted(title) + "'\r\n" +
|
||
"Write-Host ('Runner: ' + $runner)\r\n" +
|
||
"Write-Host ('Root: ' + $cdnRoot)\r\n" +
|
||
"Write-Host 'HTTP: http://127.0.0.1:" + MINI_GAME_CDN_PORT + "/ (XWorld/<platform>/ , minigame/<id>/<ver>/)'\r\n" +
|
||
"Write-Host ('Log: ' + $logPath)\r\n" +
|
||
"Write-Host ''\r\n" +
|
||
"& dotnet $runner --root $cdnRoot --port " + MINI_GAME_CDN_PORT + " --lan" +
|
||
" *>&1 | Tee-Object -FilePath $logPath\r\n" +
|
||
"Write-Host ''\r\n" +
|
||
"Write-Host 'CDN process exited. Press Enter to close this window.'\r\n" +
|
||
"Read-Host\r\n";
|
||
File.WriteAllText(scriptPath, script, new UTF8Encoding(false));
|
||
|
||
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||
startInfo.FileName = "powershell.exe";
|
||
startInfo.Arguments = "-ExecutionPolicy Bypass -File \"" + scriptPath + "\"";
|
||
startInfo.WorkingDirectory = GetProjectRoot();
|
||
startInfo.UseShellExecute = true;
|
||
startInfo.CreateNoWindow = false;
|
||
|
||
Process process = Process.Start(startInfo);
|
||
if (process == null)
|
||
{
|
||
UnityEngine.Debug.LogWarning("[CDN] 启动 Cdn.Runner 失败。");
|
||
return;
|
||
}
|
||
|
||
LocalServerProcesses.Add(process);
|
||
File.WriteAllText(GetMiniGameCdnPidPath(), process.Id.ToString(), Encoding.UTF8);
|
||
AppendLocalServerLog(logPath, "Started " + title + " pid=" + process.Id);
|
||
UnityEngine.Debug.Log(title + " started. pid=" + process.Id + " http=http://127.0.0.1:" + MINI_GAME_CDN_PORT + "/ log=" + logPath);
|
||
}
|
||
|
||
static private void SetLocalServerPrefs()
|
||
{
|
||
PlayerPrefs.SetInt("XWorldLocal", 1);
|
||
PlayerPrefs.SetString("CurNode", "ws://127.0.0.1:" + MINI_GAME_GATEWAY_PORT + "/ws?pid=1");
|
||
PlayerPrefs.SetString("CurCDN", "http://127.0.0.1:" + MINI_GAME_CDN_PORT + "/");
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
static private string EscapePowerShellSingleQuoted(string text)
|
||
{
|
||
return (text ?? string.Empty).Replace("'", "''");
|
||
}
|
||
|
||
static private void StopExitedLocalServerProcesses()
|
||
{
|
||
LocalServerProcesses.RemoveAll(process =>
|
||
{
|
||
try
|
||
{
|
||
return process == null || process.HasExited;
|
||
}
|
||
catch
|
||
{
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
|
||
static private void TryStopProcess(Process process)
|
||
{
|
||
if (process == null)
|
||
return;
|
||
|
||
try
|
||
{
|
||
if (!process.HasExited)
|
||
{
|
||
process.Kill();
|
||
process.WaitForExit(2000);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UnityEngine.Debug.LogWarning("Stop process failed: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
static private void StopMiniGameGatewayRunnerFromPidFile()
|
||
{
|
||
int pid = ReadMiniGameGatewayPid();
|
||
if (pid > 0)
|
||
{
|
||
try
|
||
{
|
||
TryStopProcess(Process.GetProcessById(pid));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UnityEngine.Debug.LogWarning("Stop Gateway.Runner by pid failed: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
string pidPath = GetMiniGameGatewayPidPath();
|
||
if (File.Exists(pidPath))
|
||
File.Delete(pidPath);
|
||
}
|
||
|
||
static private int ReadMiniGameGatewayPid()
|
||
{
|
||
string pidPath = GetMiniGameGatewayPidPath();
|
||
if (!File.Exists(pidPath))
|
||
return -1;
|
||
int pid;
|
||
return int.TryParse(File.ReadAllText(pidPath).Trim(), out pid) ? pid : -1;
|
||
}
|
||
|
||
static private int ReadMiniGameCdnPid()
|
||
{
|
||
string pidPath = GetMiniGameCdnPidPath();
|
||
if (!File.Exists(pidPath))
|
||
return -1;
|
||
int pid;
|
||
return int.TryParse(File.ReadAllText(pidPath).Trim(), out pid) ? pid : -1;
|
||
}
|
||
|
||
static private bool IsMiniGameGatewayRunnerRunning()
|
||
{
|
||
StopExitedLocalServerProcesses();
|
||
foreach (Process process in LocalServerProcesses)
|
||
{
|
||
try
|
||
{
|
||
if (process != null && !process.HasExited)
|
||
return true;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
int pid = ReadMiniGameGatewayPid();
|
||
return pid > 0 && IsProcessAlive(pid) || IsTcpPortOpen("127.0.0.1", MINI_GAME_GATEWAY_PORT);
|
||
}
|
||
|
||
static private bool IsVisibleMiniGameGatewayConsoleRunning()
|
||
{
|
||
int pid = ReadMiniGameGatewayPid();
|
||
if (pid <= 0)
|
||
return false;
|
||
|
||
try
|
||
{
|
||
Process process = Process.GetProcessById(pid);
|
||
if (process == null || process.HasExited)
|
||
return false;
|
||
|
||
string name = process.ProcessName ?? string.Empty;
|
||
return name.IndexOf("powershell", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
name.IndexOf("pwsh", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
name.IndexOf("WindowsTerminal", StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static private void WaitForMiniGameGatewayPortToClose(int timeoutMs)
|
||
{
|
||
DateTime deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
||
while (DateTime.UtcNow < deadline)
|
||
{
|
||
if (!IsTcpPortOpen("127.0.0.1", MINI_GAME_GATEWAY_PORT))
|
||
return;
|
||
System.Threading.Thread.Sleep(100);
|
||
}
|
||
}
|
||
|
||
static private bool IsProcessAlive(int pid)
|
||
{
|
||
try
|
||
{
|
||
Process process = Process.GetProcessById(pid);
|
||
return process != null && !process.HasExited;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static private bool IsTcpPortOpen(string host, int port)
|
||
{
|
||
try
|
||
{
|
||
using (var client = new System.Net.Sockets.TcpClient())
|
||
{
|
||
IAsyncResult result = client.BeginConnect(host, port, null, null);
|
||
bool connected = result.AsyncWaitHandle.WaitOne(100);
|
||
if (!connected)
|
||
return false;
|
||
client.EndConnect(result);
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static private void AppendLocalServerLog(string logPath, string line)
|
||
{
|
||
if (string.IsNullOrEmpty(line))
|
||
return;
|
||
lock (LocalServerLogLock)
|
||
{
|
||
File.AppendAllText(logPath, DateTime.Now.ToString("HH:mm:ss.fff ") + line + Environment.NewLine, Encoding.UTF8);
|
||
}
|
||
}
|
||
|
||
static private string QuoteProcessArg(string value)
|
||
{
|
||
return "\"" + (value ?? string.Empty).Replace("\"", "\\\"") + "\"";
|
||
}
|
||
|
||
static private int SafeExitCode(Process process)
|
||
{
|
||
try { return process.ExitCode; }
|
||
catch { return -1; }
|
||
}
|
||
|
||
static private void RunExecutable(string path)
|
||
{
|
||
if (System.IO.File.Exists(path))
|
||
{
|
||
Process.Start(path);
|
||
EditorUtility.DisplayDialog("Process Runner", "Executable started successfully.", "OK");
|
||
}
|
||
else
|
||
{
|
||
EditorUtility.DisplayDialog("Process Runner", "Executable not found.", "OK");
|
||
}
|
||
}
|
||
static private bool IsProcessRunning(string processName)
|
||
{
|
||
Process[] processes = Process.GetProcessesByName(processName);
|
||
return processes.Any();
|
||
}
|
||
|
||
static GameObject BuildUnityNode(HtmlNode node, Transform parent, Dictionary<string, Dictionary<string, string>> cssRules, bool isRoot = false, HtmlLayout layoutOverride = null)
|
||
{
|
||
string unityType = GetAttr(node, "data-unity-type", isRoot ? "Canvas" : "Empty");
|
||
string unityName = GetAttr(node, "data-unity-name", string.IsNullOrEmpty(node.TagName) ? "UIElement" : node.TagName);
|
||
Dictionary<string, string> style = GetComputedStyle(node, cssRules);
|
||
|
||
GameObject go = new GameObject(unityName, typeof(RectTransform));
|
||
if (parent != null)
|
||
go.transform.SetParent(parent, false);
|
||
|
||
RectTransform rect = go.GetComponent<RectTransform>();
|
||
ApplyRectTransform(rect, node, style, isRoot, layoutOverride);
|
||
|
||
Graphic graphic = null;
|
||
Selectable selectable = null;
|
||
switch (unityType)
|
||
{
|
||
case "Canvas":
|
||
Canvas canvas = go.AddComponent<Canvas>();
|
||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
CanvasScaler scaler = go.AddComponent<CanvasScaler>();
|
||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||
scaler.referenceResolution = GetDesignResolution(node);
|
||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||
scaler.matchWidthOrHeight = 0.5f;
|
||
go.AddComponent<GraphicRaycaster>();
|
||
if (style.ContainsKey("background-color") || HasAttr(node, "data-color"))
|
||
AddImage(go, node, style);
|
||
break;
|
||
case "Panel":
|
||
case "Image":
|
||
graphic = AddImage(go, node, style);
|
||
break;
|
||
case "Button":
|
||
graphic = AddImage(go, node, style);
|
||
Button button = go.AddComponent<Button>();
|
||
ApplyButtonSprites(button, node);
|
||
selectable = button;
|
||
Graphic labelGraphic = EnsureButtonLabel(go.transform, node, style, graphic == null);
|
||
button.targetGraphic = graphic != null ? graphic : labelGraphic;
|
||
break;
|
||
case "TextMeshProUGUI":
|
||
AddText(go, node, style);
|
||
break;
|
||
case "TMP_InputField":
|
||
graphic = AddImage(go, node, style);
|
||
Component input = AddComponentByTypeName(go, "TMPro.TMP_InputField");
|
||
if (input == null)
|
||
throw new Exception("项目未能创建 TMPro.TMP_InputField,请确认 TextMeshPro 包已安装并被当前程序集引用。");
|
||
SetObjectProperty(input, "targetGraphic", graphic);
|
||
SetObjectProperty(input, "characterLimit", ParseInt(GetAttr(node, "data-character-limit", "0"), 0));
|
||
SetObjectProperty(input, "contentType", ParseEnumProperty(input, "contentType", GetAttr(node, "data-content-type", "Standard")));
|
||
CreateInputTextChildren(go.transform, node, input);
|
||
break;
|
||
case "Toggle":
|
||
graphic = AddImage(go, node, style);
|
||
Toggle toggle = go.AddComponent<Toggle>();
|
||
toggle.targetGraphic = graphic;
|
||
toggle.isOn = ParseBool(GetAttr(node, "data-is-on", "false"));
|
||
CreateToggleGraphicChildren(go.transform, node, toggle);
|
||
selectable = toggle;
|
||
break;
|
||
case "Slider":
|
||
go.AddComponent<Slider>();
|
||
break;
|
||
case "Scrollbar":
|
||
go.AddComponent<Scrollbar>();
|
||
break;
|
||
case "ScrollView":
|
||
go.AddComponent<ScrollRect>();
|
||
break;
|
||
}
|
||
|
||
ApplyCommonMetadata(go, node);
|
||
if (selectable != null)
|
||
selectable.navigation = new Navigation { mode = Navigation.Mode.Automatic };
|
||
|
||
Dictionary<HtmlNode, HtmlLayout> childLayouts = ComputeChildLayouts(node, rect, style, cssRules);
|
||
foreach (HtmlNode child in node.Children)
|
||
{
|
||
if (!IsUnityNode(child))
|
||
continue;
|
||
childLayouts.TryGetValue(child, out HtmlLayout childLayout);
|
||
BuildUnityNode(child, go.transform, cssRules, false, childLayout);
|
||
}
|
||
|
||
return go;
|
||
}
|
||
|
||
static Image AddImage(GameObject go, HtmlNode node, Dictionary<string, string> style)
|
||
{
|
||
string spritePath = GetFirstAttr(node, "data-sprite", "data-sprite-normal");
|
||
bool hasColor = style.ContainsKey("background-color") || HasAttr(node, "data-color");
|
||
if (string.IsNullOrEmpty(spritePath) && !hasColor)
|
||
return null;
|
||
|
||
Sprite sprite = null;
|
||
if (!string.IsNullOrEmpty(spritePath))
|
||
{
|
||
sprite = AssetDatabase.LoadAssetAtPath<Sprite>(NormalizeAssetPath(spritePath));
|
||
if (sprite == null)
|
||
{
|
||
UnityEngine.Debug.LogWarning($"HTML 转 Prefab:找不到贴图 {spritePath},节点 {GetAttr(node, "data-unity-name", go.name)} 不使用贴图。");
|
||
if (!hasColor)
|
||
return null;
|
||
}
|
||
}
|
||
|
||
Image image = go.AddComponent<Image>();
|
||
image.raycastTarget = GetAttr(node, "data-unity-type", "") == "Button" || GetAttr(node, "data-unity-type", "") == "Toggle";
|
||
image.sprite = sprite;
|
||
image.type = ParseImageType(GetAttr(node, "data-image-type", ""));
|
||
if (style.TryGetValue("background-color", out string bgColor))
|
||
image.color = ParseColor(bgColor, Color.white);
|
||
else if (HasAttr(node, "data-color"))
|
||
image.color = ParseColor(GetAttr(node, "data-color", "#FFFFFFFF"), Color.white);
|
||
|
||
return image;
|
||
}
|
||
|
||
static Component AddText(GameObject go, HtmlNode node, Dictionary<string, string> style)
|
||
{
|
||
Component text = AddComponentByTypeName(go, "TMPro.TextMeshProUGUI");
|
||
if (text == null)
|
||
throw new Exception("项目未能创建 TMPro.TextMeshProUGUI,请确认 TextMeshPro 包已安装并被当前程序集引用。");
|
||
SetObjectProperty(text, "raycastTarget", false);
|
||
SetObjectProperty(text, "text", HtmlDecode(GetAttr(node, "data-text", GetInnerText(node))));
|
||
SetObjectProperty(text, "fontSize", ParseFloat(GetFirstValue(node, style, "data-font-size", "font-size"), 24f));
|
||
SetObjectProperty(text, "color", ParseColor(GetFirstValue(node, style, "data-color", "color"), Color.white));
|
||
SetObjectProperty(text, "alignment", ParseTextAlignment(text, GetAttr(node, "data-alignment", GetCssTextAlign(style))));
|
||
SetObjectProperty(text, "fontStyle", ParseFontStyle(text, node, style));
|
||
bool hasExplicitWidth = HasStyle(style, "width");
|
||
SetObjectProperty(text, "enableWordWrapping", hasExplicitWidth);
|
||
if (!hasExplicitWidth)
|
||
SetObjectProperty(text, "overflowMode", ParseTmpEnum(text, "overflowMode", "Overflow"));
|
||
return text;
|
||
}
|
||
|
||
static void ApplyButtonSprites(Button button, HtmlNode node)
|
||
{
|
||
if (GetAttr(node, "data-button-transition", "") != "SpriteSwap")
|
||
return;
|
||
|
||
button.transition = Selectable.Transition.SpriteSwap;
|
||
SpriteState state = button.spriteState;
|
||
state.highlightedSprite = LoadSprite(GetAttr(node, "data-sprite-highlighted", ""));
|
||
state.pressedSprite = LoadSprite(GetAttr(node, "data-sprite-pressed", ""));
|
||
state.disabledSprite = LoadSprite(GetAttr(node, "data-sprite-disabled", ""));
|
||
button.spriteState = state;
|
||
}
|
||
|
||
static Graphic EnsureButtonLabel(Transform parent, HtmlNode node, Dictionary<string, string> style, bool useAsTargetGraphic)
|
||
{
|
||
string textValue = HtmlDecode(GetAttr(node, "data-text", GetInnerText(node))).Trim();
|
||
if (string.IsNullOrEmpty(textValue) || HasTmpTextInChildren(parent))
|
||
return null;
|
||
|
||
GameObject label = new GameObject("Text", typeof(RectTransform));
|
||
label.transform.SetParent(parent, false);
|
||
RectTransform rect = label.GetComponent<RectTransform>();
|
||
rect.anchorMin = Vector2.zero;
|
||
rect.anchorMax = Vector2.one;
|
||
rect.offsetMin = Vector2.zero;
|
||
rect.offsetMax = Vector2.zero;
|
||
|
||
Component text = AddComponentByTypeName(label, "TMPro.TextMeshProUGUI");
|
||
if (text == null)
|
||
throw new Exception("项目未能创建 TMPro.TextMeshProUGUI,请确认 TextMeshPro 包已安装并被当前程序集引用。");
|
||
SetObjectProperty(text, "text", textValue);
|
||
SetObjectProperty(text, "fontSize", ParseFloat(GetFirstValue(node, style, "data-font-size", "font-size"), 28f));
|
||
SetObjectProperty(text, "color", ParseColor(GetFirstValue(node, style, "data-color", "color"), Color.white));
|
||
SetObjectProperty(text, "alignment", ParseTextAlignment(text, GetAttr(node, "data-alignment", GetCssTextAlign(style))));
|
||
SetObjectProperty(text, "fontStyle", ParseFontStyle(text, node, style));
|
||
SetObjectProperty(text, "enableWordWrapping", false);
|
||
SetObjectProperty(text, "overflowMode", ParseTmpEnum(text, "overflowMode", "Overflow"));
|
||
SetObjectProperty(text, "raycastTarget", useAsTargetGraphic);
|
||
return text as Graphic;
|
||
}
|
||
|
||
static void CreateInputTextChildren(Transform parent, HtmlNode node, Component input)
|
||
{
|
||
RectTransform parentRect = parent.GetComponent<RectTransform>();
|
||
Vector2 size = parentRect != null ? parentRect.sizeDelta : new Vector2(300, 64);
|
||
|
||
GameObject textArea = new GameObject("TextArea", typeof(RectTransform), typeof(RectMask2D));
|
||
textArea.transform.SetParent(parent, false);
|
||
RectTransform areaRect = textArea.GetComponent<RectTransform>();
|
||
areaRect.anchorMin = Vector2.zero;
|
||
areaRect.anchorMax = Vector2.one;
|
||
areaRect.offsetMin = new Vector2(24, 8);
|
||
areaRect.offsetMax = new Vector2(-24, -8);
|
||
|
||
GameObject placeholderGo = new GameObject("Placeholder", typeof(RectTransform));
|
||
placeholderGo.transform.SetParent(textArea.transform, false);
|
||
RectTransform placeholderRect = placeholderGo.GetComponent<RectTransform>();
|
||
placeholderRect.anchorMin = Vector2.zero;
|
||
placeholderRect.anchorMax = Vector2.one;
|
||
placeholderRect.offsetMin = Vector2.zero;
|
||
placeholderRect.offsetMax = Vector2.zero;
|
||
Component placeholder = AddComponentByTypeName(placeholderGo, "TMPro.TextMeshProUGUI");
|
||
SetObjectProperty(placeholder, "text", GetAttr(node, "data-placeholder", ""));
|
||
SetObjectProperty(placeholder, "fontSize", 24f);
|
||
SetObjectProperty(placeholder, "color", new Color(0.49f, 0.64f, 0.70f, 1f));
|
||
SetObjectProperty(placeholder, "alignment", ParseTextAlignment(placeholder, "MidlineLeft"));
|
||
SetObjectProperty(placeholder, "raycastTarget", false);
|
||
|
||
GameObject textGo = new GameObject("Text", typeof(RectTransform));
|
||
textGo.transform.SetParent(textArea.transform, false);
|
||
RectTransform textRect = textGo.GetComponent<RectTransform>();
|
||
textRect.anchorMin = Vector2.zero;
|
||
textRect.anchorMax = Vector2.one;
|
||
textRect.offsetMin = Vector2.zero;
|
||
textRect.offsetMax = Vector2.zero;
|
||
Component text = AddComponentByTypeName(textGo, "TMPro.TextMeshProUGUI");
|
||
SetObjectProperty(text, "text", "");
|
||
SetObjectProperty(text, "fontSize", 24f);
|
||
SetObjectProperty(text, "color", new Color(0.14f, 0.31f, 0.42f, 1f));
|
||
SetObjectProperty(text, "alignment", ParseTextAlignment(text, "MidlineLeft"));
|
||
SetObjectProperty(text, "raycastTarget", false);
|
||
|
||
SetObjectProperty(input, "textViewport", areaRect);
|
||
SetObjectProperty(input, "textComponent", text);
|
||
SetObjectProperty(input, "placeholder", placeholder);
|
||
parentRect.sizeDelta = size;
|
||
}
|
||
|
||
static void CreateToggleGraphicChildren(Transform parent, HtmlNode node, Toggle toggle)
|
||
{
|
||
string checkedSpritePath = GetAttr(node, "data-sprite-checked", "");
|
||
if (string.IsNullOrEmpty(checkedSpritePath))
|
||
return;
|
||
|
||
GameObject checkGo = new GameObject("Checkmark", typeof(RectTransform));
|
||
checkGo.transform.SetParent(parent, false);
|
||
RectTransform checkRect = checkGo.GetComponent<RectTransform>();
|
||
checkRect.anchorMin = Vector2.zero;
|
||
checkRect.anchorMax = Vector2.one;
|
||
checkRect.offsetMin = Vector2.zero;
|
||
checkRect.offsetMax = Vector2.zero;
|
||
Image checkImage = checkGo.AddComponent<Image>();
|
||
checkImage.sprite = LoadSprite(checkedSpritePath);
|
||
checkImage.type = Image.Type.Simple;
|
||
toggle.graphic = checkImage;
|
||
}
|
||
|
||
[MenuItem("XWorld/Export XWorld Base Resource", false, 802)]
|
||
static public void ExportBaseRes()
|
||
{
|
||
//SetOutputPath("XWorld");
|
||
SetCurID("XWorld");
|
||
//WebGL
|
||
//BuildHotFix("XWorld", BuildTargetGroup.WebGL, BuildTarget.WebGL);
|
||
//Android
|
||
//iOS
|
||
//pc
|
||
BuildHotFix("XWorld", BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64);
|
||
}
|
||
|
||
public static void BuildHotFix(string projPath, BuildTargetGroup targetGroup, BuildTarget target, string targetName = "")
|
||
{
|
||
string sPlatform = "pc";
|
||
//bool bLuaPackage = false;
|
||
//string[] EncryptFile = { "a.unity3d", "h.unity3d", "xhotfix/base.unity3d", "StreamingAssets", "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";
|
||
}
|
||
else
|
||
{
|
||
sPlatform = "pc";
|
||
}
|
||
m_sPlatform = sPlatform;
|
||
maps.Clear();
|
||
m_mapResNameDic.Clear();
|
||
m_mapDepResNameDic.Clear();
|
||
|
||
string outPath = $"{AppDataPath}/StreamingAssets/";
|
||
if (Directory.Exists(outPath))
|
||
ClearDir(outPath);
|
||
if (!Directory.Exists(outPath))
|
||
Directory.CreateDirectory(outPath);
|
||
|
||
//lua 打包
|
||
string luaPath = $"Assets/{projPath}/lua/";
|
||
BuildLuaAB(luaPath, "bs.unity3d");
|
||
|
||
//shader
|
||
PackAllPathFile("xw_shader.unity3d", $"Assets/XWorld/shader");//"Shadervariants"
|
||
//资源打包
|
||
string[] HotfixRes = { $"art"};//游戏资源目录, 按目录打资源 //, "res"
|
||
foreach (string respath in HotfixRes)
|
||
{
|
||
string tempPath = $"{ AppDataPath}/{projPath}/{respath}/";
|
||
string buildPath = $"Assets/{projPath}/{respath}/";//路径必须以Assets开始
|
||
if (!Directory.Exists(tempPath))
|
||
{
|
||
UnityEngine.Debug.LogError($"Path {tempPath} not exist!");
|
||
continue;
|
||
}
|
||
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($"{respath}_{name}", "*.*", buildPath + pathname);
|
||
AssetDatabase.Refresh();
|
||
}
|
||
AddBuildMap($"{respath}.unity3d", "*.*", $"Assets/{projPath}/{respath}/");
|
||
//AssetDatabase.Refresh();
|
||
}
|
||
|
||
AssetDatabase.Refresh();
|
||
|
||
|
||
//打ab包
|
||
BuildPipeline.BuildAssetBundles(outPath, maps.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression/*| BuildAssetBundleOptions.DisableWriteTypeTree*/, target);
|
||
AssetDatabase.Refresh();
|
||
|
||
//生成更新列表
|
||
//BuildFileIndex(sPlatform);//StreamingAssets 里的除了\\xaot\\ 和 \\xhotfix (dll目录)外所有文件
|
||
//添加dll更新列表
|
||
m_dicFilelist.Clear();
|
||
m_strResFileMD5 = "";
|
||
//资源列表
|
||
BuildFileIndex("");
|
||
|
||
string update_files = $"{GetUpdatePath()}files.txt";
|
||
string update_ver = $"{GetUpdatePath()}updatever.txt";
|
||
|
||
//先清理之前的
|
||
if (Directory.Exists(GetUpdatePath()))
|
||
ClearDir(GetUpdatePath());
|
||
|
||
//复制到目标地址
|
||
CopyPath(outPath, GetUpdatePath());
|
||
UnityEngine.Debug.Log($"Copy Path :{outPath} => {GetUpdatePath()}");
|
||
|
||
//File.Copy($"{outPath}files.txt", $"{GetUpdatePath()}files.txt", true);
|
||
//File.Copy($"{outPath}updatever.txt", update_ver, true);
|
||
|
||
//复制热更资源
|
||
//CopyPath($"{outPath}render", $"{GetUpdatePath()}render");
|
||
//CopyPath($"{outPath}art", $"{GetUpdatePath()}art");
|
||
|
||
//加密
|
||
string[] EncryptFile = { "bs.unity3d" };//
|
||
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, true);
|
||
//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);
|
||
}
|
||
|
||
UnityEngine.Debug.Log($"Dest Path : {GetUpdatePath()}");
|
||
//if (target == BuildTarget.WebGL)
|
||
{//给文件加上md5后缀,以便方便webgl小程序使用UnityWebRequest自动缓存
|
||
//先删除旧的
|
||
string finally_update_ver = $"{GetUpdatePath()}updatever.txt";
|
||
string OldVerString;
|
||
string finally_update_files;
|
||
if (File.Exists(finally_update_ver))
|
||
{
|
||
OldVerString = File.ReadAllText(finally_update_ver);//有两个版本号
|
||
string[] vers = OldVerString.Split(',');
|
||
|
||
vers[0] = $"{GetUpdatePath()}dllfiles.txt_{vers[0]}";
|
||
vers[1] = $"{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 = $"{GetUpdatePath()}{f}_{GetFixedMd5(md5)}";
|
||
if (File.Exists(ab))
|
||
File.Delete(ab);
|
||
}
|
||
}
|
||
}
|
||
//删除旧的dllfiles.txt文件
|
||
File.Delete(vers[0]);
|
||
}
|
||
if (File.Exists(vers[1]))
|
||
{
|
||
IEnumerable<string> 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 = $"{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 = $"{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_files = $"{GetUpdatePath()}files.txt_{GetFixedMd5(m_strResFileMD5)}";
|
||
if (!File.Exists(finally_update_files))
|
||
File.Move(update_files, finally_update_files);
|
||
|
||
if (File.Exists(finally_update_ver))
|
||
File.Delete(finally_update_ver);
|
||
File.Move(update_ver, finally_update_ver);
|
||
}
|
||
//
|
||
//。。。
|
||
UnityEngine.Debug.Log($"构建完成{DateTime.Now.ToString()}");
|
||
|
||
//清理中间打包目录
|
||
//Lua
|
||
string luapath = $"{AppDataPath}/l";
|
||
ClearDir(luapath, true);
|
||
File.Delete($"{luapath}.meta");
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
static void BuildLuaAB(string luaSrcPath, string bundleName)
|
||
{
|
||
string luapath = $"{Application.dataPath}/l/";
|
||
if (Directory.Exists(luapath))
|
||
{
|
||
Directory.Delete(luapath, true);
|
||
}
|
||
//Assets下的lua目录
|
||
CopyScriptsFile(luaSrcPath, 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 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++)
|
||
{
|
||
if (!m_mapResNameDic.ContainsKey(files[i]))
|
||
{
|
||
m_mapResNameDic[files[i]] = "1";
|
||
}
|
||
files[i] = files[i].Replace('\\', '/');
|
||
}
|
||
if (bDirPack)
|
||
{
|
||
AssetBundleBuild build = new AssetBundleBuild();
|
||
build.assetBundleName = bundleName;
|
||
build.assetNames = files;
|
||
maps.Add(build);
|
||
UnityEngine.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);
|
||
UnityEngine.Debug.LogWarning($"AddBuildMap : {build.assetBundleName}({files.Length})");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
static private List<string> GetAllDependencies(string mainAssetPath)
|
||
{
|
||
List<string> result = new List<string>();
|
||
string[] depends = AssetDatabase.GetDependencies(mainAssetPath, true);
|
||
foreach (string assetPath in depends)
|
||
{
|
||
// 注意:排除主资源对象
|
||
if (assetPath != mainAssetPath)
|
||
result.Add(assetPath);
|
||
}
|
||
return result;
|
||
}
|
||
static void PackAllPathFile(string bundleName, string path, string nopath = null)
|
||
{
|
||
paths.Clear(); files.Clear();
|
||
Recursive(path);
|
||
|
||
string[] filelist = files.ToArray();
|
||
|
||
List<string> total = new List<string>(filelist);
|
||
foreach (var file in filelist)
|
||
{
|
||
if (!m_mapResNameDic.ContainsKey(file))
|
||
{
|
||
m_mapResNameDic[file] = "1";
|
||
}
|
||
}
|
||
//*
|
||
//依赖文件 //需要检查不在自己的打包路径里
|
||
foreach (var file in filelist)
|
||
{
|
||
List<string> depends = GetAllDependencies(file);
|
||
foreach (var fd in depends)
|
||
{
|
||
if (fd.EndsWith(".hlsl"))
|
||
continue;
|
||
if (!m_mapResNameDic.ContainsKey(fd))
|
||
{
|
||
m_mapDepResNameDic[fd] = "1";
|
||
}
|
||
}
|
||
}//*/
|
||
|
||
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 {total.Count} file");
|
||
}
|
||
|
||
public static void ClearDir(string path, bool bDeleteSelf = false)
|
||
{
|
||
if (!Directory.Exists(path))
|
||
return;
|
||
System.IO.DirectoryInfo di = new DirectoryInfo(path);
|
||
foreach (FileInfo file in di.EnumerateFiles())
|
||
{
|
||
file.Delete();
|
||
}
|
||
foreach (DirectoryInfo dir in di.EnumerateDirectories())
|
||
{
|
||
dir.Delete(true);
|
||
}
|
||
if (bDeleteSelf)
|
||
Directory.Delete(path);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 遍历目录及其子目录
|
||
/// </summary>
|
||
static void Recursive(string path)
|
||
{
|
||
if (path.Contains("\\xaot\\") || path.Contains("\\xhotfix\\") || !Directory.Exists(path))
|
||
{//忽略
|
||
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);
|
||
}
|
||
}
|
||
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 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(".unity3d") && value != "StreamingAssets" && value != "bs.unity3d")//去除根路径文件
|
||
continue;
|
||
sw.WriteLine($"{{{value},{md5},{size}}}");
|
||
m_dicFilelist[value] = md5;
|
||
}
|
||
sw.Close(); fs.Close();
|
||
|
||
//版本文件ver.txt
|
||
long versize = 0;
|
||
m_strResFileMD5 = GetFixedMd5(md5file(newFilePath, out versize));
|
||
string key = newFilePath.Replace(resPath, string.Empty);
|
||
m_dicFilelist[key] = m_strResFileMD5;
|
||
File.WriteAllText($"{resPath}/updatever.txt", m_strResFileMD5);
|
||
}
|
||
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 string GetFixedMd5(string md5)
|
||
{
|
||
if (md5 == null || md5.Length < MAX_FILE_HASH)
|
||
{
|
||
UnityEngine.Debug.Log($"md5 Error!");
|
||
return "Error";
|
||
}
|
||
return md5.Substring(0, MAX_FILE_HASH);//只取8位 或 n位
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
static HtmlDocumentData ParseHtmlDocument(string html)
|
||
{
|
||
HtmlDocumentData doc = new HtmlDocumentData();
|
||
doc.CssRules = ParseCssRules(html);
|
||
doc.Root = new HtmlNode { TagName = "document" };
|
||
Stack<HtmlNode> stack = new Stack<HtmlNode>();
|
||
stack.Push(doc.Root);
|
||
|
||
Regex tokenRegex = new Regex(@"<!--.*?-->|<!doctype.*?>|<(?<close>/)?(?<tag>[a-zA-Z0-9]+)(?<attrs>[^>]*)>|(?<text>[^<]+)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||
MatchCollection matches = tokenRegex.Matches(html);
|
||
foreach (Match match in matches)
|
||
{
|
||
if (match.Value.StartsWith("<!--") || match.Value.StartsWith("<!"))
|
||
continue;
|
||
|
||
if (match.Groups["text"].Success)
|
||
{
|
||
string text = match.Groups["text"].Value;
|
||
if (!string.IsNullOrWhiteSpace(text))
|
||
stack.Peek().Text += text;
|
||
continue;
|
||
}
|
||
|
||
string tag = match.Groups["tag"].Value.ToLowerInvariant();
|
||
if (tag == "style" || tag == "script")
|
||
{
|
||
if (!match.Groups["close"].Success)
|
||
stack.Push(new HtmlNode { TagName = tag, Parent = stack.Peek() });
|
||
else if (stack.Count > 1)
|
||
stack.Pop();
|
||
continue;
|
||
}
|
||
|
||
if (match.Groups["close"].Success)
|
||
{
|
||
while (stack.Count > 1 && stack.Peek().TagName != tag)
|
||
stack.Pop();
|
||
if (stack.Count > 1)
|
||
stack.Pop();
|
||
continue;
|
||
}
|
||
|
||
HtmlNode node = new HtmlNode
|
||
{
|
||
TagName = tag,
|
||
Parent = stack.Peek(),
|
||
Attributes = ParseAttributes(match.Groups["attrs"].Value)
|
||
};
|
||
stack.Peek().Children.Add(node);
|
||
|
||
if (!IsVoidTag(tag) && !match.Groups["attrs"].Value.TrimEnd().EndsWith("/"))
|
||
stack.Push(node);
|
||
}
|
||
|
||
return doc;
|
||
}
|
||
|
||
static Dictionary<string, string> ParseAttributes(string rawAttrs)
|
||
{
|
||
Dictionary<string, string> attrs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
Regex attrRegex = new Regex(@"(?<name>[a-zA-Z_:][-a-zA-Z0-9_:.]*)(\s*=\s*(?:""(?<value>[^""]*)""|'(?<value>[^']*)'|(?<value>[^\s""'>/]+)))?", RegexOptions.Singleline);
|
||
foreach (Match match in attrRegex.Matches(rawAttrs))
|
||
{
|
||
string name = match.Groups["name"].Value;
|
||
if (string.IsNullOrEmpty(name))
|
||
continue;
|
||
attrs[name] = HtmlDecode(match.Groups["value"].Value);
|
||
}
|
||
return attrs;
|
||
}
|
||
|
||
static Dictionary<string, Dictionary<string, string>> ParseCssRules(string html)
|
||
{
|
||
Dictionary<string, Dictionary<string, string>> rules = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (Match styleMatch in Regex.Matches(html, @"<style[^>]*>(?<css>.*?)</style>", RegexOptions.IgnoreCase | RegexOptions.Singleline))
|
||
{
|
||
string css = Regex.Replace(styleMatch.Groups["css"].Value, @"/\*.*?\*/", "", RegexOptions.Singleline);
|
||
foreach (Match ruleMatch in Regex.Matches(css, @"(?<selector>[^{]+)\{(?<body>[^}]*)\}", RegexOptions.Singleline))
|
||
{
|
||
string[] selectors = ruleMatch.Groups["selector"].Value.Split(',');
|
||
foreach (string selectorRaw in selectors)
|
||
{
|
||
string selector = selectorRaw.Trim();
|
||
if (!selector.StartsWith("."))
|
||
continue;
|
||
string className = selector.Substring(1).Split(new[] { ' ', ':', '.', '#' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||
if (string.IsNullOrEmpty(className))
|
||
continue;
|
||
if (!rules.TryGetValue(className, out Dictionary<string, string> body))
|
||
{
|
||
body = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
rules[className] = body;
|
||
}
|
||
foreach (string item in ruleMatch.Groups["body"].Value.Split(';'))
|
||
{
|
||
int colon = item.IndexOf(':');
|
||
if (colon <= 0)
|
||
continue;
|
||
body[item.Substring(0, colon).Trim()] = item.Substring(colon + 1).Trim();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return rules;
|
||
}
|
||
|
||
static Dictionary<string, string> GetComputedStyle(HtmlNode node, Dictionary<string, Dictionary<string, string>> cssRules)
|
||
{
|
||
Dictionary<string, string> style = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
string classAttr = GetAttr(node, "class", "");
|
||
foreach (string className in classAttr.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||
{
|
||
if (!cssRules.TryGetValue(className, out Dictionary<string, string> classStyle))
|
||
continue;
|
||
foreach (var kv in classStyle)
|
||
style[kv.Key] = kv.Value;
|
||
}
|
||
|
||
string inlineStyle = GetAttr(node, "style", "");
|
||
foreach (string item in inlineStyle.Split(';'))
|
||
{
|
||
int colon = item.IndexOf(':');
|
||
if (colon <= 0)
|
||
continue;
|
||
style[item.Substring(0, colon).Trim()] = item.Substring(colon + 1).Trim();
|
||
}
|
||
return style;
|
||
}
|
||
|
||
static HtmlNode FindUnityRoot(HtmlNode node)
|
||
{
|
||
if (GetAttr(node, "data-unity-root", "").Equals("true", StringComparison.OrdinalIgnoreCase))
|
||
return node;
|
||
foreach (HtmlNode child in node.Children)
|
||
{
|
||
HtmlNode found = FindUnityRoot(child);
|
||
if (found != null)
|
||
return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static bool IsUnityNode(HtmlNode node)
|
||
{
|
||
return HasAttr(node, "data-unity-name") && HasAttr(node, "data-unity-type");
|
||
}
|
||
|
||
static Dictionary<HtmlNode, HtmlLayout> ComputeChildLayouts(HtmlNode parentNode, RectTransform parentRect, Dictionary<string, string> parentStyle, Dictionary<string, Dictionary<string, string>> cssRules)
|
||
{
|
||
Dictionary<HtmlNode, HtmlLayout> result = new Dictionary<HtmlNode, HtmlLayout>();
|
||
List<HtmlNode> children = parentNode.Children.Where(IsUnityNode).ToList();
|
||
if (children.Count == 0)
|
||
return result;
|
||
|
||
Vector2 parentSize = parentRect != null ? parentRect.sizeDelta : Vector2.zero;
|
||
bool isFlex = GetStyle(parentStyle, "display").Equals("flex", StringComparison.OrdinalIgnoreCase);
|
||
float cursorX = 0f;
|
||
float cursorY = 0f;
|
||
float gap = ParseCssPx(GetStyle(parentStyle, "gap"), 0f);
|
||
string parentUnityType = GetAttr(parentNode, "data-unity-type", "");
|
||
|
||
foreach (HtmlNode child in children)
|
||
{
|
||
Dictionary<string, string> childStyle = GetComputedStyle(child, cssRules);
|
||
float marginLeft = ParseCssPx(GetStyle(childStyle, "margin-left"), 0f);
|
||
float marginTop = ParseCssPx(GetStyle(childStyle, "margin-top"), 0f);
|
||
float marginRight = ParseCssPx(GetStyle(childStyle, "margin-right"), 0f);
|
||
float marginBottom = ParseCssPx(GetStyle(childStyle, "margin-bottom"), 0f);
|
||
float width = GetElementWidth(child, childStyle, GetDefaultChildWidth(child, parentSize));
|
||
float height = GetElementHeight(child, childStyle, GetDefaultChildHeight(child, parentSize));
|
||
|
||
bool hasExplicitLeft = HasStyle(childStyle, "left");
|
||
bool hasExplicitTop = HasStyle(childStyle, "top");
|
||
bool hasExplicitRight = HasStyle(childStyle, "right");
|
||
bool hasExplicitBottom = HasStyle(childStyle, "bottom");
|
||
string position = GetStyle(childStyle, "position").ToLowerInvariant();
|
||
bool fillParentText = children.Count == 1
|
||
&& GetAttr(child, "data-unity-type", "") == "TextMeshProUGUI"
|
||
&& (parentUnityType == "Image" || parentUnityType == "Button" || parentUnityType == "Panel")
|
||
&& !hasExplicitLeft
|
||
&& !hasExplicitTop
|
||
&& !HasStyle(childStyle, "width")
|
||
&& !HasStyle(childStyle, "height");
|
||
|
||
float left;
|
||
float top;
|
||
if (fillParentText)
|
||
{
|
||
left = 0f;
|
||
top = 0f;
|
||
width = parentSize.x;
|
||
height = parentSize.y;
|
||
}
|
||
else if (hasExplicitLeft || hasExplicitTop || hasExplicitRight || hasExplicitBottom || position == "absolute")
|
||
{
|
||
left = hasExplicitLeft
|
||
? ParseCssPx(GetStyle(childStyle, "left"), 0f)
|
||
: (hasExplicitRight ? parentSize.x - ParseCssPx(GetStyle(childStyle, "right"), 0f) - width : 0f);
|
||
top = hasExplicitTop
|
||
? ParseCssPx(GetStyle(childStyle, "top"), 0f)
|
||
: (hasExplicitBottom ? parentSize.y - ParseCssPx(GetStyle(childStyle, "bottom"), 0f) - height : 0f);
|
||
}
|
||
else if (isFlex)
|
||
{
|
||
left = cursorX + marginLeft;
|
||
top = marginTop;
|
||
cursorX = left + width + marginRight + gap;
|
||
}
|
||
else
|
||
{
|
||
left = marginLeft;
|
||
top = cursorY + marginTop;
|
||
cursorY = top + height + marginBottom;
|
||
}
|
||
|
||
result[child] = new HtmlLayout(left, top, width, height);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
static void ApplyRectTransform(RectTransform rect, HtmlNode node, Dictionary<string, string> style, bool isRoot, HtmlLayout layoutOverride)
|
||
{
|
||
Vector2 design = GetDesignResolution(node);
|
||
rect.anchorMin = ParseVector2(GetAttr(node, "data-anchor-min", isRoot ? "0,0" : "0,1"), isRoot ? Vector2.zero : new Vector2(0, 1));
|
||
rect.anchorMax = ParseVector2(GetAttr(node, "data-anchor-max", isRoot ? "1,1" : "0,1"), isRoot ? Vector2.one : new Vector2(0, 1));
|
||
rect.pivot = ParseVector2(GetAttr(node, "data-pivot", "0.5,0.5"), new Vector2(0.5f, 0.5f));
|
||
|
||
if (isRoot)
|
||
{
|
||
rect.sizeDelta = design;
|
||
rect.anchoredPosition = Vector2.zero;
|
||
return;
|
||
}
|
||
|
||
float width = layoutOverride != null ? layoutOverride.Width : GetElementWidth(node, style, 160f);
|
||
float height = layoutOverride != null ? layoutOverride.Height : GetElementHeight(node, style, 48f);
|
||
float left = layoutOverride != null
|
||
? layoutOverride.Left
|
||
: (HasStyle(style, "right") ? design.x - ParseCssPx(GetStyle(style, "right"), 0f) - width : ParseCssPx(GetStyle(style, "left"), 0f));
|
||
float top = layoutOverride != null
|
||
? layoutOverride.Top
|
||
: (HasStyle(style, "bottom") ? design.y - ParseCssPx(GetStyle(style, "bottom"), 0f) - height : ParseCssPx(GetStyle(style, "top"), 0f));
|
||
rect.sizeDelta = new Vector2(width, height);
|
||
|
||
Vector2 anchorMin = rect.anchorMin;
|
||
Vector2 anchorMax = rect.anchorMax;
|
||
if (Approximately(anchorMin.x, anchorMax.x) && Approximately(anchorMin.y, anchorMax.y))
|
||
{
|
||
float x = left + width * rect.pivot.x - design.x * anchorMin.x;
|
||
float y = -(top + height * (1f - rect.pivot.y)) + design.y * (1f - anchorMin.y);
|
||
rect.anchoredPosition = new Vector2(x, y);
|
||
}
|
||
}
|
||
|
||
static Vector2 GetDesignResolution(HtmlNode node)
|
||
{
|
||
HtmlNode current = node;
|
||
while (current != null)
|
||
{
|
||
if (HasAttr(current, "data-design-width") && HasAttr(current, "data-design-height"))
|
||
{
|
||
return new Vector2(
|
||
ParseFloat(GetAttr(current, "data-design-width", "2048"), 2048f),
|
||
ParseFloat(GetAttr(current, "data-design-height", "1024"), 1024f));
|
||
}
|
||
current = current.Parent;
|
||
}
|
||
return new Vector2(2048, 1024);
|
||
}
|
||
|
||
static Dictionary<string, string> ParseUnityPrefabMeta(string html)
|
||
{
|
||
Dictionary<string, string> meta = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
Match match = Regex.Match(html, @"<script[^>]*id\s*=\s*[""']unity-prefab-meta[""'][^>]*>(?<json>.*?)</script>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||
if (!match.Success)
|
||
return meta;
|
||
string json = match.Groups["json"].Value;
|
||
foreach (Match prop in Regex.Matches(json, @"""(?<key>prefabName|outputPrefabPath|htmlPath|spriteRoot)""\s*:\s*""(?<value>[^""]*)"""))
|
||
meta[prop.Groups["key"].Value] = prop.Groups["value"].Value;
|
||
return meta;
|
||
}
|
||
|
||
static void ApplyCommonMetadata(GameObject go, HtmlNode node)
|
||
{
|
||
if (HasAttr(node, "data-active-default"))
|
||
go.SetActive(ParseBool(GetAttr(node, "data-active-default", "true")));
|
||
int sibling = ParseInt(GetAttr(node, "data-sibling-index", "-1"), -1);
|
||
if (sibling >= 0)
|
||
go.transform.SetSiblingIndex(sibling);
|
||
}
|
||
|
||
static Sprite LoadSprite(string path)
|
||
{
|
||
if (string.IsNullOrEmpty(path))
|
||
return null;
|
||
return AssetDatabase.LoadAssetAtPath<Sprite>(NormalizeAssetPath(path));
|
||
}
|
||
|
||
static Image.Type ParseImageType(string value)
|
||
{
|
||
switch ((value ?? "").ToLowerInvariant())
|
||
{
|
||
case "sliced":
|
||
return Image.Type.Sliced;
|
||
case "filled":
|
||
return Image.Type.Filled;
|
||
case "tiled":
|
||
return Image.Type.Tiled;
|
||
default:
|
||
return Image.Type.Simple;
|
||
}
|
||
}
|
||
|
||
static Component AddComponentByTypeName(GameObject go, string typeName)
|
||
{
|
||
Type type = FindType(typeName);
|
||
if (type == null || !typeof(Component).IsAssignableFrom(type))
|
||
return null;
|
||
return go.AddComponent(type);
|
||
}
|
||
|
||
static Type FindType(string typeName)
|
||
{
|
||
Type type = Type.GetType(typeName);
|
||
if (type != null)
|
||
return type;
|
||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||
{
|
||
type = assembly.GetType(typeName);
|
||
if (type != null)
|
||
return type;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static bool HasTmpTextInChildren(Transform parent)
|
||
{
|
||
Type textType = FindType("TMPro.TextMeshProUGUI");
|
||
if (textType == null)
|
||
return false;
|
||
return parent.GetComponentsInChildren(textType, true).Length > 0;
|
||
}
|
||
|
||
static void SetObjectProperty(object target, string propertyName, object value)
|
||
{
|
||
if (target == null || value == null)
|
||
return;
|
||
|
||
Type type = target.GetType();
|
||
var property = type.GetProperty(propertyName);
|
||
if (property != null && property.CanWrite)
|
||
{
|
||
property.SetValue(target, value, null);
|
||
return;
|
||
}
|
||
|
||
var field = type.GetField(propertyName);
|
||
if (field != null)
|
||
field.SetValue(target, value);
|
||
}
|
||
|
||
static object ParseEnumProperty(object target, string propertyName, string value)
|
||
{
|
||
var property = target.GetType().GetProperty(propertyName);
|
||
Type enumType = property != null ? property.PropertyType : null;
|
||
if (enumType == null || !enumType.IsEnum)
|
||
return null;
|
||
try
|
||
{
|
||
return Enum.Parse(enumType, value, true);
|
||
}
|
||
catch
|
||
{
|
||
return Enum.GetValues(enumType).GetValue(0);
|
||
}
|
||
}
|
||
|
||
static object ParseTmpEnum(object target, string propertyName, string value)
|
||
{
|
||
if (target == null)
|
||
return null;
|
||
var property = target.GetType().GetProperty(propertyName);
|
||
Type enumType = property != null ? property.PropertyType : null;
|
||
if (enumType == null || !enumType.IsEnum)
|
||
return null;
|
||
try
|
||
{
|
||
return Enum.Parse(enumType, value, true);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
static object ParseTextAlignment(Component textComponent, string value)
|
||
{
|
||
var property = textComponent.GetType().GetProperty("alignment");
|
||
Type enumType = property != null ? property.PropertyType : FindType("TMPro.TextAlignmentOptions");
|
||
if (enumType == null || !enumType.IsEnum)
|
||
return null;
|
||
|
||
string enumName;
|
||
switch ((value ?? "").ToLowerInvariant())
|
||
{
|
||
case "left":
|
||
enumName = "Left";
|
||
break;
|
||
case "right":
|
||
enumName = "Right";
|
||
break;
|
||
case "middle":
|
||
case "midline":
|
||
enumName = "Midline";
|
||
break;
|
||
case "midlineleft":
|
||
enumName = "MidlineLeft";
|
||
break;
|
||
case "center":
|
||
default:
|
||
enumName = "Center";
|
||
break;
|
||
}
|
||
|
||
try
|
||
{
|
||
return Enum.Parse(enumType, enumName, true);
|
||
}
|
||
catch
|
||
{
|
||
return Enum.GetValues(enumType).GetValue(0);
|
||
}
|
||
}
|
||
|
||
static object ParseFontStyle(Component textComponent, HtmlNode node, Dictionary<string, string> style)
|
||
{
|
||
var property = textComponent.GetType().GetProperty("fontStyle");
|
||
Type enumType = property != null ? property.PropertyType : FindType("TMPro.FontStyles");
|
||
if (enumType == null || !enumType.IsEnum)
|
||
return null;
|
||
|
||
bool bold = IsBoldFont(node, style);
|
||
string enumName = bold ? "Bold" : "Normal";
|
||
|
||
try
|
||
{
|
||
return Enum.Parse(enumType, enumName, true);
|
||
}
|
||
catch
|
||
{
|
||
return Enum.GetValues(enumType).GetValue(0);
|
||
}
|
||
}
|
||
|
||
static bool IsBoldFont(HtmlNode node, Dictionary<string, string> style)
|
||
{
|
||
string weight = GetAttr(node, "data-font-weight", GetStyle(style, "font-weight"));
|
||
return weight.Equals("bold", StringComparison.OrdinalIgnoreCase)
|
||
|| weight.Equals("bolder", StringComparison.OrdinalIgnoreCase)
|
||
|| ParseInt(weight, 400) >= 600;
|
||
}
|
||
|
||
static Color ParseColor(string value, Color fallback)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
return fallback;
|
||
value = value.Trim();
|
||
if (value.StartsWith("#"))
|
||
{
|
||
if (ColorUtility.TryParseHtmlString(value, out Color color))
|
||
return color;
|
||
}
|
||
Match rgba = Regex.Match(value, @"rgba?\((?<r>[\d.]+),\s*(?<g>[\d.]+),\s*(?<b>[\d.]+)(,\s*(?<a>[\d.]+))?\)");
|
||
if (rgba.Success)
|
||
{
|
||
float r = ParseFloat(rgba.Groups["r"].Value, 255f) / 255f;
|
||
float g = ParseFloat(rgba.Groups["g"].Value, 255f) / 255f;
|
||
float b = ParseFloat(rgba.Groups["b"].Value, 255f) / 255f;
|
||
float a = rgba.Groups["a"].Success ? ParseFloat(rgba.Groups["a"].Value, 1f) : 1f;
|
||
return new Color(r, g, b, a);
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
static string GetInnerText(HtmlNode node)
|
||
{
|
||
StringBuilder sb = new StringBuilder();
|
||
if (!string.IsNullOrEmpty(node.Text))
|
||
sb.Append(node.Text);
|
||
foreach (HtmlNode child in node.Children)
|
||
sb.Append(GetInnerText(child));
|
||
return sb.ToString().Trim();
|
||
}
|
||
|
||
static string GetAttr(HtmlNode node, string key, string fallback)
|
||
{
|
||
if (node != null && node.Attributes.TryGetValue(key, out string value))
|
||
return value;
|
||
return fallback;
|
||
}
|
||
|
||
static string GetFirstAttr(HtmlNode node, params string[] keys)
|
||
{
|
||
foreach (string key in keys)
|
||
{
|
||
string value = GetAttr(node, key, "");
|
||
if (!string.IsNullOrEmpty(value))
|
||
return value;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
static bool HasAttr(HtmlNode node, string key)
|
||
{
|
||
return node != null && node.Attributes.ContainsKey(key);
|
||
}
|
||
|
||
static string GetStyle(Dictionary<string, string> style, string key)
|
||
{
|
||
return style.TryGetValue(key, out string value) ? value : "";
|
||
}
|
||
|
||
static bool HasStyle(Dictionary<string, string> style, string key)
|
||
{
|
||
return style.ContainsKey(key) && !string.IsNullOrEmpty(style[key]);
|
||
}
|
||
|
||
static string GetCssTextAlign(Dictionary<string, string> style)
|
||
{
|
||
return style.TryGetValue("text-align", out string value) ? value : "Center";
|
||
}
|
||
|
||
static string GetFirstValue(HtmlNode node, Dictionary<string, string> style, string attrKey, string styleKey)
|
||
{
|
||
string attr = GetAttr(node, attrKey, "");
|
||
if (!string.IsNullOrEmpty(attr))
|
||
return attr;
|
||
return GetStyle(style, styleKey);
|
||
}
|
||
|
||
static string GetMetaValue(Dictionary<string, string> meta, string key)
|
||
{
|
||
return meta.TryGetValue(key, out string value) ? value : "";
|
||
}
|
||
|
||
static float ParseCssPx(string value, float fallback)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
return fallback;
|
||
Match match = Regex.Match(value, @"-?[\d.]+");
|
||
if (!match.Success)
|
||
return fallback;
|
||
return ParseFloat(match.Value, fallback);
|
||
}
|
||
|
||
static float GetElementWidth(HtmlNode node, Dictionary<string, string> style, float fallback)
|
||
{
|
||
if (HasStyle(style, "width"))
|
||
return ParseCssPx(GetStyle(style, "width"), fallback);
|
||
string unityType = GetAttr(node, "data-unity-type", "");
|
||
if (unityType == "TextMeshProUGUI" || unityType == "Button")
|
||
{
|
||
string text = HtmlDecode(GetAttr(node, "data-text", GetInnerText(node))).Trim();
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
float fontSize = ParseFloat(GetFirstValue(node, style, "data-font-size", "font-size"), 24f);
|
||
return EstimateTextWidth(text, fontSize, IsBoldFont(node, style));
|
||
}
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
static float GetElementHeight(HtmlNode node, Dictionary<string, string> style, float fallback)
|
||
{
|
||
if (HasStyle(style, "height"))
|
||
return ParseCssPx(GetStyle(style, "height"), fallback);
|
||
|
||
string unityType = GetAttr(node, "data-unity-type", "");
|
||
if (unityType == "TextMeshProUGUI")
|
||
{
|
||
float fontSize = ParseFloat(GetFirstValue(node, style, "data-font-size", "font-size"), 24f);
|
||
return ParseLineHeight(GetStyle(style, "line-height"), fontSize);
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
static float GetDefaultChildWidth(HtmlNode node, Vector2 parentSize)
|
||
{
|
||
string unityType = GetAttr(node, "data-unity-type", "");
|
||
if (unityType == "TextMeshProUGUI" && parentSize.x > 1f)
|
||
return parentSize.x;
|
||
return 160f;
|
||
}
|
||
|
||
static float GetDefaultChildHeight(HtmlNode node, Vector2 parentSize)
|
||
{
|
||
string unityType = GetAttr(node, "data-unity-type", "");
|
||
if (unityType == "TextMeshProUGUI" && parentSize.y > 1f)
|
||
return parentSize.y;
|
||
return 48f;
|
||
}
|
||
|
||
static float ParseLineHeight(string value, float fontSize)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
return fontSize * 1.25f;
|
||
value = value.Trim();
|
||
if (value.EndsWith("px", StringComparison.OrdinalIgnoreCase))
|
||
return ParseCssPx(value, fontSize * 1.25f);
|
||
float multiplier = ParseFloat(value, -1f);
|
||
if (multiplier > 0f && multiplier < 10f)
|
||
return fontSize * multiplier;
|
||
return fontSize * 1.25f;
|
||
}
|
||
|
||
static float EstimateTextWidth(string text, float fontSize, bool bold)
|
||
{
|
||
float units = 0f;
|
||
foreach (char c in text)
|
||
{
|
||
if (char.IsWhiteSpace(c))
|
||
units += 0.35f;
|
||
else if (c < 128)
|
||
units += 0.62f;
|
||
else
|
||
units += 1f;
|
||
}
|
||
float boldScale = bold ? 1.16f : 1f;
|
||
float padding = Mathf.Max(12f, fontSize * 0.5f);
|
||
return Mathf.Max(fontSize, Mathf.Ceil(units * fontSize * boldScale + padding));
|
||
}
|
||
|
||
static float ParseFloat(string value, float fallback)
|
||
{
|
||
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float result))
|
||
return result;
|
||
return fallback;
|
||
}
|
||
|
||
static int ParseInt(string value, int fallback)
|
||
{
|
||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result))
|
||
return result;
|
||
return fallback;
|
||
}
|
||
|
||
static bool ParseBool(string value)
|
||
{
|
||
return value != null && (value.Equals("true", StringComparison.OrdinalIgnoreCase) || value == "1");
|
||
}
|
||
|
||
static Vector2 ParseVector2(string value, Vector2 fallback)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
return fallback;
|
||
string[] parts = value.Split(',');
|
||
if (parts.Length != 2)
|
||
return fallback;
|
||
return new Vector2(ParseFloat(parts[0], fallback.x), ParseFloat(parts[1], fallback.y));
|
||
}
|
||
|
||
static bool Approximately(float a, float b)
|
||
{
|
||
return Mathf.Abs(a - b) < 0.0001f;
|
||
}
|
||
|
||
static string NormalizeAssetPath(string path)
|
||
{
|
||
if (string.IsNullOrEmpty(path))
|
||
return path;
|
||
return path.Replace('\\', '/').Trim();
|
||
}
|
||
|
||
static void EnsureAssetDirectory(string assetDir)
|
||
{
|
||
assetDir = NormalizeAssetPath(assetDir);
|
||
if (string.IsNullOrEmpty(assetDir))
|
||
return;
|
||
string fullPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, assetDir);
|
||
if (!Directory.Exists(fullPath))
|
||
Directory.CreateDirectory(fullPath);
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
static string HtmlDecode(string text)
|
||
{
|
||
if (string.IsNullOrEmpty(text))
|
||
return "";
|
||
return text
|
||
.Replace(" ", " ")
|
||
.Replace("<", "<")
|
||
.Replace(">", ">")
|
||
.Replace(""", "\"")
|
||
.Replace("'", "'")
|
||
.Replace("&", "&");
|
||
}
|
||
|
||
static bool IsVoidTag(string tag)
|
||
{
|
||
switch (tag)
|
||
{
|
||
case "area":
|
||
case "base":
|
||
case "br":
|
||
case "col":
|
||
case "embed":
|
||
case "hr":
|
||
case "img":
|
||
case "input":
|
||
case "link":
|
||
case "meta":
|
||
case "param":
|
||
case "source":
|
||
case "track":
|
||
case "wbr":
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
class HtmlDocumentData
|
||
{
|
||
public HtmlNode Root;
|
||
public Dictionary<string, Dictionary<string, string>> CssRules = new Dictionary<string, Dictionary<string, string>>();
|
||
}
|
||
|
||
class HtmlLayout
|
||
{
|
||
public float Left;
|
||
public float Top;
|
||
public float Width;
|
||
public float Height;
|
||
|
||
public HtmlLayout(float left, float top, float width, float height)
|
||
{
|
||
Left = left;
|
||
Top = top;
|
||
Width = width;
|
||
Height = height;
|
||
}
|
||
}
|
||
|
||
class HtmlNode
|
||
{
|
||
public string TagName;
|
||
public string Text = "";
|
||
public HtmlNode Parent;
|
||
public Dictionary<string, string> Attributes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
public List<HtmlNode> Children = new List<HtmlNode>();
|
||
}
|
||
}
|
||
|
||
[InitializeOnLoad]
|
||
public static class XWorldJsonPrefabFocusAutoConverter
|
||
{
|
||
static XWorldJsonPrefabFocusAutoConverter()
|
||
{
|
||
EditorApplication.focusChanged += OnFocusChanged;
|
||
}
|
||
|
||
static void OnFocusChanged(bool hasFocus)
|
||
{
|
||
if (!hasFocus)
|
||
return;
|
||
EditorApplication.delayCall -= ConvertAfterFocus;
|
||
EditorApplication.delayCall += ConvertAfterFocus;
|
||
}
|
||
|
||
static void ConvertAfterFocus()
|
||
{
|
||
XWorldUtil.AutoConvertChangedJsonPrefabsOnEditorFocus();
|
||
}
|
||
}
|