Ship the WeChat hot-update catalogs and the camp, TideWhale T2, UI atlas, and animation-lab work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,11 +9,15 @@ namespace AOT.Boot
|
||||
{
|
||||
public string AppVersion;
|
||||
public string ResVersion;
|
||||
public string PackVersion;
|
||||
public string CdnRoot;
|
||||
public string PackageName;
|
||||
public ResourcePackage Package;
|
||||
public string HttpBase;
|
||||
public string WsUrlTemplate;
|
||||
/// <summary>Hot update signals the authored home UI is ready before the boot mask is hidden.</summary>
|
||||
public bool MainUiReady;
|
||||
public string MainUiError;
|
||||
/// <summary>CDN version.json 下发的维护/公告文案(可空)。</summary>
|
||||
public string Notice;
|
||||
/// <summary>业务 UI 当前仅使用 candy;字段保留用于旧启动配置兼容。</summary>
|
||||
@@ -24,8 +28,8 @@ namespace AOT.Boot
|
||||
if (string.IsNullOrEmpty(WsUrlTemplate))
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return $"wss://game.xworld.ren/ws?pid={pid}";
|
||||
return $"wss://game.xworld.ren/ws?pid={pid}&token={UnityEngine.Networking.UnityWebRequest.EscapeURL(token)}";
|
||||
return $"wss://BuddyDefense.xworld.ren:15888/ws?pid={pid}";
|
||||
return $"wss://BuddyDefense.xworld.ren:15888/ws?pid={pid}&token={UnityEngine.Networking.UnityWebRequest.EscapeURL(token)}";
|
||||
}
|
||||
|
||||
var url = WsUrlTemplate.Replace("{pid}", pid ?? string.Empty);
|
||||
|
||||
@@ -65,10 +65,20 @@ namespace AOT.Boot
|
||||
|
||||
_view?.SetProgress(1f);
|
||||
_view?.SetStatus("热更已启动");
|
||||
Debug.Log($"[Boot] OK app={ctx.AppVersion} res={ctx.ResVersion} cdn={ctx.CdnRoot}");
|
||||
Debug.Log($"[Boot] OK app={ctx.AppVersion} res={ctx.ResVersion} pack={ctx.PackVersion} cdn={ctx.CdnRoot}");
|
||||
|
||||
// 让热更侧完成首帧生成后再撤掉全屏启动遮罩
|
||||
yield return null;
|
||||
// Prefabs/atlases load asynchronously; two frames are not a readiness guarantee.
|
||||
_view?.SetStatus("加载主界面…");
|
||||
float uiDeadline = Time.realtimeSinceStartup + 120f;
|
||||
while (!ctx.MainUiReady && string.IsNullOrEmpty(ctx.MainUiError) && Time.realtimeSinceStartup < uiDeadline)
|
||||
yield return null;
|
||||
if (!ctx.MainUiReady)
|
||||
{
|
||||
string uiError = ctx.MainUiError ?? "主界面加载超时,请检查 Art 资源包后重试";
|
||||
_view?.SetError(uiError);
|
||||
Debug.LogError("[Boot] " + uiError);
|
||||
yield break;
|
||||
}
|
||||
yield return null;
|
||||
_view?.Hide();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,19 @@ namespace AOT.Boot
|
||||
[System.Serializable]
|
||||
public class BootSettings
|
||||
{
|
||||
public const string ProductionVersionUrl = "https://www.xworld.ren/game/Friends/webgl/cdn/version.json";
|
||||
public const string ProductionCdnRoot = "https://www.xworld.ren/game/Friends/webgl/yoo/DefaultPackage";
|
||||
public const string ProductionHttpBase = "https://BuddyDefense.xworld.ren:15888";
|
||||
public const string ProductionWsTemplate = "wss://BuddyDefense.xworld.ren:15888/ws?pid={pid}&token={token}";
|
||||
|
||||
[Tooltip("壳版本,对应提审包 appVersion")]
|
||||
public string AppVersion = "1.0.0";
|
||||
|
||||
[Tooltip("CDN 总控 version.json")]
|
||||
public string VersionUrl = "https://www.xworld.ren/wechat/version.json";
|
||||
public string VersionUrl = ProductionVersionUrl;
|
||||
|
||||
[Tooltip("CDN 根(version.json 未下发 cdnRoot 时回退;含 Package 名)")]
|
||||
public string DefaultCdnRoot = "https://www.xworld.ren/wechat/1.0.0/DefaultPackage";
|
||||
public string DefaultCdnRoot = ProductionCdnRoot;
|
||||
|
||||
[Tooltip("YooAsset 默认包名")]
|
||||
public string PackageName = "DefaultPackage";
|
||||
@@ -25,10 +30,10 @@ namespace AOT.Boot
|
||||
public string HotUpdateDllLocation = "HotUpdate.Runtime";
|
||||
|
||||
[Tooltip("登录服 HTTP 根(/register /login)")]
|
||||
public string HttpBase = "https://game.xworld.ren";
|
||||
public string HttpBase = ProductionHttpBase;
|
||||
|
||||
[Tooltip("WebSocket 模板,{pid}/{token} 会被替换")]
|
||||
public string WsUrlTemplate = "wss://game.xworld.ren/ws?pid={pid}&token={token}";
|
||||
public string WsUrlTemplate = ProductionWsTemplate;
|
||||
|
||||
[Tooltip("业务 UI 当前仅支持 candy(糖果风);保留该字段兼容旧配置,其他值回退到 Candy。")]
|
||||
public string UiTheme = "candy";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace AOT.Boot
|
||||
{
|
||||
/// <summary>AOT-owned native imports; hot-update code polls events on Unity's main thread.</summary>
|
||||
[Preserve]
|
||||
public static class WebSocketBridge
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
[DllImport("__Internal")] public static extern int AIGame_WsCreate(string url);
|
||||
[DllImport("__Internal")] public static extern string AIGame_WsPoll(int id);
|
||||
[DllImport("__Internal")] public static extern void AIGame_WsSend(int id, string text);
|
||||
[DllImport("__Internal")] public static extern void AIGame_WsDispose(int id);
|
||||
#else
|
||||
public static int AIGame_WsCreate(string url) => 0;
|
||||
public static string AIGame_WsPoll(int id) => null;
|
||||
public static void AIGame_WsSend(int id, string text) { }
|
||||
public static void AIGame_WsDispose(int id) { }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcb076c623aa72042b59e4236d7ca270
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -51,6 +51,7 @@ namespace AOT.Patch
|
||||
appVersionMin = _settings.AppVersion,
|
||||
appVersionMax = _settings.AppVersion,
|
||||
resVersion = "local",
|
||||
packVersion = "local",
|
||||
cdnRoot = _settings.DefaultCdnRoot,
|
||||
packages = new[] { _settings.PackageName },
|
||||
};
|
||||
@@ -58,7 +59,7 @@ namespace AOT.Patch
|
||||
}
|
||||
else
|
||||
{
|
||||
_view?.SetStatus($"远程版本 {version.resVersion}");
|
||||
_view?.SetStatus($"远程版本 资源 {version.resVersion} 热更 {version.packVersion}");
|
||||
}
|
||||
|
||||
if (VersionInfo.CompareVersion(_settings.AppVersion, version.appVersionMin) < 0)
|
||||
@@ -72,6 +73,7 @@ namespace AOT.Patch
|
||||
{
|
||||
AppVersion = _settings.AppVersion,
|
||||
ResVersion = version.resVersion,
|
||||
PackVersion = version.packVersion,
|
||||
CdnRoot = cdnRoot,
|
||||
PackageName = _settings.PackageName,
|
||||
HttpBase = _settings.HttpBase,
|
||||
@@ -115,26 +117,15 @@ namespace AOT.Patch
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("请求资源版本…");
|
||||
_view?.SetProgress(0.35f);
|
||||
var versionOp = package.RequestPackageVersionAsync();
|
||||
yield return versionOp;
|
||||
if (versionOp.Status != EOperationStatus.Succeeded)
|
||||
yield return EnsureManifest(package, version.packVersion, "热更");
|
||||
if (_stepError != null)
|
||||
{
|
||||
onDone?.Invoke(null, $"请求 PackageVersion 失败: {versionOp.Error}");
|
||||
onDone?.Invoke(null, _stepError);
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus($"更新清单 {versionOp.PackageVersion}");
|
||||
_view?.SetProgress(0.5f);
|
||||
var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(versionOp.PackageVersion, 60));
|
||||
yield return manifestOp;
|
||||
if (manifestOp.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"更新清单失败: {manifestOp.Error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("下载资源…");
|
||||
var downloader = package.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||||
if (downloader.TotalDownloadCount > 0)
|
||||
@@ -180,18 +171,11 @@ namespace AOT.Patch
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
var companionVersion = companion.RequestPackageVersionAsync();
|
||||
yield return companionVersion;
|
||||
if (companionVersion.Status != EOperationStatus.Succeeded)
|
||||
var pinned = packageName == "Art" ? version.resVersion : null;
|
||||
yield return EnsureManifest(companion, pinned, packageName);
|
||||
if (_stepError != null)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 请求版本失败: {companionVersion.Error}");
|
||||
yield break;
|
||||
}
|
||||
var companionManifest = companion.LoadPackageManifestAsync(new LoadPackageManifestOptions(companionVersion.PackageVersion, 60));
|
||||
yield return companionManifest;
|
||||
if (companionManifest.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 更新清单失败: {companionManifest.Error}");
|
||||
onDone?.Invoke(null, _stepError);
|
||||
yield break;
|
||||
}
|
||||
var companionDownload = companion.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||||
@@ -213,6 +197,36 @@ namespace AOT.Patch
|
||||
onDone?.Invoke(ctx, null);
|
||||
}
|
||||
|
||||
string _stepError;
|
||||
|
||||
/// <summary>
|
||||
/// pinnedVersion 来自 version.json:热更用 packVersion,Art 用 resVersion。
|
||||
/// 为空时再回退到包目录里的 .version 文件。
|
||||
/// </summary>
|
||||
IEnumerator EnsureManifest(ResourcePackage package, string pinnedVersion, string label)
|
||||
{
|
||||
_stepError = null;
|
||||
var packageVersion = pinnedVersion;
|
||||
if (string.IsNullOrEmpty(packageVersion))
|
||||
{
|
||||
_view?.SetStatus($"请求{label}版本…");
|
||||
var versionOp = package.RequestPackageVersionAsync();
|
||||
yield return versionOp;
|
||||
if (versionOp.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
_stepError = $"请求{label}版本失败: {versionOp.Error}";
|
||||
yield break;
|
||||
}
|
||||
packageVersion = versionOp.PackageVersion;
|
||||
}
|
||||
|
||||
_view?.SetStatus($"更新{label}清单 {packageVersion}");
|
||||
var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(packageVersion, 60));
|
||||
yield return manifestOp;
|
||||
if (manifestOp.Status != EOperationStatus.Succeeded)
|
||||
_stepError = $"更新{label}清单失败: {manifestOp.Error}";
|
||||
}
|
||||
|
||||
bool CanContinueWithoutVersion()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -8,8 +8,11 @@ namespace AOT.Patch
|
||||
{
|
||||
public string appVersionMin = "1.0.0";
|
||||
public string appVersionMax = "1.0.0";
|
||||
/// <summary>Art 清单文件的 MD5。清单文件名为 Art_{resVersion}.bytes。</summary>
|
||||
public string resVersion = "0";
|
||||
public string cdnRoot = "https://www.xworld.ren/wechat/1.0.0";
|
||||
/// <summary>DefaultPackage 热更清单文件的 MD5。清单文件名为 DefaultPackage_{packVersion}.bytes。</summary>
|
||||
public string packVersion = "";
|
||||
public string cdnRoot = "https://www.xworld.ren/game/Friends/webgl/yoo/DefaultPackage";
|
||||
public string[] packages = { "DefaultPackage" };
|
||||
public bool forceUpdate;
|
||||
public string notice = "";
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AOT.UI
|
||||
{
|
||||
/// <summary>Offsets authored edge layouts while keeping full-screen backgrounds unchanged.</summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public sealed class UiSafeArea : MonoBehaviour
|
||||
{
|
||||
RectTransform _rect;
|
||||
Vector2 _position;
|
||||
bool _captured;
|
||||
static readonly float[] NativeInsets = new float[4];
|
||||
static int _frame = -1;
|
||||
static Vector4 _insets;
|
||||
static float _nextRead;
|
||||
static int _width, _height;
|
||||
#if UNITY_EDITOR
|
||||
static Vector4? _preview;
|
||||
public static void SetPreviewInsets(Vector4? value) { _preview = value; _frame = -1; }
|
||||
#endif
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
[DllImport("__Internal")] static extern int AIGame_GetUiSafeInsets([Out] float[] values);
|
||||
#endif
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_rect = (RectTransform)transform;
|
||||
_position = _rect.anchoredPosition;
|
||||
_captured = true;
|
||||
Apply(CurrentInsets());
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (_captured && _rect != null) _rect.anchoredPosition = _position;
|
||||
_captured = false;
|
||||
}
|
||||
|
||||
void LateUpdate() => Apply(CurrentInsets());
|
||||
|
||||
void Apply(Vector4 inset)
|
||||
{
|
||||
if (!_captured || !(_rect.parent is RectTransform parent)) return;
|
||||
Vector2 delta = Vector2.zero;
|
||||
if (Mathf.Approximately(_rect.anchorMin.y, 1) && Mathf.Approximately(_rect.anchorMax.y, 1))
|
||||
delta.y = -parent.rect.height * inset.w;
|
||||
else if (Mathf.Approximately(_rect.anchorMin.y, 0) && Mathf.Approximately(_rect.anchorMax.y, 0))
|
||||
delta.y = parent.rect.height * inset.y;
|
||||
delta.x = parent.rect.width * (inset.x - inset.z) * .5f;
|
||||
_rect.anchoredPosition = _position + delta;
|
||||
}
|
||||
|
||||
public static Vector4 NormalizeSafeArea(int width, int height, Rect safe)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || safe.width <= 0 || safe.height <= 0) return Vector4.zero;
|
||||
return Clamp(new Vector4(safe.xMin / width, safe.yMin / height,
|
||||
1f - safe.xMax / width, 1f - safe.yMax / height));
|
||||
}
|
||||
|
||||
static Vector4 CurrentInsets()
|
||||
{
|
||||
if (_frame == Time.frameCount) return _insets;
|
||||
_frame = Time.frameCount;
|
||||
#if UNITY_EDITOR
|
||||
if (_preview.HasValue) return _insets = Clamp(_preview.Value);
|
||||
#endif
|
||||
if (Time.unscaledTime < _nextRead && _width == Screen.width && _height == Screen.height) return _insets;
|
||||
_width = Screen.width; _height = Screen.height;
|
||||
_nextRead = Time.unscaledTime + .25f;
|
||||
_insets = NormalizeSafeArea(Screen.width, Screen.height, Screen.safeArea);
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
if (AIGame_GetUiSafeInsets(NativeInsets) != 0)
|
||||
_insets = Vector4.Max(_insets, Clamp(new Vector4(NativeInsets[0], NativeInsets[1], NativeInsets[2], NativeInsets[3])));
|
||||
#endif
|
||||
return _insets;
|
||||
}
|
||||
|
||||
static Vector4 Clamp(Vector4 value) => new Vector4(Valid(value.x), Valid(value.y), Valid(value.z), Valid(value.w));
|
||||
static float Valid(float value) => float.IsNaN(value) || float.IsInfinity(value) ? 0 : Mathf.Clamp(value, 0, .35f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d68be75ebbe13724cb48bfbfc3ec5a2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
<linker>
|
||||
<!-- Attached by JSON-authored UI prefabs delivered in Art bundles. -->
|
||||
<assembly fullname="AOT.Runtime">
|
||||
<type fullname="AOT.Boot.WebSocketBridge" preserve="all" />
|
||||
<type fullname="AOT.UI.UiSafeArea" preserve="all" />
|
||||
</assembly>
|
||||
<!-- YooAsset 3 creates these file systems through Activator.CreateInstance.
|
||||
Keep this separate from HybridCLRGenerate/link.xml, which Gen All replaces. -->
|
||||
<assembly fullname="YooAsset">
|
||||
<type fullname="YooAsset.WebNetworkFileSystem" preserve="nothing">
|
||||
<method signature="System.Void .ctor()" />
|
||||
</type>
|
||||
<type fullname="YooAsset.BuiltinFileSystem" preserve="nothing">
|
||||
<method signature="System.Void .ctor()" />
|
||||
</type>
|
||||
<type fullname="YooAsset.SandboxFileSystem" preserve="nothing">
|
||||
<method signature="System.Void .ctor()" />
|
||||
</type>
|
||||
</assembly>
|
||||
</linker>
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a132fe80ca665d947b84fa739de386f9
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user