Land Explorer in HomeCamp with boot loading, journal UI, and CDN publish tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-11-04 16:18:01 +08:00
co-authored by Cursor
parent 9eec364f30
commit 80678ec499
405 changed files with 48053 additions and 420302 deletions
@@ -0,0 +1,102 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using AOT.Boot;
using AOT.UI;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.TextCore.LowLevel;
namespace AIGame.EditorTools
{
public static class BuildBootLoading
{
public const string PrefabPath = "Assets/AOT/UI/UI_BootLoading.prefab";
public const string FontPath = "Assets/AOT/UI/BootChinese.asset";
const string BootScene = "Assets/Scenes/Boot.unity";
[MenuItem("AIGame/Boot/Build Loading Screen")]
public static string Build()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play Mode first.");
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
if (UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).isDirty)
throw new InvalidOperationException("Save or discard existing scene changes first.");
BuildFont();
string json = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Doc/UIPrefabCreater/UI_BootLoading.json"));
var path = JsonUIPrefabBuilder.BuildFromJsonFile(json);
if (path != PrefabPath) throw new InvalidOperationException("Loading JSON generation failed.");
var setup = EditorSceneManager.GetSceneManagerSetup();
try
{
var scene = EditorSceneManager.OpenScene(BootScene, OpenSceneMode.Single);
var driver = scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<BootDriver>(true)).Single();
var so = new SerializedObject(driver);
so.FindProperty("loadingPrefab").objectReferenceValue = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath).GetComponent<BootPatchView>();
so.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
}
finally { EditorSceneManager.RestoreSceneManagerSetup(setup); }
if (!Application.HasProLicense()) throw new InvalidOperationException("Splash configuration requires a supported Unity license.");
PlayerSettings.SplashScreen.show = false;
PlayerSettings.SplashScreen.showUnityLogo = false;
AssetDatabase.SaveAssets();
return "PASS: JSON loading prefab, static Chinese glyphs, Boot reference and splash disabled; Player not rebuilt.";
}
public static string ValidateConfigured()
{
if (PlayerSettings.SplashScreen.show) return "启动 Splash 仍开启,请先执行 AIGame/Boot/Build Loading Screen";
if (!AssetDatabase.GetDependencies(BootScene, true).Contains(PrefabPath))
return "Boot 尚未引用启动 Prefab,请先执行 AIGame/Boot/Build Loading Screen";
return null;
}
static void BuildFont()
{
if (!AssetDatabase.IsValidFolder("Assets/AOT/UI")) AssetDatabase.CreateFolder("Assets/AOT", "UI");
const string words = "萌友防线小镇营地集结伙伴·开启冒险首次进入需要下载资源请稍候正在准备…,。检查更新系统布置就绪即将出发加载失败重新游戏连接网络等待秒完成本热代码主界面包回退编辑器直通跳过补丁请求版本超时";
string characters = new string((words + new string(Enumerable.Range(32, 95).Select(i => (char)i).ToArray())).Distinct().ToArray());
var existing = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FontPath);
if (existing != null)
{
if (!existing.HasCharacters(characters))
{
// Extend the authored subset without changing its GUID or runtime mode.
existing.atlasPopulationMode = AtlasPopulationMode.Dynamic;
try
{
if (!existing.TryAddCharacters(characters, out string missingCharacters))
throw new InvalidOperationException("Boot font missing: " + missingCharacters);
}
finally { existing.atlasPopulationMode = AtlasPopulationMode.Static; }
EditorUtility.SetDirty(existing);
foreach (var texture in existing.atlasTextures) EditorUtility.SetDirty(texture);
AssetDatabase.SaveAssetIfDirty(existing);
}
return;
}
var source = AssetDatabase.LoadAssetAtPath<Font>("Assets/Bundles/UI/Font/siyuan_CN_Bold.otf");
var font = TMP_FontAsset.CreateFontAsset(source, 58, 6, GlyphRenderMode.SDFAA, 1024, 1024, AtlasPopulationMode.Dynamic, false);
if (!font.TryAddCharacters(characters, out string missing)) throw new InvalidOperationException("Boot font missing: " + missing);
font.atlasPopulationMode = AtlasPopulationMode.Static;
font.fallbackFontAssetTable = new System.Collections.Generic.List<TMP_FontAsset>();
font.name = "BootChinese";
AssetDatabase.CreateAsset(font, FontPath);
font.material.name = "BootChinese Material";
AssetDatabase.AddObjectToAsset(font.material, font);
foreach (var texture in font.atlasTextures)
{
texture.name = "BootChinese Atlas";
AssetDatabase.AddObjectToAsset(texture, font);
}
EditorUtility.SetDirty(font);
AssetDatabase.SaveAssetIfDirty(font);
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 607a67b747362dd49a9b7ffc63efc84a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -611,6 +611,12 @@ namespace AIGame.EditorTools
{
TextMeshProUGUI t = GetOrAdd<TextMeshProUGUI>(go);
ApplyDefaultFont(t);
string fontAssetPath = (string)comp["fontAsset"];
if (!string.IsNullOrEmpty(fontAssetPath))
{
t.font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontAssetPath);
if (t.font == null) throw new InvalidOperationException("TMP font asset not found: " + fontAssetPath);
}
t.text = (string)comp["text"] ?? string.Empty;
if (comp["fontSize"] != null) t.fontSize = (float)comp["fontSize"];
t.fontStyle = string.Equals((string)comp["fontStyle"], "Bold", StringComparison.OrdinalIgnoreCase) ? FontStyles.Bold : FontStyles.Normal;
@@ -11,6 +11,12 @@ namespace AIGame.EditorTools
public static class UiSpriteNineSliceRules
{
private const string CommonRoot = "Assets/Bundles/UI/Texture/Common/";
private const string JournalRoot = "Assets/Bundles/UI/Texture/Journal/";
private static readonly HashSet<string> JournalControls = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"journal_card", "journal_hero", "journal_gold_card", "journal_primary_normal", "journal_primary_pressed",
"journal_secondary_normal", "journal_secondary_pressed", "journal_disabled", "journal_progress_bg", "journal_progress_fill"
};
// W,H,L,R,T,B:设计尺寸与边界;实际切图按尺寸换算,并包含 2px 透明留边。
private static readonly Dictionary<string, int[]> Rules = new Dictionary<string, int[]>(StringComparer.OrdinalIgnoreCase)
{
@@ -46,6 +52,8 @@ namespace AIGame.EditorTools
{
if (string.IsNullOrEmpty(path)) return false;
path = path.Replace('\\', '/');
if (string.Equals(Path.GetDirectoryName(path)?.Replace('\\', '/'), JournalRoot.TrimEnd('/'), StringComparison.OrdinalIgnoreCase))
return JournalControls.Contains(Path.GetFileNameWithoutExtension(path));
return path.StartsWith(CommonRoot, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Path.GetDirectoryName(path)?.Replace('\\', '/'), CommonRoot.TrimEnd('/'), StringComparison.OrdinalIgnoreCase)
&& Rules.ContainsKey(Path.GetFileNameWithoutExtension(path));
@@ -53,6 +61,8 @@ namespace AIGame.EditorTools
public static Vector4 GetBorder(string path, int width, int height)
{
if (JournalControls.Contains(Path.GetFileNameWithoutExtension(path)))
return new Vector4(30f * width / 128, 32f * height / 128, 30f * width / 128, 28f * height / 128);
int[] rule = Rules[Path.GetFileNameWithoutExtension(path)];
float sx = Mathf.Max(1, width - 4) / (float)rule[0];
float sy = Mathf.Max(1, height - 4) / (float)rule[1];
@@ -106,6 +116,13 @@ namespace AIGame.EditorTools
importer.SaveAndReimport();
changed++;
}
foreach (string name in JournalControls)
{
var importer = AssetImporter.GetAtPath(JournalRoot + name + ".png") as TextureImporter;
if (importer == null || !Configure(importer)) continue;
importer.SaveAndReimport();
changed++;
}
return changed;
}
}