架构大更

This commit is contained in:
SoulliesOfficial
2026-03-20 11:56:50 -04:00
parent e60ef64d01
commit d09b58fd80
3663 changed files with 15232012 additions and 105579 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b5240790581fdac4aa3b2a4db00e0e9a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,576 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Continentis.Mods;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace SLSFramework.UModAssistance
{
#region List<string>
public partial class DataEditor : Editor
{
private string _pickerTargetListName;
private Dictionary<string, Object> _assetCache;
protected virtual void OnEnable()
{
// 每次选中新对象时,都清空缓存
_assetCache = new Dictionary<string, Object>();
}
protected void DrawCharacterListGUI<T>(SerializedProperty listProperty, string searchFilter = "") where T : Object
{
if (string.IsNullOrEmpty(searchFilter))
{
searchFilter = $"t:{typeof(T).Name}";
}
listProperty.isExpanded = EditorGUILayout.Foldout(listProperty.isExpanded, listProperty.displayName, false);
if (listProperty.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUI.indentLevel++;
for (int i = 0; i < listProperty.arraySize; i++)
{
SerializedProperty elementNameProp = listProperty.GetArrayElementAtIndex(i);
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(elementNameProp, GUIContent.none);
bool valueChanged = EditorGUI.EndChangeCheck();
string assetName = elementNameProp.stringValue;
if (valueChanged || !_assetCache.TryGetValue(assetName, out Object foundAsset))
{
foundAsset = FindObjectData<T>(assetName, searchFilter);
_assetCache[assetName] = foundAsset; // 将搜索结果即使是null存入缓存
}
if (foundAsset != null)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField(foundAsset, typeof(T), false, GUILayout.Width(150));
EditorGUI.EndDisabledGroup();
}
else
{
EditorGUILayout.LabelField(new GUIContent(EditorGUIUtility.IconContent("console.warnicon").image, "资产未找到或名称不匹配"),
GUILayout.Width(20));
}
if (GUILayout.Button("-", GUILayout.Width(20)))
{
_assetCache.Remove(elementNameProp.stringValue);
listProperty.DeleteArrayElementAtIndex(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button("Add by Search..."))
{
_pickerTargetListName = listProperty.propertyPath;
EditorGUIUtility.ShowObjectPicker<T>(null, false, searchFilter, GUI.skin.GetHashCode());
}
EditorGUI.indentLevel--;
}
}
protected T FindObjectData<T>(string assetName, string searchFilter) where T : Object
{
if (string.IsNullOrEmpty(assetName)) return null;
string[] guids = AssetDatabase.FindAssets($"{assetName} {searchFilter}");
if (guids.Length > 0)
{
return AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(guids[0]));
}
return null;
}
protected void HandleObjectPicker()
{
if (Event.current.commandName == "ObjectSelectorUpdated" && EditorGUIUtility.GetObjectPickerControlID() == GUI.skin.GetHashCode())
{
Object pickedObject = EditorGUIUtility.GetObjectPickerObject();
if (pickedObject != null)
{
SerializedProperty targetListProp = serializedObject.FindProperty(_pickerTargetListName);
if (targetListProp != null)
{
bool exists = false;
for (int i = 0; i < targetListProp.arraySize; i++)
{
if (targetListProp.GetArrayElementAtIndex(i).stringValue == pickedObject.name)
{
exists = true;
break;
}
}
if (!exists)
{
targetListProp.InsertArrayElementAtIndex(targetListProp.arraySize);
targetListProp.GetArrayElementAtIndex(targetListProp.arraySize - 1).stringValue = pickedObject.name;
}
}
_pickerTargetListName = null;
}
}
}
}
#endregion
#region List<string>
public partial class DataEditor
{
/// <summary>
/// (新功能) 从当前对象的另一个字段 (List<string> 或 Dictionary) 中选择
/// </summary>
/// <param name="targetListProp">要添加元素的目标 List</param>
/// <param name="sourceFieldName">数据源字段的变量名 (可以是 List<string> 或 Dictionary<string, T>)</param>
/// <param name="label">可选标签</param>
/// <param name="buttonLabel">可选按钮标签</param>
private void DrawLocalContentSelector(SerializedProperty targetListProp, string sourceFieldName, string label = null, string buttonLabel = null)
{
if (string.IsNullOrEmpty(buttonLabel)) buttonLabel = $"Add from {sourceFieldName}...";
DrawGenericSelectorButton(buttonLabel, targetListProp, () =>
{
// 数据获取逻辑:利用反射从当前对象内部查找
var targetObj = serializedObject.targetObject;
// 支持私有和公有字段
var field = targetObj.GetType().GetField(sourceFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null)
{
var value = field.GetValue(targetObj);
if (value != null)
{
// 情况 1: 它是 Dictionary (SerializableDictionary 或 普通 Dictionary)
// 我们通过反射查找 "Keys" 属性,这样兼容性最好
var keysProp = value.GetType().GetProperty("Keys");
if (keysProp != null)
{
var keys = keysProp.GetValue(value) as System.Collections.IEnumerable;
if (keys != null)
{
// 将所有 Key 转为 string 返回
return keys.Cast<object>().Select(k => k.ToString()).ToList();
}
}
// 情况 2: 它是 List<string> 或 string[]
if (value is IEnumerable<string> list)
{
return list.ToList();
}
}
}
Debug.LogWarning($"Could not find valid string list or dictionary keys in field '{sourceFieldName}'.");
return null;
}, $"Select from {sourceFieldName}");
}
/// <summary>
/// 这是一个私有的通用辅助方法,处理按钮绘制和窗口调用
/// </summary>
private void DrawGenericSelectorButton(string buttonText, SerializedProperty targetListProp, Func<List<string>> dataProvider, string windowTitle)
{
EditorGUILayout.BeginHorizontal();
// 仅仅为了排版美观的占位
// EditorGUILayout.LabelField("", GUILayout.Width(0));
if (GUILayout.Button(buttonText, GUILayout.Height(20)))
{
// 1. 获取数据 (执行传入的逻辑)
List<string> items = dataProvider?.Invoke();
if (items != null && items.Count > 0)
{
// 2. 打开通用窗口
ContentSelectorWindow.Show(windowTitle, items, (selectedItem) =>
{
// 3. 添加到列表的通用逻辑
bool alreadyExists = false;
for (int i = 0; i < targetListProp.arraySize; i++)
{
if (targetListProp.GetArrayElementAtIndex(i).stringValue == selectedItem)
{
alreadyExists = true;
break;
}
}
if (!alreadyExists)
{
targetListProp.InsertArrayElementAtIndex(targetListProp.arraySize);
targetListProp.GetArrayElementAtIndex(targetListProp.arraySize - 1).stringValue = selectedItem;
serializedObject.ApplyModifiedProperties();
}
else
{
Debug.LogWarning($"Item '{selectedItem}' already exists.");
}
});
}
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 组合绘制默认列表UI + 本地源选择按钮
/// </summary>
protected void DrawListWithLocalSelector(SerializedProperty listProp, string sourceFieldName, string label = null)
{
if (label != null) EditorGUILayout.PropertyField(listProp, new GUIContent(label), true);
else EditorGUILayout.PropertyField(listProp, true);
if (listProp.isExpanded)
{
DrawLocalContentSelector(listProp, sourceFieldName);
}
//EditorGUILayout.Space();
}
// --- 重构后的通用窗口 ---
private class ContentSelectorWindow : EditorWindow
{
private Action<string> _onSelectCallback;
private List<string> _allItems; // 数据直接从外部传入
private List<string> _filteredItems;
private string _searchString = "";
private Vector2 _scrollPos;
// Show 方法现在的签名变得更通用了,不再绑定 groupName
public static void Show(string title, List<string> items, Action<string> onSelect)
{
var window = GetWindow<ContentSelectorWindow>(true, title, true);
window.minSize = new Vector2(250, 300);
window._onSelectCallback = onSelect;
window._allItems = items; // 直接接收数据
window.FilterList();
}
private void OnGUI()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUI.BeginChangeCheck();
_searchString = GUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSearchTextField"));
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton"))) _searchString = "";
if (EditorGUI.EndChangeCheck()) FilterList();
GUILayout.EndHorizontal();
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
if (_filteredItems != null)
{
foreach (var item in _filteredItems)
{
if (GUILayout.Button(item, EditorStyles.label))
{
_onSelectCallback?.Invoke(item);
this.Close(); // 可选:点击后关闭
}
}
}
EditorGUILayout.EndScrollView();
}
private void FilterList()
{
if (string.IsNullOrEmpty(_searchString)) _filteredItems = new List<string>(_allItems);
else _filteredItems = _allItems.Where(i => i.IndexOf(_searchString, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
}
}
}
#endregion
#region Searchable Type选择器string中
public partial class DataEditor
{
private static Dictionary<Type, (string[] paths, Type[] types)> _typeCache =
new Dictionary<Type, (string[], Type[])>();
/// <summary>
/// 绘制一个带 "Select" 按钮的字段,点击后会弹出可搜索的类型选择窗口。
/// </summary>
/// <param name="classNameProp">存储类名的字符串属性</param>
/// <param name="label">在Inspector中显示的标签</param>
/// <param name="baseType">要查找的基类</param>
/// <param name="onTypeSelected">【关键】当一个类型被选中时执行的回调</param>
/// <param name="namespacePrefix">要搜索的命名空间前缀</param>
/// <param name="namespaceToRemove">要从路径中移除的命名空间部分</param>
protected void DrawSearchableTypeSelector(
SerializedProperty classNameProp,
string label,
Type baseType,
Action<Type> onTypeSelected,
string namespacePrefix = null,
string namespaceToRemove = null)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextField(label, classNameProp.stringValue);
if (GUILayout.Button("Select", GUILayout.Width(60)))
{
TypeSelectorWindow.Show(baseType, namespacePrefix, namespaceToRemove, (selectedType) =>
{
classNameProp.stringValue = selectedType.Name;
serializedObject.ApplyModifiedProperties();
onTypeSelected?.Invoke(selectedType);
});
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 弹出式搜索窗口(作为私有内部类)
/// </summary>
private class TypeSelectorWindow : EditorWindow
{
// --- 核心修改 1新的树状数据结构 ---
private class CategoryNode
{
public string Name;
public bool IsExpanded = false;
public Dictionary<string, CategoryNode> SubCategories = new Dictionary<string, CategoryNode>();
public List<(string name, Type type)> Items = new List<(string, Type)>();
}
private static Dictionary<Tuple<Type, string, string>, (string[] paths, Type[] types)> _staticTypeCache =
new Dictionary<Tuple<Type, string, string>, (string[], Type[])>();
private CategoryNode _rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
private Action<Type> _onSelectCallback;
private Type _baseType;
private string _namespacePrefix;
private string _namespaceToRemove;
private string _searchString = "";
private Vector2 _scrollPos;
private Dictionary<string, List<(string path, Type type)>> _groupedFilteredList;
private Dictionary<string, bool> _categoryFoldoutStates = new Dictionary<string, bool>();
public static void Show(Type baseType, string namespacePrefix, string namespaceToRemove, Action<Type> onSelect)
{
var window = GetWindow<TypeSelectorWindow>(true, "Select Type", true);
window.minSize = new Vector2(300, 400);
window._baseType = baseType;
window._namespacePrefix = namespacePrefix;
window._namespaceToRemove = namespaceToRemove;
window._onSelectCallback = onSelect;
window.FilterList();
}
private void OnGUI()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUI.BeginChangeCheck();
_searchString = GUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSearchTextField"));
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton"))) _searchString = "";
if (EditorGUI.EndChangeCheck())
{
FilterList();
}
GUILayout.EndHorizontal();
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
if (_rootNode.SubCategories.Count == 0 && _rootNode.Items.Count == 0)
{
EditorGUILayout.LabelField("No matching types found.");
}
else
{
DrawNodeGUI(_rootNode, 0);
}
EditorGUILayout.EndScrollView();
}
// --- 核心修改 1重写 CacheDerivedTypes使其更健壮 ---
private void CacheDerivedTypes()
{
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
if (_staticTypeCache.ContainsKey(cacheKey)) return;
List<(string path, Type type)> typeList = new List<(string, Type)>();
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(t => _baseType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && t != _baseType);
foreach (var type in types)
{
string path;
if (!string.IsNullOrEmpty(_namespacePrefix) && type.Namespace != null && type.Namespace.StartsWith(_namespacePrefix))
{
// 1. 获取前缀后的命名空间
// e.g., ".Basic.Cards.General.Skills"
string formattedNamespace = type.Namespace.Substring(_namespacePrefix.Length);
// 2. 按 '.' 拆分为段落,并移除空条目 (比如开头的那个)
List<string> segments = formattedNamespace.Split('.').Where(s => !string.IsNullOrEmpty(s)).ToList();
// 3. 安全地移除指定的 'namespaceToRemove' 段落
if (!string.IsNullOrEmpty(_namespaceToRemove))
{
segments.Remove(_namespaceToRemove);
}
// 4. 用 '/' 重新组合路径
if (segments.Count > 0)
{
path = string.Join("/", segments) + "/" + type.Name;
}
else
{
path = type.Name; // 如果没有剩余段落,则直接使用类名
}
}
else
{
path = "Uncategorized/" + type.Name;
}
typeList.Add((path, type));
}
typeList.Sort((a, b) => a.path.CompareTo(b.path));
_staticTypeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
}
private void FilterList()
{
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
if (!_staticTypeCache.ContainsKey(cacheKey))
{
CacheDerivedTypes();
}
var (allPaths, allTypes) = _staticTypeCache[cacheKey];
// 1. 重置根节点
_rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
for (int i = 0; i < allPaths.Length; i++)
{
string fullPath = allPaths[i];
Type type = allTypes[i];
// 2. 检查是否匹配搜索词
if (string.IsNullOrEmpty(_searchString) ||
fullPath.IndexOf(_searchString, StringComparison.OrdinalIgnoreCase) >= 0)
{
var segments = fullPath.Split('/');
CategoryNode currentNode = _rootNode;
// 3. 遍历所有“目录”部分,构建树
for (int j = 0; j < segments.Length - 1; j++)
{
string categoryName = segments[j];
if (!currentNode.SubCategories.ContainsKey(categoryName))
{
var newNode = new CategoryNode { Name = categoryName };
// 如果在搜索,自动展开所有匹配路径上的节点
if (!string.IsNullOrEmpty(_searchString))
{
newNode.IsExpanded = true;
}
currentNode.SubCategories[categoryName] = newNode;
}
currentNode = currentNode.SubCategories[categoryName];
}
// 4. 将“文件”部分(即类名)添加到它所属的节点
string itemName = segments.Last();
currentNode.Items.Add((itemName, type));
}
}
}
// --- 核心修改 4全新的递归绘制方法 ---
private void DrawNodeGUI(CategoryNode node, int indentLevel)
{
// 1. 绘制所有子类别作为可折叠的Foldouts
foreach (var subCategory in node.SubCategories.Values.OrderBy(c => c.Name))
{
// 设置当前层级的缩进
EditorGUI.indentLevel = indentLevel;
// 使用我们自定义的、带三角箭头的粗体样式
subCategory.IsExpanded = EditorGUILayout.Foldout(subCategory.IsExpanded, subCategory.Name, true);
if (subCategory.IsExpanded)
{
// 递归调用,绘制子节点的内容,缩进+1
DrawNodeGUI(subCategory, indentLevel + 1);
}
}
EditorGUI.indentLevel = indentLevel;
foreach (var (name, type) in node.Items.OrderBy(i => i.name))
{
// B. 从布局系统中获取一个**已经正确缩进**的矩形区域
Rect controlRect = EditorGUILayout.GetControlRect();
// C. 在这个矩形区域内绘制标签,它会自动使用我们设置的缩进
// 我们使用 _clickableLabelStyle 来实现悬停变色效果
EditorGUI.LabelField(controlRect, name);
// D. 为这个矩形区域添加一个可点击的光标提示
EditorGUIUtility.AddCursorRect(controlRect, MouseCursor.Link);
// E. 手动检查在这个矩形区域内的点击事件
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 &&
controlRect.Contains(Event.current.mousePosition))
{
_onSelectCallback?.Invoke(type);
this.Close();
Event.current.Use(); // 消耗掉这个点击事件
}
}
}
}
}
#endregion
#region
public partial class DataEditor
{
protected void DrawMethodButton<T>(string buttonLabel, string functionName) where T : Object
{
if (GUILayout.Button(buttonLabel))
{
T invoker = target as T;
MethodInfo method = typeof(T).GetMethod(functionName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
method.Invoke(invoker, null);
}
else
{
Debug.LogWarning($"Method '{functionName}' not found in type '{typeof(T).Name}'.");
}
}
}
}
#endregion
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0cc91a8081a9a9f44ab0df9fe725a089

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Continentis.MainGame;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using Continentis.MainGame.Equipment;
using Continentis.MainGame.Rules;
using Continentis.Mods;
using I2.Loc;
using SLSFramework.General;
using UMod;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace SLSFramework.UModAssistance
{
public partial class ModBrowser : MonoBehaviour
{
#region Inspector
public UnityEvent OnModLoadBegin;
public UnityEvent OnModLoadEnd;
#endregion
// Public
public bool persistent = true;
public Button loadButton;
public RectTransform modButtonContainer;
public GameObject modLoadTabPrefab;
public List<ModLoadTab> modTabs = new List<ModLoadTab>();
public List<IModInfo> selectedMods = new List<IModInfo>();
private void Awake()
{
#if UNITY_EDITOR
Mod.DefaultDirectory = new ModDirectory(Application.dataPath + "/ExportedMods");
#else
Mod.DefaultDirectory = new ModDirectory(Application.dataPath + "/Mods");
#endif
loadButton.onClick.AddListener(OnLoadClicked);
GenerateUIList();
}
}
public partial class ModBrowser
{
private async void OnLoadClicked()
{
OnModLoadBegin?.Invoke();
GetAllSelectedMods();
foreach (IModInfo mod in selectedMods)
{
ModHost host = await ModManager.LoadAsync(mod);
ModManager.RegisterTypesFromMod(host, typeof(RulesCollectionBase));
ModManager.RegisterTypesFromMod(host, typeof(CharacterLogicBase));
ModManager.RegisterTypesFromMod(host, typeof(CardLogicBase));
ModManager.RegisterTypesFromMod(host, typeof(EquipmentBase));
ModManager.RegisterTypesFromMod(host, typeof(CardCombatBuffBase));
ModManager.RegisterTypesFromMod(host, typeof(CharacterCombatBuffBase));
string manifestName = host.CurrentMod.NameInfo.ModName + "_Manifest";
ModManifest manifest = host.Assets.Load<ModManifest>(manifestName);
manifest.SaveToDatabase(host);
List<TextAsset> localizationFiles = manifest.localizationFiles;
foreach (TextAsset localizationFile in localizationFiles)
{
LanguageSourceData sourceData = new LanguageSourceData();
sourceData.Import_CSV(string.Empty, localizationFile.text, eSpreadsheetUpdateMode.Merge, ',');
LocalizationManager.AddSource(sourceData);
}
}
LocalizationManager.LocalizeAll();
OnModLoadEnd?.Invoke();
}
private void GenerateUIList()
{
// Destroy all cells
modButtonContainer.DestroyAllChildren();
// Create new cells
foreach (IModInfo info in Mod.DefaultDirectory.GetMods())// ModDirectory.GetMods())
{
CreateUICell(info, modButtonContainer);
}
}
private void CreateUICell(IModInfo mod, RectTransform container)
{
ModLoadTab modTab = Instantiate(modLoadTabPrefab, container).GetComponent<ModLoadTab>();
modTab.Initialize(mod);
modTabs.Add(modTab);
}
private void GetAllSelectedMods()
{
selectedMods = modTabs.FindAll(t => t.isSelected).ConvertAll(t => t.modInfo);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 222420d8b6807ad458d012a439512436

View File

@@ -0,0 +1,31 @@
using System;
using TMPro;
using UMod;
using UnityEngine;
using UnityEngine.UI;
namespace SLSFramework.UModAssistance
{
public class ModLoadTab : MonoBehaviour
{
public IModInfo modInfo;
public string path;
public bool isSelected => loadToggle.isOn;
public TMP_Text nameText;
public TMP_Text versionText;
public TMP_Text pathText;
public Toggle loadToggle;
public void Initialize(IModInfo modInfo)
{
this.modInfo = modInfo;
string relative = Mod.DefaultDirectory.GetModPath(modInfo.NameInfo.ModName).ToString();
path = relative.Replace(Application.dataPath + "/", "");
nameText.text = modInfo.NameInfo.ModName;
versionText.text = modInfo.NameInfo.ModVersion;
pathText.text = path;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f0ea9bf78368bff43b7f5145f22492c3

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SLSFramework.General;
using UMod;
using UMod.Scripting;
using UnityEngine;
using Object = UnityEngine.Object;
namespace SLSFramework.UModAssistance
{
public static partial class ModManager
{
public static ModHost LoadMod(IModInfo modInfo)
{
string modName = modInfo.NameInfo.ModName;
ModHost host = Mod.Load(Mod.DefaultDirectory.GetModPath(modName));
LoadedMods.Add(modName, host);
Debug.Log($"Mod '{modName}' loaded successfully.");
return host;
}
public static async Task<ModHost> LoadAsync(IModInfo modInfo)
{
string modName = modInfo.NameInfo.ModName;
var host = Mod.LoadAsync(Mod.DefaultDirectory.GetModPath(modName));
while (!host.IsDone)
{
await Task.Yield();
}
if (!host.IsSuccessful)
{
Debug.LogError($"[ModLoader] Mod load operation for '{modName}' failed (hostTask.IsSuccessful == false).");
throw new Exception($"Failed to load mod '{modName}' asynchronously (Operation Failed)");
}
ModHost modHost = host.Result;
if (modHost == null || !modHost.IsModLoaded)
{
if (modHost != null)
{
var errorMessage = modHost.LoadResult.Message;
Debug.LogError($"[ModLoader] Mod load operation for '{modName}' failed: {errorMessage}");
}
throw new Exception($"Failed to load mod '{modName}' asynchronously (ModHost is null or not loaded)");
}
LoadedMods.Add(modName, host.Result);
Debug.Log($"Mod '{modName}' async loaded successfully.");
return modHost;
}
}
public static partial class ModManager
{
public static readonly SerializableDictionary<string, ModHost> LoadedMods = new SerializableDictionary<string, ModHost>();
public static readonly Dictionary<Type, Dictionary<string, ScriptableObject>> Database = new Dictionary<Type, Dictionary<string, ScriptableObject>>();
/// <summary>
/// Get the standardized class name for a mod class, combining its class name and mod name.
/// Format: "ModName_ClassName"
/// </summary>
public static string GetModClassName(Type type)
{
string modName = type.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
string className = type.Name;
return $"{modName}_{className}";
}
public static bool IsValidAssetName(string assetName) => Regex.IsMatch(assetName, @"^\w+_\w+_.+$");
/// <summary>
/// Get asset by its name, automatically determining which mod it belongs to.
/// </summary>
/// <param name="assetName">Name of the asset <b>MUST</b> in the format "Type_ModName_AssetName"</param>
public static T GetAsset<T>(string assetName) where T : Object
{
//命名符合“Type_ModName_AssetName”格式规范
if (IsValidAssetName(assetName))
{
string assumeModName = assetName.Split('_')[1];
if (LoadedMods.TryGetValue(assumeModName, out ModHost host))
{
T asset = host.Assets.Load<T>(assetName);
if (asset != null)
{
return asset;
}
}
Debug.LogWarning($"Mod '{assumeModName}' is not loaded, or cannot get asset '{assetName}'.");
}
else
{
Debug.LogError($"Please check the name format (Type_ModName_AssetName) of this asset, '{assetName}'.");
}
return null;
}
/// <summary>
/// Get asset from specified mod.
/// </summary>
/// <param name="modName">Name of the mod</param>
/// <param name="assetName">Name of the asset, recommend name format "Type_ModName_AssetName"</param>
public static T GetAsset<T>(string modName, string assetName) where T : Object
{
if (!IsValidAssetName(assetName))
{
Debug.LogWarning($"Asset name '{assetName}' does not follow the 'Type_ModName_AssetName' format.");
}
if (LoadedMods.TryGetValue(modName, out ModHost host))
{
return host.Assets.Load<T>(assetName);
}
Debug.LogWarning($"Mod '{modName}' is not loaded, or cannot get asset '{assetName}'.");
return null;
}
public static bool TryGetData<T>(string assetName, out T data) where T : ScriptableObject
{
return (data = GetData<T>(assetName)) != null;
}
/// <summary>
/// Get data (Scriptable Objects, loaded by data manifest) from the database by its name and type.
/// </summary>
/// <param name="assetName">Name of the asset</param>
public static T GetData<T>(string assetName) where T : ScriptableObject
{
if (!IsValidAssetName(assetName))
{
Debug.LogWarning($"Asset name '{assetName}' does not follow the 'Type_ModName_AssetName' format.");
}
Type assetType = typeof(T);
if (Database.TryGetValue(assetType, out Dictionary<string, ScriptableObject> assets))
{
if (assets.TryGetValue(assetName, out ScriptableObject asset))
{
return asset as T;
}
else
{
Debug.LogWarning($"Data Asset '{assetName}' of type '{assetType}' not found in database.");
return null;
}
}
else
{
Debug.LogWarning($"No assets of type '{assetType}' found in database.");
return null;
}
}
}
public partial class ModManager
{
public static readonly Dictionary<string, Type> TypeRegistry = new Dictionary<string, Type>();
public static string GetTypeID(Type type)
{
return type.Namespace!.Replace("Continentis.Mods.", "") + "." + type.Name;
}
public static string GetTypeID(string modName, string classification, string category, string className)
{
if (string.IsNullOrEmpty(category))
{
return $"{modName}.{classification}.{className}";
}
return $"{modName}.{classification}.{category}.{className}";
}
/// <summary>
/// 从一个已加载的Mod中查找所有指定基类的子类并将其注册到全局字典中。
/// </summary>
/// <param name="host">已加载的ModHost对象</param>
/// <param name="baseType">要查找的基类,例如 typeof(CardLogicBase)</param>
public static void RegisterTypesFromMod(ModHost host, Type baseType)
{
if (host?.ScriptDomain == null)
{
return;
}
int countBefore = TypeRegistry.Count;
// 遍历Mod包含的所有程序集(DLLs)
foreach (ScriptAssembly assembly in host.ScriptDomain.Assemblies)
{
// assembly.RawAssembly 是 uMod 封装的真实 System.Reflection.Assembly 对象
var typesInAssembly = assembly.RawAssembly.GetTypes()
.Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface);
foreach (var type in typesInAssembly)
{
string typeID = GetTypeID(type);
if (TypeRegistry.TryAdd(typeID, type))
{
Debug.Log($"Registered script type '{typeID}' from mod '{host.CurrentMod.NameInfo.ModName}'.");
}
else
{
// 处理命名冲突如果不同Mod中存在同名的类后加载的会被忽略
Debug.LogWarning($"Duplicate script type name found: '{typeID}'. The existing type from assembly '{TypeRegistry[type.Name].Assembly.FullName}' will be kept.");
}
}
}
int countAfter = TypeRegistry.Count;
if (countAfter > countBefore)
{
Debug.Log($"Registered {countAfter - countBefore} new script types deriving from '{baseType.Name}' from mod '{host.CurrentMod.NameInfo.ModName}'.");
}
}
public static Type GetType(string typeID)
{
if (TypeRegistry.TryGetValue(typeID, out Type type))
{
return type;
}
Debug.LogWarning($"Type '{typeID}' not found in TypeRegistry.");
return null;
}
public static T CreateInstance<T>(string typeName) where T : class
{
Type type = GetType(typeName);
if (type != null && typeof(T).IsAssignableFrom(type))
{
return Activator.CreateInstance(type) as T;
}
Debug.LogWarning($"Cannot create instance of type '{typeName}' as it is not found or not assignable to '{typeof(T).Name}'.");
return null;
}
public static T CreateInstance<T>(string typeName, params object[] parameters) where T : class
{
Type type = GetType(typeName);
if (type != null && typeof(T).IsAssignableFrom(type))
{
return Activator.CreateInstance(type, parameters) as T;
}
Debug.LogWarning($"Cannot create instance of type '{typeName}' as it is not found or not assignable to '{typeof(T).Name}'.");
return null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0d2fb7a334e649f4ba61fc074840774d