MOD!
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb9d36169a1d74647b9533156c7d83a1
|
||||
@@ -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)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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. 解除“忙碌”状态,并立即尝试处理队列中的下一个指令。
|
||||
// 这就形成了“一个接一个”的链式反应。
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 操作符用于在流的特定生命周期点执行副作用(例如打印日志)。
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
59
Assets/Scripts/ScriptExtensions/General/GameEvent.cs
Normal file
59
Assets/Scripts/ScriptExtensions/General/GameEvent.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4a739f10fb7d8d49a75be3de4f95ac8
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SoulliesFramework.General
|
||||
namespace SLSFramework.General
|
||||
{
|
||||
|
||||
public static class SpaceConverter
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
12
Assets/Scripts/ScriptExtensions/General/SpriteExtension.cs
Normal file
12
Assets/Scripts/ScriptExtensions/General/SpriteExtension.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3531403c3553ec4e9db865958d48ef4
|
||||
18
Assets/Scripts/ScriptExtensions/General/StringExtension.cs
Normal file
18
Assets/Scripts/ScriptExtensions/General/StringExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd4a697d946242343a065437c29f0cf2
|
||||
@@ -1,7 +1,7 @@
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SoulliesFramework.General
|
||||
namespace SLSFramework.General
|
||||
{
|
||||
public static class TransformExtension
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e42871c192a020c4488db7d8a373130e
|
||||
guid: d43790bbfdf096c4aacee891d230d77b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfc79d04c0439624b848efbb0e52b465
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92457a38d7647bf43aac9ee7db913593
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4351450027391954e85fea86db758c08
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08185d6eb814648ce9cdfca048e1611b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af44f85b3a51e40cb8b1285fb308b2a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36996041f1dde6b46942025e4519df17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/ScriptExtensions/UModAssistance.meta
Normal file
8
Assets/Scripts/ScriptExtensions/UModAssistance.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e75e9d47b465af4890bb1dc142df2ab
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5240790581fdac4aa3b2a4db00e0e9a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cc91a8081a9a9f44ab0df9fe725a089
|
||||
95
Assets/Scripts/ScriptExtensions/UModAssistance/ModBrowser.cs
Normal file
95
Assets/Scripts/ScriptExtensions/UModAssistance/ModBrowser.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 222420d8b6807ad458d012a439512436
|
||||
31
Assets/Scripts/ScriptExtensions/UModAssistance/ModLoadTab.cs
Normal file
31
Assets/Scripts/ScriptExtensions/UModAssistance/ModLoadTab.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0ea9bf78368bff43b7f5145f22492c3
|
||||
199
Assets/Scripts/ScriptExtensions/UModAssistance/ModManager.cs
Normal file
199
Assets/Scripts/ScriptExtensions/UModAssistance/ModManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d2fb7a334e649f4ba61fc074840774d
|
||||
Reference in New Issue
Block a user