chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
#endif
namespace BaseGames.Editor
{
/// <summary>
/// Editor 工具:验证 <see cref="BaseGames.Core.Assets.AddressKeys"/> 中所有常量
/// 是否与 Addressable 分组中实际存在的地址同步(架构 13_AssetPoolModule §10
///
/// 菜单BaseGames → Tools → Validate Address Keys
/// </summary>
public static class AddressKeyValidator
{
[MenuItem("BaseGames/Tools/Validate Address Keys")]
public static void ValidateAll()
{
var results = RunValidation();
LogResults(results);
}
/// <summary>
/// 执行验证,返回每个 key 的验证结果。供 Build Pre-process 或测试调用。
/// </summary>
public static List<ValidationResult> RunValidation()
{
var results = new List<ValidationResult>();
var registeredAddresses = GetAllAddressableAddresses();
// 通过反射取出 AddressKeys 中所有 public const string 字段
var keyType = typeof(BaseGames.Core.Assets.AddressKeys);
var fields = keyType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string));
foreach (var field in fields)
{
var value = (string)field.GetRawConstantValue();
var exists = registeredAddresses.Contains(value);
results.Add(new ValidationResult(field.Name, value, exists));
}
return results;
}
// ── Internal ──────────────────────────────────────────────────────────
private static HashSet<string> GetAllAddressableAddresses()
{
var addresses = new HashSet<string>(StringComparer.Ordinal);
#if UNITY_EDITOR
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogWarning("[AddressKeyValidator] Addressable Settings 未找到,请先初始化 Addressables。");
return addresses;
}
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var entry in group.entries)
{
if (entry != null)
addresses.Add(entry.address);
}
}
#endif
return addresses;
}
private static void LogResults(List<ValidationResult> results)
{
int missing = 0;
foreach (var r in results)
{
if (!r.ExistsInAddressables)
{
Debug.LogWarning($"[AddressKeyValidator] ⚠ 孤儿 KeyAddressKeys.{r.FieldName} = \"{r.Value}\" — 未在 Addressable 分组中找到对应地址。");
missing++;
}
}
if (missing == 0)
Debug.Log($"[AddressKeyValidator] ✓ 所有 {results.Count} 个 AddressKeys 常量均在 Addressable 分组中存在。");
else
Debug.LogWarning($"[AddressKeyValidator] 共 {results.Count} 个常量,发现 {missing} 个孤儿 Key请检查 Addressable 分组配置。");
}
// ── 结果结构 ──────────────────────────────────────────────────────────
public readonly struct ValidationResult
{
public readonly string FieldName;
public readonly string Value;
public readonly bool ExistsInAddressables;
public ValidationResult(string fieldName, string value, bool exists)
{
FieldName = fieldName;
Value = value;
ExistsInAddressables = exists;
}
}
}
/// <summary>
/// 资产导入后自动触发 AddressKey 验证(架构 13_AssetPoolModule §10
/// 仅在 Addressable Group 资产发生变更时触发,避免每次导入都验证。
/// </summary>
public class AddressKeyImportWatcher : AssetPostprocessor
{
private const string AddressableGroupAssetExt = ".asset";
private const string AddressableGroupFolder = "Assets/AddressableAssetsData";
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
bool addressablesChanged = false;
foreach (var path in importedAssets)
{
if (path.StartsWith(AddressableGroupFolder, StringComparison.OrdinalIgnoreCase)
&& path.EndsWith(AddressableGroupAssetExt, StringComparison.OrdinalIgnoreCase))
{
addressablesChanged = true;
break;
}
}
if (!addressablesChanged)
{
foreach (var path in deletedAssets)
{
if (path.StartsWith(AddressableGroupFolder, StringComparison.OrdinalIgnoreCase))
{
addressablesChanged = true;
break;
}
}
}
if (addressablesChanged)
{
// 延迟一帧执行,等待 AssetDatabase 完全刷新
EditorApplication.delayCall += () =>
{
Debug.Log("[AddressKeyImportWatcher] 检测到 Addressable 分组变更,自动触发 Key 验证...");
AddressKeyValidator.ValidateAll();
};
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d43f71baaed2b5443a8c9f05f0a8e441
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Editor",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Editor",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"Unity.Addressables",
"Unity.Addressables.Editor",
"BaseGames.Core.Save",
"BaseGames.Input",
"BaseGames.Combat",
"BaseGames.Player",
"BaseGames.Enemies",
"BaseGames.World",
"BaseGames.UI",
"BaseGames.Audio",
"BaseGames.Feedback",
"BaseGames.Dialogue",
"BaseGames.Progression"
],
"autoReferenced": false,
"overrideReferences": false,
"includePlatforms": [
"Editor"
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0cb7f1698076b424bbbf87d8789ca0ed
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,141 @@
using UnityEngine;
using UnityEditor;
using BaseGames.Core.Events;
using BaseGames.Combat;
using BaseGames.Player;
using BaseGames.Dialogue;
using BaseGames.Progression;
using System.IO;
namespace BaseGames.Editor
{
/// <summary>
/// Editor 工具:一键在 Assets/Data/Events/ 下生成所有全局事件频道 .asset 资产。
/// 菜单BaseGames → Tools → Create Event Channel Assets
/// 已存在的资产会自动跳过(幂等)。
/// </summary>
public static class CreateEventChannelAssets
{
private const string RootPath = "Assets/Data/Events";
[MenuItem("BaseGames/Tools/Create Event Channel Assets")]
public static void CreateAll()
{
// ── Core 原始类型频道 ──────────────────────────────────────────────
CreateAsset<VoidEventChannelSO> ("Core", "EVT_Void");
CreateAsset<BoolEventChannelSO> ("Core", "EVT_Bool");
CreateAsset<IntEventChannelSO> ("Core", "EVT_Int");
CreateAsset<FloatEventChannelSO> ("Core", "EVT_Float");
CreateAsset<StringEventChannelSO> ("Core", "EVT_String");
CreateAsset<Vector2EventChannelSO> ("Core", "EVT_Vector2");
CreateAsset<TransformEventChannelSO> ("Core", "EVT_Transform");
CreateAsset<GameStateEventChannelSO> ("Core", "EVT_GameState");
CreateAsset<SceneLoadRequestEventChannelSO>("Core", "EVT_SceneLoadRequest");
// ── 难度 ──────────────────────────────────────────────────────────
CreateAsset<DifficultyChangedEventChannel>("Difficulty", "EVT_DifficultyChanged");
// ── 战斗 ──────────────────────────────────────────────────────────
CreateAsset<HitConfirmedEventChannelSO> ("Combat", "EVT_HitConfirmed");
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_PlayerDied");
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_EnemyDied");
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_ParrySuccess");
CreateAsset<VoidEventChannelSO> ("Combat", "EVT_PlayerRespawn");
// ── Boss ──────────────────────────────────────────────────────────
CreateAsset<BossSkillEventChannelSO> ("Boss", "EVT_BossSkill");
CreateAsset<BossPhaseEventChannelSO> ("Boss", "EVT_BossPhase");
CreateAsset<StatusEffectEventChannelSO> ("Boss", "EVT_StatusEffect");
// ── 任务 ──────────────────────────────────────────────────────────
CreateAsset<QuestStateChangedEventChannel>("Quest", "EVT_QuestStateChanged");
CreateAsset<QuestObjectiveEventChannelSO> ("Quest", "EVT_QuestObjective");
// ── UI ────────────────────────────────────────────────────────────
CreateAsset<VoidEventChannelSO> ("UI", "EVT_PauseRequested");
CreateAsset<VoidEventChannelSO> ("UI", "EVT_PauseResumed");
CreateAsset<ColorblindModeEventChannelSO> ("UI", "EVT_ColorblindMode");
// ── 对话/商店 ─────────────────────────────────────────────────────
CreateAsset<ShopPurchaseEventChannelSO> ("Dialogue", "EVT_ShopPurchase");
CreateAsset<DialogueEventChannelSO> ("Dialogue", "EVT_DialogueStartRequest");
CreateAsset<VoidEventChannelSO> ("Dialogue", "EVT_DialogueEnded");
// ── 玩家能力 ──────────────────────────────────────────────────────
CreateAsset<AbilityTypeEventChannelSO> ("Player", "EVT_AbilityUnlocked");
CreateAsset<StringEventChannelSO> ("Player", "EVT_AbilityUnlockedStr");
// ── 音频 ──────────────────────────────────────────────────────────
CreateAsset<StringEventChannelSO> ("Audio", "EVT_BGMRequest");
CreateAsset<VoidEventChannelSO> ("Audio", "EVT_BGMStop");
// ── 进度/成就 ─────────────────────────────────────────────────────
CreateAsset<ToolUsedEventChannelSO> ("Progression", "EVT_ToolUsed");
CreateAsset<AchievementEventChannelSO> ("Progression", "EVT_AchievementUnlocked");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[CreateEventChannelAssets] 所有事件频道资产生成完毕。");
}
[MenuItem("BaseGames/Tools/Reimport Event Channel Assets")]
public static void ReimportAllEventAssets()
{
if (!AssetDatabase.IsValidFolder(RootPath))
{
Debug.LogWarning($"[CreateEventChannelAssets] 未找到目录: {RootPath}");
return;
}
string absoluteRoot = Path.Combine(Directory.GetCurrentDirectory(), RootPath);
string[] files = Directory.GetFiles(absoluteRoot, "*.asset", SearchOption.AllDirectories);
int count = 0;
foreach (string file in files)
{
string relativePath = "Assets" + file.Replace('\\', '/').Substring(Directory.GetCurrentDirectory().Length);
AssetDatabase.ImportAsset(relativePath, ImportAssetOptions.ForceUpdate);
count++;
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"[CreateEventChannelAssets] 已重导入 {count} 个事件资产。");
}
private static void CreateAsset<T>(string subfolder, string assetName) where T : ScriptableObject
{
string folderPath = $"{RootPath}/{subfolder}";
EnsureDirectory(folderPath);
string fullPath = $"{folderPath}/{assetName}.asset";
if (AssetDatabase.LoadAssetAtPath<T>(fullPath) != null)
{
Debug.Log($"[CreateEventChannelAssets] 已跳过(已存在): {fullPath}");
return;
}
T asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, fullPath);
Debug.Log($"[CreateEventChannelAssets] 已创建: {fullPath}");
}
/// <summary>递归创建所有缺失的中间文件夹(兼容 AssetDatabase。</summary>
private static void EnsureDirectory(string path)
{
if (AssetDatabase.IsValidFolder(path))
return;
string[] parts = path.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = $"{current}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c6f33a6c3ce1f6469e1a2ac17c95b6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Editor { }

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efa269d575c20564089e0ae4009e7157
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: