This commit is contained in:
SoulliesOfficial
2025-10-23 00:49:44 -04:00
parent 9b1b5ca93f
commit 61a397dd4c
9846 changed files with 2618439 additions and 793547 deletions

View File

@@ -1,107 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace SoulliesFramework.General
{
/// <summary>
/// 静态的Addressables资源加载器负责加载和释放资源。
/// 警告:此类不处理依赖关系,仅管理通过它加载的资源的生命周期。
/// </summary>
public static class AssetLoader
{
// 使用字典缓存已加载资源的句柄Key为资源的Address。
// 这样可以防止同一资源被重复加载,并方便我们后续释放。
private static Dictionary<string, AsyncOperationHandle> CachedHandles = new Dictionary<string, AsyncOperationHandle>();
public static List<T> LoadAssetsWithLabel<T>(string label) where T : Object
{
return Addressables.LoadAssetsAsync<T>(label, null).WaitForCompletion().ToList();
}
public static Task<IList<T>> LoadAssetsWithLabelAsync<T>(string label) where T : Object
{
return Addressables.LoadAssetsAsync<T>(label, null).Task;
}
public static T LoadAsset<T>(string address) where T : Object
{
return Addressables.LoadAssetAsync<T>(address).WaitForCompletion();
}
/// <summary>
/// 异步加载指定类型的资源。
/// </summary>
/// <typeparam name="T">要加载的资源类型 (e.g., GameObject, Sprite, AudioClip)</typeparam>
/// <param name="address">资源的Addressable地址</param>
/// <returns>返回加载到的资源如果失败则返回null</returns>
public static async Task<T> LoadAssetAsync<T>(string address) where T : Object
{
// 1. 检查句柄是否已经被缓存
if (CachedHandles.TryGetValue(address, out var handle))
{
// 如果句柄存在,直接返回其结果
return handle.Result as T;
}
// 2. 如果没有缓存,则异步加载资源
AsyncOperationHandle<T> newHandle = Addressables.LoadAssetAsync<T>(address);
// 3. 等待加载完成
T result = await newHandle.Task;
// 4. 检查加载是否成功,如果成功则缓存句柄
if (newHandle.Status == AsyncOperationStatus.Succeeded)
{
// 注意:我们缓存的是通用的句柄,而不是带泛型的句柄
CachedHandles[address] = newHandle;
return result;
}
else
{
Debug.LogError($"[AssetLoader] Failed to load asset at address: {address}");
return null;
}
}
/// <summary>
/// 释放指定地址的资源。
/// </summary>
/// <param name="address">要释放资源的Addressable地址</param>
public static void ReleaseAsset(string address)
{
if (CachedHandles.TryGetValue(address, out var handle))
{
// 从缓存中移除
CachedHandles.Remove(address);
// 释放句柄,这会减少资源的引用计数
Addressables.Release(handle);
Debug.Log($"[AssetLoader] Released asset: {address}");
}
else
{
Debug.LogWarning($"[AssetLoader] Tried to release an asset that was not loaded through this loader: {address}");
}
}
/// <summary>
/// 释放所有通过此类加载的资源。
/// 通常在切换场景或退出游戏时调用。
/// </summary>
public static void ReleaseAllAssets()
{
foreach (var pair in CachedHandles)
{
Addressables.Release(pair.Value);
}
CachedHandles.Clear();
Debug.Log("[AssetLoader] All cached assets released.");
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
using UniRx;
using System;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public enum ConditionFailedAction
{
@@ -9,6 +9,7 @@ namespace SoulliesFramework.General
Wait
}
/*
public interface ICommand
{
/// <summary>
@@ -20,36 +21,51 @@ namespace SoulliesFramework.General
void ForceComplete();
}
public abstract class CommandBase : ICommand
*/
public abstract class CommandBase
{
public CommandContext selfContext;
protected readonly Subject<Unit> forceCompleteSubject = new Subject<Unit>();
public void ForceComplete() => forceCompleteSubject.OnNext(Unit.Default);
// 子类必须实现这个方法,定义指令的核心行为
protected abstract IObservable<Unit> OnExecute(CommandContext context);
protected abstract IObservable<Unit> OnExecute(CommandContext outerContext);
protected virtual Func<CommandContext, bool> InnerCondition => (ctx) => true; // 默认条件为总是 true
// 子类通过重写这个 Func 来定义自己的执行条件
protected virtual Func<CommandContext, bool> Condition => (ctx) => true; // 默认条件为总是 true
protected virtual Func<CommandContext, bool> OuterCondition => (ctx) => true; // 默认条件为总是 true
// 子类定义条件失败时的处理方式
protected virtual ConditionFailedAction OnConditionFailed => ConditionFailedAction.Discard;
public CommandBase(CommandContext selfContext = null)
{
this.selfContext = selfContext ?? new CommandContext();
}
public virtual CommandBase Clone()
{
CommandBase clone = (CommandBase)MemberwiseClone();
clone.selfContext = selfContext.Clone();
return clone;
}
/// <summary>
/// Execute 的公共入口。
/// 它封装了条件判断、等待、或舍弃的复杂逻辑。
/// 子类不应该重写此方法,而应该实现 OnExecute。
/// </summary>
public IObservable<Unit> Execute(CommandContext context)
public IObservable<Unit> Execute(CommandContext outerContext)
{
// 检查初始条件
if (Condition(context))
if (InnerCondition(selfContext) && OuterCondition(outerContext))
{
// 条件满足,直接执行
return CreateAsyncOperation(() => OnExecute(context));
return CreateAsyncOperation(() => OnExecute(outerContext));
}
// 初始条件不满足
@@ -64,16 +80,16 @@ namespace SoulliesFramework.General
return Observable.Defer(() =>
{
// Defer 用于延迟执行,确保每次重试时都重新检查条件
if (Condition(context)) return CreateAsyncOperation(() => OnExecute(context));
if (OuterCondition(outerContext)) return CreateAsyncOperation(() => OnExecute(outerContext));
// 如果条件仍不满足,则开始等待游戏状态变化
return CommandQueueManager.Instance.OnStateChanged
// 检查每次状态变化后的条件
.Where(_ => Condition(context))
.Where(_ => OuterCondition(outerContext))
// 我们只需要条件第一次变为 true 的那个时刻
.First()
// 当条件满足时,切换到真正的执行逻辑
.SelectMany(_ => CreateAsyncOperation(() => OnExecute(context)));
.SelectMany(_ => CreateAsyncOperation(() => OnExecute(outerContext)));
});
}
}

View File

@@ -1,17 +1,56 @@
using UnityEngine;
using System.Collections.Generic;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
/// <summary>
/// 指令上下文 (Command Context)
/// 指令内容 (Command Context)
/// 包含了指令执行时可能需要的所有游戏状态信息。
/// 这个对象在指令组开始执行时创建,并被传递给每一个子指令。
/// 指令组开始执行时创建的CommandContext被传递给每一个子指令。
/// 在ICommand内置的CommandContext中则包含了该指令执行时特有的信息。
/// </summary>
public class CommandContext
{
public Dictionary<string, object> sharedInfo = new Dictionary<string, object>();
public readonly Dictionary<string, object> context;
public CommandContext()
{
context = new Dictionary<string, object>();
}
public CommandContext(string key, object value)
{
context = new Dictionary<string, object>
{
{ key, value }
};
}
public CommandContext(List<KeyValuePair<string, object>> initialInfo)
{
context = new Dictionary<string, object>();
foreach (var pair in initialInfo)
{
context[pair.Key] = pair.Value;
}
}
public CommandContext Clone()
{
var newContext = new CommandContext();
foreach (var pair in context)
{
newContext.context[pair.Key] = pair.Value;
}
return newContext;
}
public T GetInfo<T>(string key)
{
if (context.TryGetValue(key, out object value) && value is T typedValue)
{
return typedValue;
}
return default;
}
}
}

View File

@@ -4,35 +4,45 @@ using System.Linq;
using UniRx;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public enum ExecutionMode { Sequential, Parallel }
public class CommandGroup : CommandBase
{
private readonly List<ICommand> commands = new List<ICommand>();
private readonly ExecutionMode mode;
public readonly List<CommandBase> commands = new List<CommandBase>();
public readonly ExecutionMode mode;
public CommandGroup(ExecutionMode mode, params ICommand[] commands)
public CommandGroup(ExecutionMode mode, params CommandBase[] commands)
{
this.mode = mode;
this.commands.AddRange(commands);
}
public CommandGroup AddCommand(ICommand command)
public override CommandBase Clone()
{
CommandGroup newGroup = new CommandGroup(mode);
foreach (var cmd in commands)
{
newGroup.AddCommand(cmd.Clone());
}
return newGroup;
}
public CommandGroup AddCommand(CommandBase command)
{
commands.Add(command);
return this;
}
protected override IObservable<Unit> OnExecute(CommandContext context)
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
// --- 核心修正 ---
// 我们不再直接调用 cmd.Execute(context)。
// 而是创建一个“延迟执行”的 Observable 序列。
// Defer 会将对 Execute 的调用推迟到 Concat/WhenAll 真正订阅它的时候。
var lazyCommandObservables = commands.Select(cmd =>
Observable.Defer(() => cmd.Execute(context))
Observable.Defer(() => cmd.Execute(outerContext))
);
IObservable<Unit> executionFlow;

View File

@@ -2,15 +2,12 @@ using UniRx;
using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public class CommandQueueManager : SerializedMonoBehaviour
public class CommandQueueManager : Singleton<CommandQueueManager>
{
public static CommandQueueManager Instance { get; private set; }
// --- 新增:游戏状态变化的广播器 ---
/// <summary>
/// 一个全局的 Subject当任何可能影响指令执行条件的状态发生变化时
@@ -22,15 +19,15 @@ namespace SoulliesFramework.General
// 队列的入口现在需要一个能接收 Context 的指令工厂
// 1. 我们使用一个标准的 Queue<T> 来存储待执行的指令。
// 这比 Subject 更能明确地表达“队列”的意图。
private readonly Queue<Tuple<ICommand, CommandContext>> commandQueue = new Queue<Tuple<ICommand, CommandContext>>();
private readonly Queue<Tuple<CommandBase, CommandContext>> commandQueue = new Queue<Tuple<CommandBase, CommandContext>>();
// 2. 一个布尔值标志,用于追踪管理器当前是否正在执行一个指令。
private bool isBusy = false;
private void Awake()
protected override void Awake()
{
// ... 单例模式代码 ...
Instance = this;
base.Awake();
commandQueue
.Select(entry => entry.Item1.Execute(entry.Item2))
@@ -44,6 +41,7 @@ namespace SoulliesFramework.General
private void Start()
{
/*
var context = new CommandContext();
// 2. 创建第一个指令组(串行执行)。
@@ -65,14 +63,19 @@ namespace SoulliesFramework.General
new Cmd_GetAndLogVariable("TestMessage"),
new Cmd_WaitAndLog(1f, "第三个指令组,第二步完成。")
);
AddCommand(group1, context); // 传入我们创建的上下文
AddCommand(group2, context); // 传入完全相同的上下文,这样 group2 才能读取到 group1 设置的变量
AddCommand(group3, context); // 传入完全相同的上下文,这样 group3 才能读取到 group1 设置的变量
*/
}
public void AddCommand(ICommand command, CommandContext context = null)
public void AddCommand(CommandBase command, CommandContext context = null)
{
context ??= new CommandContext();
// 将指令和其上下文入队
commandQueue.Enqueue(Tuple.Create(command, context));
//Debug.Log($"[Queue] 添加指令: {command.GetType()},队列长度: {commandQueue.Count}");
// 尝试启动队列处理。
// 如果队列当前不忙,这个调用会立即开始处理我们刚刚添加的指令。
// 如果队列正忙,这个调用什么也不做,因为当前指令完成后会自动处理下一个。
@@ -99,7 +102,7 @@ namespace SoulliesFramework.General
var commandToExecute = nextEntry.Item1;
var context = nextEntry.Item2;
//Debug.Log($"--- [Queue] 开始执行指令: {commandToExecute.GetType().Name} ---");
//Debug.Log($"--- [Queue] 开始执行指令: {commandToExecute.GetType()} ---");
// 5. 【核心】在这里,我们才真正调用 Execute 方法。
// 此时,可以保证所有之前的指令都已经彻底完成了。
@@ -108,13 +111,13 @@ namespace SoulliesFramework.General
_ => { /* OnNext: 忽略 */ },
ex => {
// 错误处理:即使出错,也要解锁队列,避免卡死
Debug.LogError($"[Queue] 指令 {commandToExecute.GetType().Name} 执行出错: {ex}");
Debug.LogError($"[Queue] 指令 {commandToExecute.GetType()} 执行出错: {ex}");
isBusy = false;
ProcessNextInQueue(); // 尝试执行下一个
},
() => {
// 6. OnCompleted: 当指令成功完成时执行。
//Debug.Log($"--- [Queue] 指令 {commandToExecute.GetType().Name} 执行完毕 ---");
//Debug.Log($"--- [Queue] 指令 {commandToExecute.GetType()} 执行完毕 ---");
// 7. 解除“忙碌”状态,并立即尝试处理队列中的下一个指令。
// 这就形成了“一个接一个”的链式反应。

View File

@@ -2,7 +2,7 @@ using System;
using UniRx;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
/// <summary>
/// 指令:从指令上下文中获取一个变量,并将其作为字符串在控制台输出。
@@ -16,10 +16,10 @@ namespace SoulliesFramework.General
this.variableName = variableName;
}
protected override IObservable<Unit> OnExecute(CommandContext context)
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
// 尝试从 SharedData 中获取变量。
if (context.sharedInfo.TryGetValue(variableName, out object value))
if (outerContext.context.TryGetValue(variableName, out object value))
{
// 获取成功,将其转换为字符串并输出。
string stringValue = value?.ToString() ?? "null";

View File

@@ -2,7 +2,7 @@ using System;
using UniRx;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
/// <summary>
/// 指令:在指令上下文 (CommandContext) 中声明并设置一个变量。
@@ -18,13 +18,13 @@ namespace SoulliesFramework.General
this.value = value;
}
protected override IObservable<Unit> OnExecute(CommandContext context)
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
Debug.Log($"[Cmd_SetVariable] 正在设置变量 '{variableName}',值为: '{value}'");
// 直接操作 Context 的 SharedData 字典来存入数据。
// 使用索引器会覆盖同名旧值,如果需要更复杂的逻辑可以先用 TryGetValue 检查。
context.sharedInfo[variableName] = value;
outerContext.context[variableName] = value;
// 这是一个瞬时操作,所以返回一个立即完成的 Observable。
return Observable.Return(Unit.Default);

View File

@@ -2,7 +2,7 @@ using UniRx;
using System;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
/// <summary>
/// 指令:等待指定秒数后,在控制台输出一条信息。
@@ -23,7 +23,7 @@ namespace SoulliesFramework.General
this.message = message;
}
protected override IObservable<Unit> OnExecute(CommandContext context)
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
// 使用 Observable.Timer 来创建一个在指定时间后发出信号的流。
// Do 操作符用于在流的特定生命周期点执行副作用(例如打印日志)。

View File

@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using Continentis.MainGame;
using SoftCircuits.Collections;
using UnityEngine;
using UnityEngine.Events;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public static class DictionaryExtension
{
@@ -68,7 +66,7 @@ namespace SoulliesFramework.General
/// <summary>
/// 将源字典中的所有键值对粘贴到目标字典中,避免重复键。
/// </summary>
public static void PasteDictionary<T1, T2>(this Dictionary<T1, T2> source, Dictionary<T1, T2> target)
public static void PasteDictionary<T1, T2>(this IDictionary<T1, T2> source, IDictionary<T1, T2> target)
{
foreach (var pair in source)
{

View File

@@ -0,0 +1,59 @@
using SLSFramework.General;
using UnityEngine;
using UnityEngine.Events;
namespace SLSFramework.General
{
public class EventUnit : IPrioritized
{
private readonly UnityAction action;
public int Priority { get; set; }
public EventUnit(UnityAction action, int priority = 0)
{
this.action = action;
this.Priority = priority;
}
public void Invoke()
{
action.Invoke();
}
}
public class EventUnit<T> : IPrioritized
{
private readonly UnityAction<T> action;
public int Priority { get; set; }
public EventUnit(UnityAction<T> action, int priority = 0)
{
this.action = action;
this.Priority = priority;
}
public void Invoke(T arg)
{
action.Invoke(arg);
}
}
public class EventUnit<T1, T2> : IPrioritized
{
private readonly UnityAction<T1, T2> action;
public int Priority { get; set; }
public EventUnit(UnityAction<T1, T2> action, int priority = 0)
{
this.action = action;
this.Priority = priority;
}
public void Invoke(T1 arg1, T2 arg2)
{
action.Invoke(arg1, arg2);
}
}
}

View File

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

View File

@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public static class ListExtension
{
@@ -74,5 +77,43 @@ namespace SoulliesFramework.General
list.Swap(i, Random.Range(i, list.Count));
}
}
/// <summary>
/// 尝试从列表中随机获取一个元素
/// </summary>
public static bool TryGetRandom<T>(this IList<T> list, out T element)
{
if (list.Count == 0)
{
element = default;
return false;
}
element = list[Random.Range(0, list.Count)];
return true;
}
/// <summary>
/// 返回一个新的列表,包含原列表中排除指定元素后的所有元素
/// </summary>
public static IList<T> Exclude<T>(this IList<T> list, T element)
{
List<T> newList = new List<T>(list);
newList.Remove(element);
return newList;
}
/// <summary>
/// 根据指定的过滤器函数,返回一个新的列表,包含符合条件的元素
/// </summary>
public static List<T> Filtered<T>(this IList<T> list, Func<T, bool> filter, bool include = true)
{
return list.Where(item => filter(item) == include).ToList();
}
public static bool All<T>(this IList<Func<T, bool>> filters, T item)
{
return filters.All(filter => filter(item));
}
}
}

View File

@@ -1,9 +1,8 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public class Singleton<T> : SerializedMonoBehaviour where T : SerializedMonoBehaviour
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance => instance;

View File

@@ -1,8 +1,7 @@
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public static class SpaceConverter
{
/// <summary>

View File

@@ -0,0 +1,12 @@
using UnityEngine;
namespace SLSFramework.General
{
public static class SpriteExtension
{
public static Sprite Create(Texture2D texture)
{
return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
}
}
}

View File

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

View File

@@ -0,0 +1,18 @@
using I2.Loc;
using UnityEngine;
namespace SLSFramework.General
{
public static class StringExtension
{
public static string Localize(this string original)
{
if (LocalizationManager.TryGetTranslation(original, out string translated))
{
return translated;
}
return original;
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
using Lean.Pool;
using UnityEngine;
namespace SoulliesFramework.General
namespace SLSFramework.General
{
public static class TransformExtension
{

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e42871c192a020c4488db7d8a373130e
guid: d43790bbfdf096c4aacee891d230d77b
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using Lean.Pool;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.Serialization;
namespace SLSFramework.General.LeanPoolAssistance
{
public class PooledObject : MonoBehaviour, IPoolable
{
[FormerlySerializedAs("autoDespawn")] [Tooltip("是否在生成后定时自动回收")]
public bool isAutoDespawn = true;
[ShowIf("isAutoDespawn")][Tooltip("自动回收时间")]
public float autoDespawnTime = 1;
private List<IPoolable> children;
public void OnSpawn()
{
children = GetComponentsInChildren<IPoolable>().ToList();
children.Remove(this);
children.ForEach(child => child.OnSpawn());
if (isAutoDespawn)
{
LeanPool.Despawn(gameObject, autoDespawnTime);
}
}
public void OnDespawn()
{
children.ForEach(child => child.OnDespawn());
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,70 @@
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class InterfaceHolder<T> where T : class
{
[SerializeField] private MonoBehaviour value;
public T Value
{
get
{
if (value == null)
{
Debug.LogError("value is null");
return null;
}
T castValue = value as T;
if (castValue == null)
{
Debug.LogError($"value cannot be cast to {typeof(T)}. It is of type {value.GetType()}");
}
return castValue;
}
}
public InterfaceHolder(MonoBehaviour value)
{
this.value = value;
}
}
#if UNITY_EDITOR && !ODIN_INSPECTOR
[CustomPropertyDrawer(typeof(InterfaceHolder<>))]
public class InterfaceHolderDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
SerializedProperty valueProperty = property.FindPropertyRelative("value");
EditorGUI.BeginChangeCheck();
MonoBehaviour newValue = (MonoBehaviour)EditorGUI
.ObjectField(position, label, valueProperty.objectReferenceValue,
typeof(MonoBehaviour), true);
if (EditorGUI.EndChangeCheck())
{
if (newValue == null || newValue.GetComponent(fieldInfo.FieldType.GenericTypeArguments[0]) != null)
{
valueProperty.objectReferenceValue = newValue;
}
else
{
Debug.LogWarning($"Assigned object must implement interface {fieldInfo.FieldType.GenericTypeArguments[0].Name}");
}
}
EditorGUI.EndProperty();
}
}
#endif
}

View File

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

View File

@@ -0,0 +1,97 @@
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
[SerializeField] private List<SerializedDictionaryKVPProps<TKey, TValue>> dictionaryList = new();
[SerializeField] private float dividerPosProp = 0.5f;
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
foreach (var kvp in this)
{
if (dictionaryList.FirstOrDefault(value => this.Comparer.Equals(value.Key, kvp.Key))
is SerializedDictionaryKVPProps<TKey, TValue> serializedKVP)
{
serializedKVP.Value = kvp.Value;
}
else
{
dictionaryList.Add(kvp);
}
}
dictionaryList.RemoveAll(value => ContainsKey(value.Key) == false);
for (int i = 0; i < dictionaryList.Count; i++)
{
dictionaryList[i].index = i;
}
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
Clear();
dictionaryList.RemoveAll(r => r.Key == null);
foreach (var serializedKVP in dictionaryList)
{
if (!(serializedKVP.isKeyDuplicated = ContainsKey(serializedKVP.Key)))
{
Add(serializedKVP.Key, serializedKVP.Value);
}
}
}
public new TValue this[TKey key]
{
get
{
#if UNITY_EDITOR
if (ContainsKey(key))
{
var duplicateKeysWithCount = dictionaryList.GroupBy(item => item.Key)
.Where(group => group.Count() > 1)
.Select(group => new { Key = group.Key, Count = group.Count() });
foreach (var duplicatedKey in duplicateKeysWithCount)
{
Debug.LogError($"Key '{duplicatedKey.Key}' is duplicated {duplicatedKey.Count} times in the dictionary.");
}
return base[key];
}
else
{
Debug.LogError($"Key '{key}' not found in dictionary.");
return default(TValue);
}
#else
return base[key];
#endif
}
}
[System.Serializable]
public class SerializedDictionaryKVPProps<TypeKey, TypeValue>
{
public TypeKey Key;
public TypeValue Value;
public int index;
public bool isKeyDuplicated;
public SerializedDictionaryKVPProps(TypeKey key, TypeValue value) { this.Key = key; this.Value = value; }
public static implicit operator SerializedDictionaryKVPProps<TypeKey, TypeValue>(KeyValuePair<TypeKey, TypeValue> kvp)
=> new SerializedDictionaryKVPProps<TypeKey, TypeValue>(kvp.Key, kvp.Value);
public static implicit operator KeyValuePair<TypeKey, TypeValue>(SerializedDictionaryKVPProps<TypeKey, TypeValue> kvp)
=> new KeyValuePair<TypeKey, TypeValue>(kvp.Key, kvp.Value);
}
}
}

View File

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

View File

@@ -0,0 +1,433 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace SLSFramework.General
{
[CustomPropertyDrawer(typeof(SerializableDictionary<,>), true)]
public class SerializableDictionaryDrawer : PropertyDrawer
{
public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label)
{
var indentedRect = EditorGUI.IndentedRect(rect);
void Head()
{
var headerRect = indentedRect;
headerRect.height = EditorGUIUtility.singleLineHeight;
void ExpandablePanel()
{
var fullHeaderRect = new Rect(headerRect);
fullHeaderRect.x -= 17;
fullHeaderRect.width += 34;
if (Event.current != null && fullHeaderRect.Contains(Event.current.mousePosition))
{
Color transparentGrey = new Color(0.4f, 0.4f, 0.4f, 0.4f);
EditorGUI.DrawRect(fullHeaderRect, transparentGrey);
}
GUI.color = Color.clear;
if (GUI.Button(new Rect(fullHeaderRect.x, fullHeaderRect.y, fullHeaderRect.width - 40,
fullHeaderRect.height), ""))
{
prop.isExpanded = !prop.isExpanded;
}
GUI.color = Color.white;
var triangleRect = rect;
triangleRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.Foldout(triangleRect, prop.isExpanded, "");
}
void DisplayName()
{
GUI.color = Color.white;
#if UNITY_2022_1_OR_NEWER
var labelRect = headerRect;
GUI.Label(labelRect, prop.displayName);
#else
GUI.Label(headerRect, prop.displayName);
#endif
GUI.color = Color.white;
GUI.skin.label.fontSize = 12;
GUI.skin.label.fontStyle = FontStyle.Normal;
GUI.skin.label.alignment = TextAnchor.MiddleLeft;
}
void DuplicatedKeysWarning()
{
if (Event.current != null && Event.current.type != EventType.Repaint)
{
return;
}
var hasRepeated = false;
var repeatedKeys = new List<string>();
for (int i = 0; i < dictionaryList.arraySize; i++)
{
SerializedProperty isKeyRepeatedProperty = dictionaryList.GetArrayElementAtIndex(i)
.FindPropertyRelative("isKeyDuplicated");
if (isKeyRepeatedProperty.boolValue)
{
hasRepeated = true;
SerializedProperty keyProperty = dictionaryList.GetArrayElementAtIndex(i).FindPropertyRelative("Key");
string keyString = GetSerializedPropertyValueAsString(keyProperty);
repeatedKeys.Add(keyString);
}
}
if (!hasRepeated)
{
return;
}
float with = GUI.skin.label.CalcSize(new GUIContent(prop.displayName)).x;
headerRect.x += with + 35f;
var warningRect = headerRect;
Rect warningRectIcon = new Rect(headerRect.x - 18, headerRect.y, headerRect.width, headerRect.height);
GUI.color = Color.white;
GUI.Label(warningRectIcon, EditorGUIUtility.IconContent("console.erroricon"));
GUI.color = new Color(1.0f, 0.443f, 0.443f);
GUI.skin.label.fontStyle = FontStyle.Bold;
GUI.Label(warningRect, "Duplicated keys: " + string.Join(", ", repeatedKeys));
GUI.color = Color.white;
GUI.skin.label.fontStyle = FontStyle.Normal;
}
string GetSerializedPropertyValueAsString(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue.ToString();
case SerializedPropertyType.Boolean:
return property.boolValue.ToString();
case SerializedPropertyType.Float:
return property.floatValue.ToString();
case SerializedPropertyType.String:
return property.stringValue;
default:
return "(Unsupported Type)";
}
}
ExpandablePanel();
DisplayName();
DuplicatedKeysWarning();
}
void List()
{
if (!prop.isExpanded)
{
return;
}
SetupList(prop);
float newHeight = indentedRect.height - EditorGUIUtility.singleLineHeight - 3;
indentedRect.y += indentedRect.height - newHeight;
indentedRect.height = newHeight;
reorderableList.DoList(indentedRect);
}
SetupProps(prop);
Head();
List();
}
public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
{
SetupProps(prop);
var height = EditorGUIUtility.singleLineHeight;
if (prop.isExpanded)
{
SetupList(prop);
height += reorderableList.GetHeight() + 5;
}
return height;
}
private float GetListElementHeight(int index)
{
if (index >= dictionaryList.arraySize) return 0;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
float keyHeight = EditorGUI.GetPropertyHeight(keyProp, true);
float valueHeight;
if (IsSingleLine(valueProp))
{
// 如果Value是单行高度就是它自身的高度
valueHeight = EditorGUI.GetPropertyHeight(valueProp, true);
}
else
{
// 如果Value是复杂类型基础高度是标题行的高度
valueHeight = EditorGUIUtility.singleLineHeight;
// 如果它被展开了,需要加上所有子属性的高度
if (valueProp.isExpanded)
{
foreach (var child in GetChildren(valueProp))
{
valueHeight += EditorGUI.GetPropertyHeight(child, true) + EditorGUIUtility.standardVerticalSpacing;
}
}
}
// 返回Key和Value中较高者的高度并增加一点垂直间距
return Mathf.Max(keyHeight, valueHeight) + EditorGUIUtility.standardVerticalSpacing;
}
void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
{
if (index >= dictionaryList.arraySize) return;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
// 为整个元素添加一点垂直内边距
rect.y += 2;
// --- 区域计算 ---
var dividerWidh = 6f;
var dividerPosition = (dividerPosProp != null) ? dividerPosProp.floatValue : 0.3f;
var fullRect = rect;
fullRect.width -= 1;
var keyRect = fullRect;
keyRect.width *= dividerPosition;
keyRect.width -= dividerWidh / 2;
var valueRect = fullRect;
valueRect.x += fullRect.width * dividerPosition;
valueRect.width *= (1 - dividerPosition);
valueRect.width -= dividerWidh / 2;
// --- 绘制Key (保持不变) ---
// 确保Key的高度是单行避免它被拉伸
keyRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(keyRect, keyProp, GUIContent.none, true);
// --- 核心修改自定义绘制Value列 ---
if (IsSingleLine(valueProp))
{
// 如果Value是单行则正常绘制
valueRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(valueRect, valueProp, GUIContent.none, true);
}
else
{
// 如果Value是复杂类型则自定义绘制
var headerRect = new Rect(valueRect.x + 12, valueRect.y, valueRect.width, EditorGUIUtility.singleLineHeight);
// --- 问题1修复分离绘制三角箭头和标题增加间距 ---
// 1. 先只绘制三角箭头
valueProp.isExpanded = EditorGUI.Foldout(headerRect, valueProp.isExpanded, GUIContent.none, true);
// 2. 在箭头右侧留出空间后,再绘制标题
var titleRect = new Rect(headerRect.x + 3, headerRect.y, headerRect.width - 15, headerRect.height);
EditorGUI.LabelField(titleRect, valueProp.type, EditorStyles.boldLabel);
// 如果展开了,则在下方绘制所有子属性
if (valueProp.isExpanded)
{
// --- 问题2修复正确的循环绘制逻辑 ---
var contentRect = new Rect(valueRect.x,
valueRect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, valueRect.width,
valueRect.height);
EditorGUI.indentLevel++;
foreach (var child in GetChildren(valueProp))
{
// 1. 获取子属性自身的高度
var childHeight = EditorGUI.GetPropertyHeight(child, true);
// 2. 为子属性创建一个精确的矩形区域
var childRect = new Rect(contentRect.x, contentRect.y, contentRect.width, childHeight);
// 3. 在这个精确的区域内绘制子属性
//EditorGUI.PropertyField(childRect, child, true);
// 4. 保留属性名称,拉长子属性的输入框
if (child.hasVisibleChildren == false)
{
EditorGUI.LabelField(new Rect(childRect.x, childRect.y, EditorGUIUtility.labelWidth, childHeight), child.displayName);
Rect valueFieldRect = new Rect(childRect.x + EditorGUIUtility.labelWidth / 2,
childRect.y, childRect.width - EditorGUIUtility.labelWidth / 2, childHeight);
EditorGUI.PropertyField(valueFieldRect, child, GUIContent.none);
}
else
{
EditorGUI.PropertyField(childRect, child, true);
}
// 5. 将下一个绘制的Y坐标向下移动为下一个属性留出空间
contentRect.y += childHeight + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.indentLevel--;
}
}
// --- 绘制和处理分割线 (保持不变) ---
Divider(rect, fullRect, dividerPosition, dividerWidh);
}
void Divider(Rect originalRect, Rect fullRect, float dividerPosition, float dividerWidth)
{
Rect dividerRect = fullRect;
dividerRect.x += fullRect.width * dividerPosition - dividerWidth / 2;
dividerRect.width = dividerWidth;
EditorGUIUtility.AddCursorRect(dividerRect, MouseCursor.ResizeHorizontal);
if (Event.current != null && dividerRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.MouseDown)
{
//isDividerDragged = true;
}
else if (Event.current.type == EventType.MouseUp
|| Event.current.type == EventType.MouseMove
|| Event.current.type == EventType.MouseLeaveWindow)
{
isDividerDragged = false;
}
}
if (isDividerDragged && Event.current != null && Event.current.type == EventType.MouseDrag)
{
dividerPosProp.floatValue = Mathf.Clamp(dividerPosProp.floatValue + Event.current.delta.x / originalRect.width, .2f, .8f);
}
}
private void ShowDictIsEmptyMessage(Rect rect)
{
GUI.Label(rect, "Empty");
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop)
{
prop = prop.Copy();
var endProp = prop.GetEndProperty();
prop.NextVisible(true);
while (!SerializedProperty.EqualContents(prop, endProp))
{
yield return prop;
if (!prop.NextVisible(false))
break;
}
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop, bool enterVisibleGrandchildren)
{
prop = prop.Copy();
var startPath = prop.propertyPath;
var enterVisibleChildren = true;
while (prop.NextVisible(enterVisibleChildren) && prop.propertyPath.StartsWith(startPath))
{
yield return prop;
enterVisibleChildren = enterVisibleGrandchildren;
}
}
private bool IsSingleLine(SerializedProperty prop)
{
return prop.propertyType != SerializedPropertyType.Generic || prop.hasVisibleChildren == false;
}
private void SetupList(SerializedProperty prop)
{
if (reorderableList != null)
{
return;
}
SetupProps(prop);
this.reorderableList = new ReorderableList(dictionaryList.serializedObject, dictionaryList, true, false, true, true);
this.reorderableList.drawElementCallback = DrawListElement;
this.reorderableList.elementHeightCallback = GetListElementHeight;
this.reorderableList.drawNoneElementCallback = ShowDictIsEmptyMessage;
}
private ReorderableList reorderableList;
private bool isDividerDragged;
public void SetupProps(SerializedProperty prop)
{
if (this.property != null)
{
return;
}
this.property = prop;
this.dictionaryList = prop.FindPropertyRelative("dictionaryList");
this.dividerPosProp = prop.FindPropertyRelative("dividerPosProp");
// 尝试获取字段上的 KeyWidthAttribute
// 如果找到了该属性,则设置 dividerPosProp 的值
if (fieldInfo.GetCustomAttributes(typeof(KeyWidthAttribute), true).FirstOrDefault() is KeyWidthAttribute keyWidthAttribute)
{
this.dividerPosProp.floatValue = keyWidthAttribute.WidthPercentage;
}
else
{
this.dividerPosProp.floatValue = 0.5f; // 默认值
}
}
private SerializedProperty property;
private SerializedProperty dictionaryList;
private SerializedProperty dividerPosProp;
}
}
#endif
namespace SLSFramework.General
{
/// <summary>
/// 用于指定 SerializableDictionary 抽屉中 Key 区域的宽度占比。
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field)]
public class KeyWidthAttribute : PropertyAttribute
{
public readonly float WidthPercentage;
/// <summary>
/// 设置 Key 区域的宽度占比。
/// </summary>
/// <param name="widthPercentage">一个0.1到0.9之间的浮点数代表Key区域占总宽度的百分比。</param>
public KeyWidthAttribute(float widthPercentage)
{
// 将值限制在一个合理的范围内避免UI错乱
WidthPercentage = Mathf.Clamp(widthPercentage, 0.1f, 0.9f);
}
}
}

View File

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

View File

@@ -0,0 +1,17 @@
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class UnityObjectWrapper<T> where T : class
{
[SerializeField] private Object value;
public T Value => value as T;
public UnityObjectWrapper(Object value)
{
this.value = value;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,247 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
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++;
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 Type选择器string中
public partial class DataEditor
{
private static Dictionary<Tuple<Type, string>, (string[] paths, Type[] types)> _typeCache =
new Dictionary<Tuple<Type, string>, (string[], Type[])>();
/// <summary>
/// 绘制一个用于选择指定基类的所有子类的下拉菜单
/// </summary>
/// <param name="classNameProp">存储类名的字符串属性</param>
/// <param name="label">在Inspector中显示的标签</param>
/// <param name="baseType">要查找的基类 (例如 typeof(CardLogicBase))</param>
/// <param name="namespaceToRemove">可选参数,用于从路径中移除特定的命名空间部分 (例如 ".Cards")</param>
/// <returns>如果值被用户改变则返回true</returns>
protected bool DrawTypeSelectorGUI(SerializedProperty classNameProp, string label, Type baseType,
string namespacePrefix = null, string namespaceToRemove = null)
{
// --- 核心修改 2使用包含 namespaceToRemove 的复合键 ---
var cacheKey = new Tuple<Type, string>(baseType, namespaceToRemove ?? string.Empty);
if (!_typeCache.ContainsKey(cacheKey))
{
CacheDerivedTypes(baseType, namespacePrefix, namespaceToRemove, cacheKey);
}
var (paths, types) = _typeCache[cacheKey];
int currentIndex = -1;
for (int i = 0; i < types.Length; i++)
{
if (types[i].Name == classNameProp.stringValue)
{
currentIndex = i;
break;
}
}
int newIndex = EditorGUILayout.Popup(label, currentIndex, paths);
if (newIndex != currentIndex)
{
classNameProp.stringValue = (newIndex >= 0 && newIndex < types.Length)
? types[newIndex].Name
: string.Empty;
return true;
}
return false;
}
private void CacheDerivedTypes(Type baseType, string namespacePrefix, string namespaceToRemove, Tuple<Type, string> cacheKey)
{
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 = "Uncategorized/" + type.Name;
if (type.Namespace != null && type.Namespace.StartsWith(namespacePrefix))
{
string formattedNamespace = type.Namespace.Substring(namespacePrefix.Length);
// --- 核心修改 3使用传入的参数来替换硬编码 ---
if (!string.IsNullOrEmpty(namespaceToRemove))
{
formattedNamespace = formattedNamespace.Replace("." + namespaceToRemove, "");
}
formattedNamespace = formattedNamespace.Replace('.', '/');
if (formattedNamespace.StartsWith("/")) formattedNamespace = formattedNamespace.Substring(1);
path = formattedNamespace + "/" + type.Name;
}
typeList.Add((path, type));
}
typeList.Sort((a, b) => a.path.CompareTo(b.path));
_typeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
}
}
#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,95 @@
using System;
using System.Collections.Generic;
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.UI;
namespace SLSFramework.UModAssistance
{
public partial class ModBrowser : MonoBehaviour
{
// 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 void OnLoadClicked()
{
GetAllSelectedMods();
foreach (IModInfo mod in selectedMods)
{
ModHost host = ModManager.LoadMod(mod);
ModManager.RegisterTypesFromMod(host, typeof(CharacterBase));
ModManager.RegisterTypesFromMod(host, typeof(CardLogicBase));
ModManager.RegisterTypesFromMod(host, typeof(EquipmentBase));
ModManager.RegisterTypesFromMod(host,typeof(Continentis.MainGame.Card.CombatBuffBase));
ModManager.RegisterTypesFromMod(host,typeof(Continentis.MainGame.Character.CombatBuffBase));
ModManager.RegisterTypesFromMod(host, typeof(RulesCollectionBase));
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();
}
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,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
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 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>>();
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>();
/// <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)
{
if (!TypeRegistry.ContainsKey(type.Name))
{
TypeRegistry.Add(type.Name, type);
Debug.Log($"Registered script type '{type.FullName}' from mod '{host.CurrentMod.NameInfo.ModName}'.");
}
else
{
// 处理命名冲突如果不同Mod中存在同名的类后加载的会被忽略
Debug.LogWarning($"Duplicate script type name found: '{type.Name}'. 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 typeName)
{
if (TypeRegistry.TryGetValue(typeName, out Type type))
{
return type;
}
Debug.LogWarning($"Type '{typeName}' 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