feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb836b5fd9485e94cbefc8f1d743bc0f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[DefaultExecutionOrder(-2100)]
|
||||
public sealed class ShrinkModBootstrap : MonoBehaviour
|
||||
{
|
||||
[Header("Override (留空则只使用组件上的选项)")]
|
||||
[SerializeField] private ShrinkModFrameworkSettings settingsOverride;
|
||||
|
||||
[Header("Bootstrap")]
|
||||
[SerializeField] private bool autoLoadOnAwake = true;
|
||||
|
||||
private static bool _bootstrapped;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_bootstrapped)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_bootstrapped = true;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
var shouldAutoLoad = settingsOverride ? settingsOverride.autoLoadOnStartup : autoLoadOnAwake;
|
||||
if (shouldAutoLoad)
|
||||
ShrinkModLoader.LoadAll(settingsOverride);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23fd762abde4cfe48bf93f9ff86ffdd5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ShrinkModFrameworkSettings", menuName = "ShrinkModFramework/Settings")]
|
||||
public class ShrinkModFrameworkSettings : ScriptableObject
|
||||
{
|
||||
private static ShrinkModFrameworkSettings _instance;
|
||||
|
||||
public static ShrinkModFrameworkSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance) return _instance;
|
||||
|
||||
_instance = Resources.Load<ShrinkModFrameworkSettings>("ShrinkModFrameworkSettings");
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!_instance)
|
||||
{
|
||||
var guids = UnityEditor.AssetDatabase.FindAssets("t:ShrinkModFrameworkSettings");
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
_instance = UnityEditor.AssetDatabase.LoadAssetAtPath<ShrinkModFrameworkSettings>(path);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!_instance)
|
||||
{
|
||||
_instance = CreateInstance<ShrinkModFrameworkSettings>();
|
||||
Debug.LogWarning(
|
||||
"[ShrinkModFramework] 未找到 ShrinkModFrameworkSettings 配置文件,当前使用默认配置。");
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
internal set => _instance = value;
|
||||
}
|
||||
|
||||
[Header("Bootstrap")]
|
||||
public bool autoLoadOnStartup = true;
|
||||
[Tooltip("使用 ShrinkContextHost 协调模组组件;关闭时回退到旧的只增不减 ShrinkModLoader。")]
|
||||
public bool useContextHost = true;
|
||||
|
||||
[Header("Logging")]
|
||||
public bool verboseLogging = true;
|
||||
|
||||
[Header("Discovery")]
|
||||
public string[] assemblyNamePrefixes = new string[0];
|
||||
|
||||
[Header("External Mods")]
|
||||
public bool enableExternalDllMods = true;
|
||||
public bool autoCreateExternalModsDirectory = true;
|
||||
public string externalModsFolderName = "Mods";
|
||||
public bool watchExternalModsDirectory = true;
|
||||
public float externalModsReloadDelaySeconds = 0.5f;
|
||||
|
||||
[Header("Harmony")]
|
||||
public bool enableHarmonyPatching = true;
|
||||
|
||||
[Header("Networking")]
|
||||
public bool enableNetworkSync = true;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f9516fe9a338fb448308a9205c3d448
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public static class ShrinkModRuntimeBootstrap
|
||||
{
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticStateForPlayMode()
|
||||
{
|
||||
ShrinkModLoader.ResetForDomainReload();
|
||||
ShrinkModNetworkManager.ResetForDomainReload();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||||
private static void AutoLoad()
|
||||
{
|
||||
var settings = ShrinkModFrameworkSettings.Instance;
|
||||
ShrinkModRuntimeDriver.EnsureCreated(settings);
|
||||
|
||||
if (settings && !settings.autoLoadOnStartup)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Debug.LogError($"[ShrinkModFramework] 自动启动失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 329c6f80f39fd4840b987c16a6124b1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[DefaultExecutionOrder(-2099)]
|
||||
internal sealed class ShrinkModRuntimeDriver : MonoBehaviour
|
||||
{
|
||||
private static ShrinkModRuntimeDriver _instance;
|
||||
|
||||
private readonly ConcurrentQueue<Action> _mainThreadActions = new();
|
||||
private FileSystemWatcher _watcher;
|
||||
private ShrinkModFrameworkSettings _settings;
|
||||
private bool _pendingExternalReload;
|
||||
private float _reloadAtRealtime;
|
||||
|
||||
public static void EnsureCreated(ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
if (_instance)
|
||||
{
|
||||
_instance.ApplySettings(settings);
|
||||
return;
|
||||
}
|
||||
|
||||
var go = new GameObject("ShrinkModRuntimeDriver");
|
||||
DontDestroyOnLoad(go);
|
||||
_instance = go.AddComponent<ShrinkModRuntimeDriver>();
|
||||
_instance.ApplySettings(settings);
|
||||
}
|
||||
|
||||
public static void Enqueue(Action action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (_instance == null)
|
||||
throw new InvalidOperationException("ShrinkModRuntimeDriver 尚未创建。");
|
||||
|
||||
_instance._mainThreadActions.Enqueue(action);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
while (_mainThreadActions.TryDequeue(out var action))
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_pendingExternalReload || Time.realtimeSinceStartup < _reloadAtRealtime)
|
||||
return;
|
||||
|
||||
_pendingExternalReload = false;
|
||||
|
||||
try
|
||||
{
|
||||
ShrinkModLoader.LoadNewExternalMods(_settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Debug.LogError($"[ShrinkModFramework] 自动热加载外部模组失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
DisposeWatcher();
|
||||
if (_instance == this)
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
private void ApplySettings(ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
_settings = settings ?? ShrinkModFrameworkSettings.Instance;
|
||||
|
||||
if (_settings == null || !_settings.enableExternalDllMods || !_settings.watchExternalModsDirectory)
|
||||
{
|
||||
DisposeWatcher();
|
||||
return;
|
||||
}
|
||||
|
||||
TrySetupWatcher();
|
||||
}
|
||||
|
||||
private void TrySetupWatcher()
|
||||
{
|
||||
#if ENABLE_IL2CPP && !UNITY_EDITOR
|
||||
return;
|
||||
#else
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(_settings);
|
||||
if (_settings.autoCreateExternalModsDirectory)
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
if (_watcher != null && string.Equals(_watcher.Path, directory, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
DisposeWatcher();
|
||||
|
||||
try
|
||||
{
|
||||
_watcher = new FileSystemWatcher(directory, "*.dll")
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime
|
||||
};
|
||||
_watcher.Created += OnExternalModsChanged;
|
||||
_watcher.Changed += OnExternalModsChanged;
|
||||
_watcher.Renamed += OnExternalModsRenamed;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 外部模组目录监听启动失败:{ex.Message}");
|
||||
DisposeWatcher();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnExternalModsChanged(object sender, FileSystemEventArgs args)
|
||||
{
|
||||
ScheduleExternalReload(args.FullPath);
|
||||
}
|
||||
|
||||
private void OnExternalModsRenamed(object sender, RenamedEventArgs args)
|
||||
{
|
||||
ScheduleExternalReload(args.FullPath);
|
||||
}
|
||||
|
||||
private void ScheduleExternalReload(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
|
||||
_mainThreadActions.Enqueue(() =>
|
||||
{
|
||||
_pendingExternalReload = true;
|
||||
var delay = _settings ? Mathf.Max(0.05f, _settings.externalModsReloadDelaySeconds) : 0.5f;
|
||||
_reloadAtRealtime = Time.realtimeSinceStartup + delay;
|
||||
});
|
||||
}
|
||||
|
||||
private void DisposeWatcher()
|
||||
{
|
||||
if (_watcher == null)
|
||||
return;
|
||||
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Created -= OnExternalModsChanged;
|
||||
_watcher.Changed -= OnExternalModsChanged;
|
||||
_watcher.Renamed -= OnExternalModsRenamed;
|
||||
_watcher.Dispose();
|
||||
_watcher = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a850cdc47a8b5b41a7559c3d1fd9efc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eddd3eb11f065f4d912ab35a69576f3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>把 IShrinkMod 四阶段生命周期映射为一个可逆 Cordis 组件。</summary>
|
||||
internal sealed class ShrinkModComponent : IShrinkComponent
|
||||
{
|
||||
private readonly ShrinkModContextHost _host;
|
||||
private readonly ShrinkModComponentSource _source;
|
||||
private readonly string[] _inject;
|
||||
private readonly string[] _provide;
|
||||
|
||||
public ShrinkModComponent(ShrinkModContextHost host, ShrinkModComponentSource source)
|
||||
{
|
||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
_inject = source.Info.Dependencies
|
||||
.Where(dependency => !dependency.Optional)
|
||||
.Select(dependency => ShrinkModContextHost.GetModKey(dependency.ModId))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
_provide = new[] { ShrinkModContextHost.GetModKey(source.Info.ModId) };
|
||||
}
|
||||
|
||||
public string Name => "shrink.mod/" + _source.Info.ModId;
|
||||
public IReadOnlyList<string> Inject => _inject;
|
||||
public IReadOnlyList<string> Provide => _provide;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object config)
|
||||
{
|
||||
var instance = _source.Factory();
|
||||
if (instance == null)
|
||||
throw new InvalidOperationException($"Mod factory returned null: {_source.Info.ModId}");
|
||||
|
||||
var handle = new ShrinkModHandle(_source.Info, instance)
|
||||
{
|
||||
Generation = _host.NextGeneration()
|
||||
};
|
||||
|
||||
ctx.Effect(
|
||||
() => _host.RegisterHandle(handle),
|
||||
() =>
|
||||
{
|
||||
_host.UnregisterHandle(handle);
|
||||
handle.State = ShrinkModState.Unloaded;
|
||||
});
|
||||
|
||||
IDisposable harmonyLease = null;
|
||||
ctx.Effect(
|
||||
() => harmonyLease = ShrinkHarmonyPatchService.AcquirePatchesIfNeeded(
|
||||
handle.Info, _host.EnableHarmonyPatching, _host.VerboseLogging),
|
||||
() => harmonyLease?.Dispose());
|
||||
|
||||
var modContext = new ShrinkModContext(handle.Info, _host.RegistryManager,
|
||||
_host.Mods, _host.VerboseLogging, ctx);
|
||||
|
||||
instance.OnConstruct(modContext);
|
||||
handle.State = ShrinkModState.Constructed;
|
||||
|
||||
// 先登记归属清理,OnRegisterContent 中途失败也能撤回已注册的部分内容。
|
||||
ctx.Effect(() => { }, () => _host.RegistryManager.RemoveOwnedEntries(handle.Info.ModId));
|
||||
instance.OnRegisterContent(modContext);
|
||||
handle.State = ShrinkModState.ContentRegistered;
|
||||
|
||||
// 模组网络 handler 以 ModId 为归属键,可在失败或卸载时整体撤回。
|
||||
ctx.Effect(() => { }, () => ShrinkModNetworkManager.UnregisterHandlers(handle.Info.ModId));
|
||||
instance.OnInitialize(modContext);
|
||||
handle.State = ShrinkModState.Initialized;
|
||||
|
||||
instance.OnReady(modContext);
|
||||
handle.State = ShrinkModState.Ready;
|
||||
ctx.Set(_provide[0], handle);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 167a3359a4a357a49ab81ba266caa07f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 把工程内程序集和外部 DLL 的当前 revision 投影为延迟组件源。
|
||||
/// 同一路径的旧外部程序集无法从 Mono AppDomain 卸载,但不会再进入候选集合。
|
||||
/// </summary>
|
||||
internal static class ShrinkModComponentDiscovery
|
||||
{
|
||||
public static IReadOnlyList<ShrinkModComponentSource> Discover(ShrinkModFrameworkSettings settings,
|
||||
bool verboseLogging)
|
||||
{
|
||||
ShrinkExternalModAssemblyLoader.ScanExternalAssemblyRevisions(settings, verboseLogging);
|
||||
var results = new List<ShrinkModComponentSource>();
|
||||
var prefixes = settings?.assemblyNamePrefixes;
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (ShrinkExternalModAssemblyLoader.IsExternalAssembly(assembly) &&
|
||||
!ShrinkExternalModAssemblyLoader.TryGetCurrentRevision(assembly, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ShouldScanAssembly(assembly, prefixes))
|
||||
continue;
|
||||
|
||||
var assemblyRevision = GetAssemblyRevision(assembly);
|
||||
foreach (var type in GetTypesSafely(assembly))
|
||||
{
|
||||
if (type == null || type.IsAbstract || type.IsInterface)
|
||||
continue;
|
||||
|
||||
var modAttribute = type.GetCustomAttribute<ShrinkModAttribute>(false);
|
||||
if (modAttribute == null)
|
||||
continue;
|
||||
if (!typeof(IShrinkMod).IsAssignableFrom(type))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[ShrinkModFramework] 类型 {type.FullName} 标记了 ShrinkMod,但没有实现 IShrinkMod,已跳过。");
|
||||
continue;
|
||||
}
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
throw new InvalidOperationException($"模组 {type.FullName} 缺少无参构造函数,无法实例化。");
|
||||
|
||||
var dependencies = type
|
||||
.GetCustomAttributes<ShrinkModDependencyAttribute>(false)
|
||||
.Select(attribute => new ShrinkModDependency(
|
||||
attribute.ModId, attribute.MinimumVersion, attribute.Optional))
|
||||
.ToArray();
|
||||
var info = new ShrinkModInfo(
|
||||
modAttribute.ModId,
|
||||
modAttribute.DisplayName,
|
||||
modAttribute.Version,
|
||||
modAttribute.LoadOrder,
|
||||
type,
|
||||
modAttribute.AutoApplyHarmonyPatches,
|
||||
dependencies);
|
||||
var revision = $"{assemblyRevision}:{type.FullName}";
|
||||
results.Add(new ShrinkModComponentSource(info, revision,
|
||||
() => (IShrinkMod)Activator.CreateInstance(type)));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static string GetAssemblyRevision(Assembly assembly)
|
||||
{
|
||||
if (ShrinkExternalModAssemblyLoader.TryGetCurrentRevision(assembly, out var externalRevision))
|
||||
return externalRevision;
|
||||
|
||||
try
|
||||
{
|
||||
return assembly.ManifestModule.ModuleVersionId.ToString("N");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return assembly.FullName ?? assembly.GetName().Name ?? "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldScanAssembly(Assembly assembly, string[] prefixes)
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
if (name.StartsWith("Unity", StringComparison.Ordinal) ||
|
||||
name.StartsWith("System", StringComparison.Ordinal) ||
|
||||
name.StartsWith("mscorlib", StringComparison.Ordinal) ||
|
||||
name.StartsWith("netstandard", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (prefixes == null || prefixes.Length == 0)
|
||||
return true;
|
||||
|
||||
return prefixes.Any(prefix => !string.IsNullOrWhiteSpace(prefix) &&
|
||||
name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetTypesSafely(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException exception)
|
||||
{
|
||||
return exception.Types.Where(type => type != null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95e3bad3c1c50b44db3b144af1aafda8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 延迟模组组件源。Revision 是来源内容的稳定指纹(外部 DLL 可使用文件哈希);
|
||||
/// 同一 ModId 的 revision/factory 变化由 ShrinkModContextHost 作为替换事务处理。
|
||||
/// </summary>
|
||||
public sealed class ShrinkModComponentSource
|
||||
{
|
||||
public ShrinkModComponentSource(ShrinkModInfo info, string revision, Func<IShrinkMod> factory)
|
||||
{
|
||||
Info = info ?? throw new ArgumentNullException(nameof(info));
|
||||
if (string.IsNullOrWhiteSpace(info.ModId))
|
||||
throw new ArgumentException("ModId must not be empty.", nameof(info));
|
||||
if (string.IsNullOrWhiteSpace(revision))
|
||||
throw new ArgumentException("Revision must not be empty.", nameof(revision));
|
||||
|
||||
Revision = revision.Trim();
|
||||
Factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public ShrinkModInfo Info { get; }
|
||||
public string Revision { get; }
|
||||
public Func<IShrinkMod> Factory { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0878db300b244c645bdc69c5b96b70d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModTransactionException : Exception
|
||||
{
|
||||
public ShrinkModTransactionException(string message, Exception applyError, Exception restoreError = null)
|
||||
: base(message, restoreError == null ? applyError : new AggregateException(applyError, restoreError))
|
||||
{
|
||||
ApplyError = applyError;
|
||||
RestoreError = restoreError;
|
||||
}
|
||||
|
||||
public Exception ApplyError { get; }
|
||||
public Exception RestoreError { get; }
|
||||
public bool PreviousCompositionRestored => RestoreError == null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模组的 Cordis 组合宿主。配置应用是事务:新组合任一 fiber 失败时,重新协调到旧组件源;
|
||||
/// Mono/IL2CPP 下旧程序集仍驻留,但旧/新模组实例及其已追踪效应会被正确卸载或恢复。
|
||||
/// </summary>
|
||||
public sealed class ShrinkModContextHost
|
||||
{
|
||||
private readonly Dictionary<string, ShrinkModHandle> _mods = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<ShrinkModComponentSource, string> _catalogNames = new();
|
||||
private readonly List<ShrinkModComponentSource> _currentSources = new();
|
||||
private readonly ShrinkComponentCatalog _catalog = new();
|
||||
private long _generation;
|
||||
private int _catalogGeneration;
|
||||
private bool _applying;
|
||||
|
||||
public ShrinkModContextHost(bool enableHarmonyPatching = true, bool verboseLogging = false)
|
||||
{
|
||||
EnableHarmonyPatching = enableHarmonyPatching;
|
||||
VerboseLogging = verboseLogging;
|
||||
Runtime = new ShrinkContextRuntime();
|
||||
Loader = new ShrinkContextLoader(Runtime, _catalog);
|
||||
RegistryManager = new ShrinkModRegistryManager();
|
||||
}
|
||||
|
||||
public event Action<ShrinkModHandle> OnModReady;
|
||||
public event Action<IReadOnlyDictionary<string, ShrinkModHandle>> OnAllModsReady;
|
||||
|
||||
public bool EnableHarmonyPatching { get; }
|
||||
public bool VerboseLogging { get; }
|
||||
public ShrinkContextRuntime Runtime { get; }
|
||||
public ShrinkContextLoader Loader { get; }
|
||||
public IReadOnlyDictionary<string, ShrinkModHandle> Mods => _mods;
|
||||
public IReadOnlyList<ShrinkModComponentSource> CurrentSources => _currentSources.ToArray();
|
||||
internal ShrinkModRegistryManager RegistryManager { get; }
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public static string GetModKey(string modId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("ModId must not be empty.", nameof(modId));
|
||||
return "shrink.mod/" + modId.Trim();
|
||||
}
|
||||
|
||||
public async UniTask ApplyAsync(IReadOnlyList<ShrinkModComponentSource> desiredSources)
|
||||
{
|
||||
if (desiredSources == null)
|
||||
throw new ArgumentNullException(nameof(desiredSources));
|
||||
if (_applying)
|
||||
throw new InvalidOperationException("A mod composition transaction is already running.");
|
||||
|
||||
var desired = ReuseUnchangedSources(ValidateAndOrder(desiredSources));
|
||||
var previous = _currentSources.ToArray();
|
||||
var previousById = previous.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal);
|
||||
_applying = true;
|
||||
try
|
||||
{
|
||||
await Loader.ApplyAsync(BuildEntries(desired));
|
||||
EnsureActive(desired);
|
||||
|
||||
_currentSources.Clear();
|
||||
_currentSources.AddRange(desired);
|
||||
foreach (var source in desired)
|
||||
{
|
||||
if (!previousById.TryGetValue(source.Info.ModId, out var oldSource) ||
|
||||
!ReferenceEquals(source, oldSource))
|
||||
{
|
||||
OnModReady?.Invoke(_mods[source.Info.ModId]);
|
||||
}
|
||||
}
|
||||
OnAllModsReady?.Invoke(Mods);
|
||||
}
|
||||
catch (Exception applyError)
|
||||
{
|
||||
Exception restoreError = null;
|
||||
try
|
||||
{
|
||||
await Loader.ApplyAsync(BuildEntries(previous));
|
||||
EnsureActive(previous);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
restoreError = ex;
|
||||
}
|
||||
|
||||
throw new ShrinkModTransactionException(
|
||||
restoreError == null
|
||||
? "Mod composition failed; the previous composition was restored."
|
||||
: "Mod composition failed and restoring the previous composition also failed.",
|
||||
applyError,
|
||||
restoreError);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync()
|
||||
{
|
||||
if (_applying)
|
||||
throw new InvalidOperationException("Cannot shut down while a mod transaction is running.");
|
||||
|
||||
await Loader.ApplyAsync(Array.Empty<ShrinkLoaderEntry>());
|
||||
_currentSources.Clear();
|
||||
}
|
||||
|
||||
internal long NextGeneration() => ++_generation;
|
||||
|
||||
internal void RegisterHandle(ShrinkModHandle handle)
|
||||
{
|
||||
if (!_mods.TryAdd(handle.Info.ModId, handle))
|
||||
throw new InvalidOperationException($"Mod handle already active: {handle.Info.ModId}");
|
||||
}
|
||||
|
||||
internal void UnregisterHandle(ShrinkModHandle handle)
|
||||
{
|
||||
if (_mods.TryGetValue(handle.Info.ModId, out var current) && ReferenceEquals(current, handle))
|
||||
_mods.Remove(handle.Info.ModId);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ShrinkLoaderEntry> BuildEntries(IEnumerable<ShrinkModComponentSource> sources)
|
||||
{
|
||||
var entries = new List<ShrinkLoaderEntry>();
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (!_catalogNames.TryGetValue(source, out var catalogName))
|
||||
{
|
||||
catalogName = $"shrink.mod.source/{source.Info.ModId}/{++_catalogGeneration}";
|
||||
_catalogNames.Add(source, catalogName);
|
||||
_catalog.Register(catalogName, () => new ShrinkModComponent(this, source));
|
||||
}
|
||||
|
||||
entries.Add(new ShrinkLoaderEntry(source.Info.ModId, catalogName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private void EnsureActive(IEnumerable<ShrinkModComponentSource> sources)
|
||||
{
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (!Loader.TryGetFiber(source.Info.ModId, out var fiber))
|
||||
throw new InvalidOperationException($"Mod fiber was not created: {source.Info.ModId}");
|
||||
if (fiber.LastError != null)
|
||||
throw new InvalidOperationException($"Mod {source.Info.ModId} failed during apply.", fiber.LastError);
|
||||
if (fiber.State != ShrinkFiberState.Active)
|
||||
throw new InvalidOperationException(
|
||||
$"Mod {source.Info.ModId} did not become active (state={fiber.State}).");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ShrinkModComponentSource> ValidateAndOrder(
|
||||
IReadOnlyList<ShrinkModComponentSource> sources)
|
||||
{
|
||||
var map = new Dictionary<string, ShrinkModComponentSource>(StringComparer.Ordinal);
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentException("Mod sources must not contain null.", nameof(sources));
|
||||
if (!map.TryAdd(source.Info.ModId, source))
|
||||
throw new InvalidOperationException($"Duplicate mod source id: {source.Info.ModId}");
|
||||
}
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
foreach (var dependency in source.Info.Dependencies)
|
||||
{
|
||||
if (!map.TryGetValue(dependency.ModId, out var target))
|
||||
{
|
||||
if (!dependency.Optional)
|
||||
throw new InvalidOperationException(
|
||||
$"Mod {source.Info.ModId} is missing required dependency {dependency.ModId}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dependency.MinimumVersion) &&
|
||||
ShrinkVersionUtility.Compare(target.Info.Version, dependency.MinimumVersion) < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Mod {source.Info.ModId} requires {dependency.ModId} >= {dependency.MinimumVersion}, " +
|
||||
$"but found {target.Info.Version}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<ShrinkModComponentSource>();
|
||||
var visiting = new HashSet<string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var source in sources.OrderBy(item => item.Info.LoadOrder)
|
||||
.ThenBy(item => item.Info.ModId, StringComparer.Ordinal))
|
||||
{
|
||||
Visit(source, map, visiting, visited, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ShrinkModComponentSource> ReuseUnchangedSources(
|
||||
IEnumerable<ShrinkModComponentSource> desired)
|
||||
{
|
||||
var currentById = _currentSources.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal);
|
||||
var normalized = new List<ShrinkModComponentSource>();
|
||||
foreach (var source in desired)
|
||||
{
|
||||
if (currentById.TryGetValue(source.Info.ModId, out var current) &&
|
||||
string.Equals(current.Revision, source.Revision, StringComparison.Ordinal))
|
||||
{
|
||||
normalized.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
normalized.Add(source);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void Visit(ShrinkModComponentSource source,
|
||||
IReadOnlyDictionary<string, ShrinkModComponentSource> map,
|
||||
ISet<string> visiting,
|
||||
ISet<string> visited,
|
||||
ICollection<ShrinkModComponentSource> result)
|
||||
{
|
||||
if (visited.Contains(source.Info.ModId))
|
||||
return;
|
||||
if (!visiting.Add(source.Info.ModId))
|
||||
throw new InvalidOperationException($"Circular mod dependency includes {source.Info.ModId}.");
|
||||
|
||||
foreach (var dependency in source.Info.Dependencies.OrderBy(item => item.ModId, StringComparer.Ordinal))
|
||||
{
|
||||
if (map.TryGetValue(dependency.ModId, out var target))
|
||||
Visit(target, map, visiting, visited, result);
|
||||
}
|
||||
|
||||
visiting.Remove(source.Info.ModId);
|
||||
visited.Add(source.Info.ModId);
|
||||
result.Add(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a1df7a5c357c614a976ff968dac34fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>默认模组组件根;目录变更只提交完整的新组合,失败时由 Host 恢复旧组合。</summary>
|
||||
public static class ShrinkModCordisRuntime
|
||||
{
|
||||
private static ShrinkModContextHost _host;
|
||||
|
||||
public static bool IsInitialized => _host != null;
|
||||
public static ShrinkModContextHost Host => _host ?? throw new InvalidOperationException(
|
||||
"ShrinkModCordisRuntime has not been initialized.");
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> Mods =>
|
||||
_host?.Mods ?? EmptyMods;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, ShrinkModHandle> EmptyMods =
|
||||
new Dictionary<string, ShrinkModHandle>();
|
||||
|
||||
public static async UniTask<IReadOnlyDictionary<string, ShrinkModHandle>> ApplyDiscoveredAsync(
|
||||
ShrinkModFrameworkSettings settings = null)
|
||||
{
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
var verboseLogging = settings == null || settings.verboseLogging;
|
||||
if (_host == null)
|
||||
{
|
||||
_host = new ShrinkModContextHost(
|
||||
settings == null || settings.enableHarmonyPatching,
|
||||
verboseLogging);
|
||||
}
|
||||
|
||||
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging);
|
||||
var sources = ShrinkModComponentDiscovery.Discover(settings, verboseLogging);
|
||||
await _host.ApplyAsync(sources);
|
||||
|
||||
if (verboseLogging)
|
||||
UnityEngine.Debug.Log($"[ShrinkModFramework] Cordis 模组组合已提交,共 {_host.Mods.Count} 个。");
|
||||
return _host.Mods;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
Host.GetOrCreateRegistry<T>(name);
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
_host = null;
|
||||
}
|
||||
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
if (_host != null)
|
||||
_host.ShutdownAsync().GetAwaiter().GetResult();
|
||||
_host = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8767fd6541a55046a6d2f2ec992c446
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f3fa4ee5dcc33646922caaf6b2b72d8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public interface IShrinkMod
|
||||
{
|
||||
void OnConstruct(ShrinkModContext context);
|
||||
void OnRegisterContent(ShrinkModContext context);
|
||||
void OnInitialize(ShrinkModContext context);
|
||||
void OnReady(ShrinkModContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6736c9aa27396dc4d9ceb5efd0db3ccb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public abstract class ShrinkModBase : IShrinkMod
|
||||
{
|
||||
public virtual void OnConstruct(ShrinkModContext context) { }
|
||||
public virtual void OnRegisterContent(ShrinkModContext context) { }
|
||||
public virtual void OnInitialize(ShrinkModContext context) { }
|
||||
public virtual void OnReady(ShrinkModContext context) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb330a4a244f00444a73f5b1786ff80a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ShrinkContext;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModContext
|
||||
{
|
||||
private readonly ShrinkModRegistryManager _registryManager;
|
||||
private readonly IReadOnlyDictionary<string, ShrinkModHandle> _loadedMods;
|
||||
private readonly bool _verboseLogging;
|
||||
private readonly ShrinkCtx _effectContext;
|
||||
|
||||
internal ShrinkModContext(ShrinkModInfo modInfo, ShrinkModRegistryManager registryManager,
|
||||
IReadOnlyDictionary<string, ShrinkModHandle> loadedMods, bool verboseLogging,
|
||||
ShrinkCtx effectContext = null)
|
||||
{
|
||||
ModInfo = modInfo;
|
||||
_registryManager = registryManager;
|
||||
_loadedMods = loadedMods;
|
||||
_verboseLogging = verboseLogging;
|
||||
_effectContext = effectContext;
|
||||
}
|
||||
|
||||
public ShrinkModInfo ModInfo { get; }
|
||||
public IReadOnlyDictionary<string, ShrinkModHandle> LoadedMods => _loadedMods;
|
||||
public bool IsContextManaged => _effectContext != null;
|
||||
|
||||
public ShrinkModRegistry<T> GetRegistry<T>(string name) => _registryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
=> _registryManager.TryGetRegistry(name, out registry);
|
||||
|
||||
public bool IsModLoaded(string modId) => !string.IsNullOrWhiteSpace(modId) && _loadedMods.ContainsKey(modId);
|
||||
|
||||
public bool TryGetLoadedMod(string modId, out ShrinkModHandle handle)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(modId))
|
||||
return _loadedMods.TryGetValue(modId, out handle);
|
||||
|
||||
handle = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在模组组件上下文中执行前向动作并就地登记逆操作。逆操作随 fiber 卸载按 LIFO 执行。
|
||||
/// 旧 ShrinkModLoader 路径没有可逆上下文,调用本 API 会明确失败而不是静默丢失 cleanup。
|
||||
/// </summary>
|
||||
public void Effect(Action forward, Action inverse)
|
||||
{
|
||||
if (_effectContext == null)
|
||||
throw new InvalidOperationException(
|
||||
"ShrinkModContext.Effect requires ShrinkModContextHost. The legacy ShrinkModLoader cannot track inverses.");
|
||||
_effectContext.Effect(forward, inverse);
|
||||
}
|
||||
|
||||
/// <summary>为已完成的前向动作登记同步逆操作。</summary>
|
||||
public void EffectInverse(Action inverse)
|
||||
{
|
||||
if (inverse == null)
|
||||
throw new ArgumentNullException(nameof(inverse));
|
||||
Effect(() => { }, inverse);
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
if (_verboseLogging)
|
||||
Debug.Log($"[ShrinkMod:{ModInfo.ModId}] {message}");
|
||||
}
|
||||
|
||||
public void RegisterNetworkHandler<T>(string channel, System.Action<ShrinkModNetworkMessageContext, T> handler)
|
||||
=> ShrinkModNetworkManager.RegisterHandler(ModInfo.ModId, channel, handler);
|
||||
|
||||
public void SendToServer<T>(string channel, T payload, string senderPeerId = null)
|
||||
=> ShrinkModNetworkManager.SendToServer(ModInfo.ModId, channel, payload, senderPeerId);
|
||||
|
||||
public void SendToAllClients<T>(string channel, T payload, string senderPeerId = null)
|
||||
=> ShrinkModNetworkManager.SendToAllClients(ModInfo.ModId, channel, payload, senderPeerId);
|
||||
|
||||
public void SendToClient<T>(string channel, T payload, string targetPeerId, string senderPeerId = null)
|
||||
=> ShrinkModNetworkManager.SendToClient(ModInfo.ModId, channel, payload, targetPeerId, senderPeerId);
|
||||
|
||||
public void LogWarning(string message) => Debug.LogWarning($"[ShrinkMod:{ModInfo.ModId}] {message}");
|
||||
public void LogError(string message) => Debug.LogError($"[ShrinkMod:{ModInfo.ModId}] {message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b8a0f4cfa66c5142a114eacadb74b75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModHandle
|
||||
{
|
||||
public ShrinkModInfo Info { get; }
|
||||
public IShrinkMod Instance { get; }
|
||||
public ShrinkModState State { get; internal set; }
|
||||
public long Generation { get; internal set; }
|
||||
|
||||
internal ShrinkModHandle(ShrinkModInfo info, IShrinkMod instance)
|
||||
{
|
||||
Info = info;
|
||||
Instance = instance;
|
||||
State = ShrinkModState.Discovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6609ebbb46db87c42a2fa1d45bd12efe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d81e389d24bac1a45bedd3a97d679a87
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkModOptionalRuntimeIntegration
|
||||
{
|
||||
public static void RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo, bool verboseLogging)
|
||||
{
|
||||
if (modInstance == null || modInfo == null)
|
||||
return;
|
||||
|
||||
var modType = modInstance.GetType();
|
||||
TryRegisterEventBusInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterCommandInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterNetworkInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
}
|
||||
|
||||
public static void RefreshGlobalBindings(bool verboseLogging)
|
||||
{
|
||||
TryRefreshNetworkEventBusBridge(verboseLogging);
|
||||
}
|
||||
|
||||
private static void TryRegisterEventBusInstance(Type modType, object modInstance, string modId, bool verboseLogging)
|
||||
{
|
||||
if (!HasAttribute(modType, "ShrinkEventBus.EventBusSubscriberAttribute", "ShrinkEventBus.Runtime"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var eventBusType = FindType("ShrinkEventBus.EventBus", "ShrinkEventBus.Runtime");
|
||||
var autoRegisterMethod = eventBusType?.GetMethod("AutoRegister",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(object) },
|
||||
null);
|
||||
if (autoRegisterMethod == null)
|
||||
return;
|
||||
|
||||
autoRegisterMethod.Invoke(null, new[] { modInstance });
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 模组 {modId} 已接入 ShrinkEventBus 实例订阅。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 模组 {modId} 接入 ShrinkEventBus 失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRegisterCommandInstance(Type modType, object modInstance, string modId, bool verboseLogging)
|
||||
{
|
||||
if (!HasAttribute(modType, "ShrinkCommand.ShrinkCommandSubscriberAttribute", "ShrinkCommand.Runtime"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var runtimeType = FindType("ShrinkCommand.ShrinkCommandRuntime", "ShrinkCommand.Runtime");
|
||||
var service = runtimeType?.GetProperty("Default", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
||||
var registerMethod = service?.GetType().GetMethod("RegisterCommands",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { typeof(object) },
|
||||
null);
|
||||
if (registerMethod == null)
|
||||
return;
|
||||
|
||||
registerMethod.Invoke(service, new[] { modInstance });
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 模组 {modId} 已接入 ShrinkCommand 默认服务。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 模组 {modId} 接入 ShrinkCommand 失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRegisterNetworkInstance(Type modType, object modInstance, string modId, bool verboseLogging)
|
||||
{
|
||||
if (!HasAttribute(modType, "ShrinkNetwork.ShrinkNetworkSubscriberAttribute", "ShrinkNetwork.Runtime"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var runtimeType = FindType("ShrinkNetwork.ShrinkNetworkRuntime", "ShrinkNetwork.Runtime");
|
||||
var service = runtimeType?.GetProperty("Default", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
||||
var registerMethod = service?.GetType().GetMethod("RegisterHandlers",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { typeof(object) },
|
||||
null);
|
||||
if (registerMethod == null)
|
||||
return;
|
||||
|
||||
registerMethod.Invoke(service, new[] { modInstance });
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 模组 {modId} 已接入 ShrinkNetwork 默认服务。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 模组 {modId} 接入 ShrinkNetwork 失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRefreshNetworkEventBusBridge(bool verboseLogging)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bridgeType = FindType("ShrinkNetwork.Integration.ShrinkNetworkEventBusBridge",
|
||||
"ShrinkNetwork.Integration.EventBus");
|
||||
var refreshMethod = bridgeType?.GetMethod("RefreshBindings",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
if (refreshMethod == null)
|
||||
return;
|
||||
|
||||
refreshMethod.Invoke(null, null);
|
||||
if (verboseLogging)
|
||||
Debug.Log("[ShrinkModFramework] 已刷新 ShrinkNetwork.Integration.EventBus 绑定。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 刷新 ShrinkNetwork.Integration.EventBus 绑定失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasAttribute(Type targetType, string attributeFullName, string assemblyName)
|
||||
{
|
||||
var attributeType = FindType(attributeFullName, assemblyName);
|
||||
return attributeType != null && targetType.GetCustomAttribute(attributeType, false) != null;
|
||||
}
|
||||
|
||||
private static Type FindType(string fullName, string assemblyName)
|
||||
{
|
||||
return Type.GetType($"{fullName}, {assemblyName}", false) ??
|
||||
AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(assembly => string.Equals(assembly.GetName().Name, assemblyName, StringComparison.Ordinal))
|
||||
.Select(assembly => assembly.GetType(fullName, false))
|
||||
.FirstOrDefault(type => type != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 733bd8496a2bbcb4fa29594a14d3b0b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fba48516fe357cd4eb44de16cea78788
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkExternalModAssemblyLoader
|
||||
{
|
||||
internal sealed class ExternalAssemblyRevision
|
||||
{
|
||||
public string Path;
|
||||
public string Revision;
|
||||
public Assembly Assembly;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, ExternalAssemblyRevision> CurrentAssemblyRevisions =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<Assembly> ExternalAssemblyHistory = new();
|
||||
private static readonly Dictionary<string, string> KnownAssemblyFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly object ResolveLock = new();
|
||||
private static bool _resolveRegistered;
|
||||
|
||||
public static IReadOnlyList<Assembly> LoadExternalAssemblies(ShrinkModFrameworkSettings settings, bool verboseLogging)
|
||||
{
|
||||
return ScanExternalAssemblyRevisions(settings, verboseLogging)
|
||||
.Select(revision => revision.Assembly)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<ExternalAssemblyRevision> ScanExternalAssemblyRevisions(
|
||||
ShrinkModFrameworkSettings settings, bool verboseLogging)
|
||||
{
|
||||
if (settings != null && !settings.enableExternalDllMods)
|
||||
{
|
||||
CurrentAssemblyRevisions.Clear();
|
||||
KnownAssemblyFiles.Clear();
|
||||
return Array.Empty<ExternalAssemblyRevision>();
|
||||
}
|
||||
|
||||
#if ENABLE_IL2CPP && !UNITY_EDITOR
|
||||
Debug.LogWarning("[ShrinkModFramework] IL2CPP 运行时不支持外部 DLL 热加载,已跳过外部模组扫描。");
|
||||
return Array.Empty<ExternalAssemblyRevision>();
|
||||
#else
|
||||
var modsDirectory = GetExternalModsDirectory(settings);
|
||||
if (settings == null || settings.autoCreateExternalModsDirectory)
|
||||
Directory.CreateDirectory(modsDirectory);
|
||||
|
||||
RegisterAssemblyResolve();
|
||||
|
||||
var dllPaths = Directory.GetFiles(modsDirectory, "*.dll", SearchOption.AllDirectories)
|
||||
.Select(Path.GetFullPath)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var presentPaths = new HashSet<string>(dllPaths, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var dllPath in dllPaths
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var assemblyName = Path.GetFileNameWithoutExtension(dllPath);
|
||||
KnownAssemblyFiles[assemblyName] = dllPath;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(dllPath);
|
||||
var revision = ComputeSha256(bytes);
|
||||
if (CurrentAssemblyRevisions.TryGetValue(dllPath, out var current) &&
|
||||
string.Equals(current.Revision, revision, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pdbPath = Path.ChangeExtension(dllPath, ".pdb");
|
||||
var assembly = File.Exists(pdbPath)
|
||||
? Assembly.Load(bytes, File.ReadAllBytes(pdbPath))
|
||||
: Assembly.Load(bytes);
|
||||
|
||||
var loadedRevision = new ExternalAssemblyRevision
|
||||
{
|
||||
Path = dllPath,
|
||||
Revision = revision,
|
||||
Assembly = assembly
|
||||
};
|
||||
CurrentAssemblyRevisions[dllPath] = loadedRevision;
|
||||
ExternalAssemblyHistory.Add(assembly);
|
||||
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 已加载外部模组程序集:{assembly.GetName().Name} ({revision[..12]})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ShrinkModFramework] 加载外部 DLL 失败:{dllPath}\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var missingPath in CurrentAssemblyRevisions.Keys
|
||||
.Where(path => !presentPaths.Contains(path))
|
||||
.ToArray())
|
||||
{
|
||||
CurrentAssemblyRevisions.Remove(missingPath);
|
||||
}
|
||||
|
||||
return CurrentAssemblyRevisions.Values
|
||||
.OrderBy(revision => revision.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static bool IsExternalAssembly(Assembly assembly) =>
|
||||
assembly != null && ExternalAssemblyHistory.Contains(assembly);
|
||||
|
||||
internal static bool TryGetCurrentRevision(Assembly assembly, out string revision)
|
||||
{
|
||||
foreach (var current in CurrentAssemblyRevisions.Values)
|
||||
{
|
||||
if (ReferenceEquals(current.Assembly, assembly))
|
||||
{
|
||||
revision = current.Revision;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
revision = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string GetExternalModsDirectory(ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
var folderName = settings != null && !string.IsNullOrWhiteSpace(settings.externalModsFolderName)
|
||||
? settings.externalModsFolderName.Trim()
|
||||
: "Mods";
|
||||
return Path.Combine(Application.persistentDataPath, folderName);
|
||||
}
|
||||
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
CurrentAssemblyRevisions.Clear();
|
||||
ExternalAssemblyHistory.Clear();
|
||||
KnownAssemblyFiles.Clear();
|
||||
if (_resolveRegistered)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= OnAssemblyResolve;
|
||||
_resolveRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
return BitConverter.ToString(sha256.ComputeHash(bytes)).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
private static void RegisterAssemblyResolve()
|
||||
{
|
||||
lock (ResolveLock)
|
||||
{
|
||||
if (_resolveRegistered)
|
||||
return;
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
||||
_resolveRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
|
||||
{
|
||||
var requestedName = new AssemblyName(args.Name).Name;
|
||||
if (string.IsNullOrWhiteSpace(requestedName))
|
||||
return null;
|
||||
|
||||
if (!KnownAssemblyFiles.TryGetValue(requestedName, out var path) || !File.Exists(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return Assembly.Load(File.ReadAllBytes(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd2c1a02b62d790449c129e226cde40b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkHarmonyPatchService
|
||||
{
|
||||
private sealed class EmptyLease : IDisposable
|
||||
{
|
||||
public static readonly EmptyLease Instance = new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private sealed class HarmonyLease : IDisposable
|
||||
{
|
||||
private readonly Type _harmonyType;
|
||||
private readonly object _harmony;
|
||||
private readonly string _harmonyId;
|
||||
private readonly bool _verboseLogging;
|
||||
private bool _disposed;
|
||||
|
||||
public HarmonyLease(Type harmonyType, object harmony, string harmonyId, bool verboseLogging)
|
||||
{
|
||||
_harmonyType = harmonyType;
|
||||
_harmony = harmony;
|
||||
_harmonyId = harmonyId;
|
||||
_verboseLogging = verboseLogging;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
try
|
||||
{
|
||||
var unpatchSelf = _harmonyType.GetMethod("UnpatchSelf",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
if (unpatchSelf != null)
|
||||
{
|
||||
unpatchSelf.Invoke(_harmony, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unpatchAll = _harmonyType.GetMethod("UnpatchAll",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(string) },
|
||||
null);
|
||||
if (unpatchAll == null)
|
||||
throw new MissingMethodException("Harmony.UnpatchSelf()/UnpatchAll(string) 不存在。");
|
||||
unpatchAll.Invoke(null, new object[] { _harmonyId });
|
||||
}
|
||||
|
||||
if (_verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 已撤回 Harmony 补丁:{_harmonyId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ShrinkModFramework] 撤回 Harmony 补丁失败:{_harmonyId}\n{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
AppliedHarmonyIds.Remove(_harmonyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> AppliedHarmonyIds = new(StringComparer.Ordinal);
|
||||
|
||||
public static void ApplyPatchesIfNeeded(ShrinkModInfo modInfo, bool enableHarmonyPatching, bool verboseLogging)
|
||||
{
|
||||
_ = ApplyPatchesCore(modInfo, enableHarmonyPatching, verboseLogging, reversible: false);
|
||||
}
|
||||
|
||||
public static IDisposable AcquirePatchesIfNeeded(ShrinkModInfo modInfo, bool enableHarmonyPatching,
|
||||
bool verboseLogging)
|
||||
{
|
||||
return ApplyPatchesCore(modInfo, enableHarmonyPatching, verboseLogging, reversible: true);
|
||||
}
|
||||
|
||||
private static IDisposable ApplyPatchesCore(ShrinkModInfo modInfo, bool enableHarmonyPatching,
|
||||
bool verboseLogging, bool reversible)
|
||||
{
|
||||
if (!enableHarmonyPatching || !modInfo.AutoApplyHarmonyPatches)
|
||||
return EmptyLease.Instance;
|
||||
|
||||
var harmonyType = Type.GetType("HarmonyLib.Harmony, 0Harmony");
|
||||
if (harmonyType == null)
|
||||
{
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 未检测到 Harmony,跳过模组 {modInfo.ModId} 的补丁自动应用。");
|
||||
return EmptyLease.Instance;
|
||||
}
|
||||
|
||||
var harmonyId = $"shrink.mod.{modInfo.ModId}";
|
||||
if (!AppliedHarmonyIds.Add(harmonyId))
|
||||
return EmptyLease.Instance;
|
||||
|
||||
try
|
||||
{
|
||||
var harmony = Activator.CreateInstance(harmonyType, harmonyId);
|
||||
var patchAll = harmonyType.GetMethod("PatchAll", new[] { typeof(Assembly) });
|
||||
if (patchAll == null)
|
||||
throw new MissingMethodException("Harmony.PatchAll(Assembly) 不存在。");
|
||||
|
||||
patchAll.Invoke(harmony, new object[] { modInfo.EntryType.Assembly });
|
||||
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 已应用 Harmony 补丁:{modInfo.ModId}");
|
||||
|
||||
return reversible
|
||||
? new HarmonyLease(harmonyType, harmony, harmonyId, verboseLogging)
|
||||
: EmptyLease.Instance;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppliedHarmonyIds.Remove(harmonyId);
|
||||
throw new InvalidOperationException($"模组 {modInfo.ModId} 应用 Harmony 补丁失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTesting() => AppliedHarmonyIds.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1bf43d28651dea4fa7d5e5c56361da4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,459 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public static class ShrinkModLoader
|
||||
{
|
||||
private sealed class DiscoveredMod
|
||||
{
|
||||
public ShrinkModInfo Info;
|
||||
public Type EntryType;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, ShrinkModHandle> LoadedMods = new(StringComparer.Ordinal);
|
||||
private static readonly List<ShrinkModHandle> LoadSequence = new();
|
||||
private static readonly ShrinkModRegistryManager RegistryManager = new();
|
||||
|
||||
public static bool IsLoaded { get; private set; }
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> Mods =>
|
||||
ShrinkModCordisRuntime.IsInitialized ? ShrinkModCordisRuntime.Mods : LoadedMods;
|
||||
|
||||
public static event Action<ShrinkModHandle> OnModReady;
|
||||
public static event Action<IReadOnlyDictionary<string, ShrinkModHandle>> OnAllModsReady;
|
||||
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> LoadAll(ShrinkModFrameworkSettings settings = null)
|
||||
{
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
Debug.Log("[ShrinkModLoader] 模组已装载,跳过重复装载。");
|
||||
return Mods;
|
||||
}
|
||||
|
||||
if (settings == null || settings.useContextHost)
|
||||
return ApplyContextComposition(settings);
|
||||
|
||||
var verboseLogging = settings == null || settings.verboseLogging;
|
||||
|
||||
try
|
||||
{
|
||||
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging);
|
||||
ShrinkExternalModAssemblyLoader.LoadExternalAssemblies(settings, verboseLogging);
|
||||
|
||||
var discovered = DiscoverMods(settings);
|
||||
var ordered = ResolveLoadOrder(discovered, allowExistingLoadedDependencies: true);
|
||||
|
||||
foreach (var discoveredMod in ordered)
|
||||
{
|
||||
var handle = InstantiateAndTrack(discoveredMod, settings, verboseLogging);
|
||||
LoadSequence.Add(handle);
|
||||
}
|
||||
|
||||
RunLifecycle(LoadSequence, verboseLogging);
|
||||
|
||||
IsLoaded = true;
|
||||
OnAllModsReady?.Invoke(LoadedMods);
|
||||
Debug.Log($"[ShrinkModLoader] 模组装载完成,共 {LoadedMods.Count} 个。");
|
||||
return LoadedMods;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ResetForTesting();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> LoadNewExternalMods(ShrinkModFrameworkSettings settings = null)
|
||||
{
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
if (settings == null || settings.useContextHost)
|
||||
return ApplyContextComposition(settings);
|
||||
if (!IsLoaded)
|
||||
return LoadAll(settings);
|
||||
|
||||
var verboseLogging = settings == null || settings.verboseLogging;
|
||||
ShrinkExternalModAssemblyLoader.LoadExternalAssemblies(settings, verboseLogging);
|
||||
|
||||
var discovered = DiscoverMods(settings)
|
||||
.Where(mod => !LoadedMods.ContainsKey(mod.Info.ModId))
|
||||
.ToList();
|
||||
|
||||
if (discovered.Count == 0)
|
||||
return LoadedMods;
|
||||
|
||||
ValidateNewMods(discovered);
|
||||
var ordered = ResolveLoadOrder(discovered, allowExistingLoadedDependencies: true);
|
||||
var newHandles = new List<ShrinkModHandle>(ordered.Count);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var discoveredMod in ordered)
|
||||
{
|
||||
var handle = InstantiateAndTrack(discoveredMod, settings, verboseLogging);
|
||||
LoadSequence.Add(handle);
|
||||
newHandles.Add(handle);
|
||||
}
|
||||
|
||||
RunLifecycle(newHandles, verboseLogging);
|
||||
return LoadedMods;
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (var handle in newHandles)
|
||||
{
|
||||
LoadedMods.Remove(handle.Info.ModId);
|
||||
LoadSequence.Remove(handle);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetMod(string modId, out ShrinkModHandle handle)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(modId))
|
||||
return Mods.TryGetValue(modId, out handle);
|
||||
|
||||
handle = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
=> ShrinkModCordisRuntime.IsInitialized
|
||||
? ShrinkModCordisRuntime.GetOrCreateRegistry<T>(name)
|
||||
: RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForTesting();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
IsLoaded = false;
|
||||
OnModReady = null;
|
||||
OnAllModsReady = null;
|
||||
ShrinkModNetworkManager.ResetForDomainReload();
|
||||
ShrinkExternalModAssemblyLoader.ResetForTesting();
|
||||
ShrinkHarmonyPatchService.ResetForTesting();
|
||||
}
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForDomainReload();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
IsLoaded = false;
|
||||
OnModReady = null;
|
||||
OnAllModsReady = null;
|
||||
ShrinkModNetworkManager.ResetForDomainReload();
|
||||
ShrinkExternalModAssemblyLoader.ResetForTesting();
|
||||
ShrinkHarmonyPatchService.ResetForTesting();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, ShrinkModHandle> ApplyContextComposition(
|
||||
ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
var previousGenerations = Mods.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value.Generation,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
var result = ShrinkModCordisRuntime.ApplyDiscoveredAsync(settings).GetAwaiter().GetResult();
|
||||
IsLoaded = true;
|
||||
foreach (var pair in result.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (!previousGenerations.TryGetValue(pair.Key, out var generation) ||
|
||||
generation != pair.Value.Generation)
|
||||
{
|
||||
OnModReady?.Invoke(pair.Value);
|
||||
}
|
||||
}
|
||||
OnAllModsReady?.Invoke(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ShrinkModHandle InstantiateAndTrack(DiscoveredMod discoveredMod,
|
||||
ShrinkModFrameworkSettings settings, bool verboseLogging)
|
||||
{
|
||||
var instance = CreateInstance(discoveredMod);
|
||||
var handle = new ShrinkModHandle(discoveredMod.Info, instance);
|
||||
LoadedMods.Add(handle.Info.ModId, handle);
|
||||
|
||||
ShrinkHarmonyPatchService.ApplyPatchesIfNeeded(
|
||||
handle.Info,
|
||||
settings == null || settings.enableHarmonyPatching,
|
||||
verboseLogging);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static List<DiscoveredMod> DiscoverMods(ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
var results = new List<DiscoveredMod>();
|
||||
var prefixes = settings?.assemblyNamePrefixes;
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (!ShouldScanAssembly(assembly, prefixes))
|
||||
continue;
|
||||
|
||||
foreach (var type in GetTypesSafely(assembly))
|
||||
{
|
||||
if (type == null || type.IsAbstract || type.IsInterface)
|
||||
continue;
|
||||
|
||||
var modAttribute = type.GetCustomAttribute<ShrinkModAttribute>(false);
|
||||
if (modAttribute == null)
|
||||
continue;
|
||||
|
||||
if (!typeof(IShrinkMod).IsAssignableFrom(type))
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModLoader] 类型 {type.FullName} 标记了 ShrinkMod,但没有实现 IShrinkMod,已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
throw new InvalidOperationException($"模组 {type.FullName} 缺少无参构造函数,无法实例化。");
|
||||
|
||||
var dependencies = type
|
||||
.GetCustomAttributes<ShrinkModDependencyAttribute>(false)
|
||||
.Select(attr => new ShrinkModDependency(attr.ModId, attr.MinimumVersion, attr.Optional))
|
||||
.ToArray();
|
||||
|
||||
results.Add(new DiscoveredMod
|
||||
{
|
||||
EntryType = type,
|
||||
Info = new ShrinkModInfo(
|
||||
modAttribute.ModId,
|
||||
modAttribute.DisplayName,
|
||||
modAttribute.Version,
|
||||
modAttribute.LoadOrder,
|
||||
type,
|
||||
modAttribute.AutoApplyHarmonyPatches,
|
||||
dependencies)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ValidateDiscoveredMods(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void ValidateDiscoveredMods(List<DiscoveredMod> discovered)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var mod in discovered)
|
||||
{
|
||||
if (!seen.Add(mod.Info.ModId))
|
||||
throw new InvalidOperationException($"发现重复模组 ID:{mod.Info.ModId}");
|
||||
}
|
||||
|
||||
var map = discovered.ToDictionary(x => x.Info.ModId, x => x, StringComparer.Ordinal);
|
||||
foreach (var mod in discovered)
|
||||
{
|
||||
foreach (var dependency in mod.Info.Dependencies)
|
||||
{
|
||||
if (!map.TryGetValue(dependency.ModId, out var target))
|
||||
{
|
||||
if (!dependency.Optional && !LoadedMods.ContainsKey(dependency.ModId))
|
||||
throw new InvalidOperationException($"模组 {mod.Info.ModId} 缺少必选依赖 {dependency.ModId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidateDependencyVersion(mod.Info, dependency, target.Info.Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateNewMods(List<DiscoveredMod> newMods)
|
||||
{
|
||||
var seen = new HashSet<string>(LoadedMods.Keys, StringComparer.Ordinal);
|
||||
foreach (var mod in newMods)
|
||||
{
|
||||
if (!seen.Add(mod.Info.ModId))
|
||||
throw new InvalidOperationException($"发现重复模组 ID:{mod.Info.ModId}");
|
||||
}
|
||||
|
||||
var map = newMods.ToDictionary(x => x.Info.ModId, x => x, StringComparer.Ordinal);
|
||||
foreach (var mod in newMods)
|
||||
{
|
||||
foreach (var dependency in mod.Info.Dependencies)
|
||||
{
|
||||
if (map.TryGetValue(dependency.ModId, out var newTarget))
|
||||
{
|
||||
ValidateDependencyVersion(mod.Info, dependency, newTarget.Info.Version);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LoadedMods.TryGetValue(dependency.ModId, out var loadedTarget))
|
||||
{
|
||||
ValidateDependencyVersion(mod.Info, dependency, loadedTarget.Info.Version);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dependency.Optional)
|
||||
throw new InvalidOperationException($"模组 {mod.Info.ModId} 缺少必选依赖 {dependency.ModId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDependencyVersion(ShrinkModInfo owner, ShrinkModDependency dependency, string actualVersion)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(dependency.MinimumVersion) &&
|
||||
ShrinkVersionUtility.Compare(actualVersion, dependency.MinimumVersion) < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"模组 {owner.ModId} 依赖 {dependency.ModId} >= {dependency.MinimumVersion},但当前只有 {actualVersion}");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<DiscoveredMod> ResolveLoadOrder(List<DiscoveredMod> discovered, bool allowExistingLoadedDependencies)
|
||||
{
|
||||
var map = discovered.ToDictionary(x => x.Info.ModId, x => x, StringComparer.Ordinal);
|
||||
var result = new List<DiscoveredMod>();
|
||||
var visiting = new HashSet<string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var mod in discovered.OrderBy(x => x.Info.LoadOrder).ThenBy(x => x.Info.ModId, StringComparer.Ordinal))
|
||||
Visit(mod, map, visiting, visited, result, allowExistingLoadedDependencies);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Visit(DiscoveredMod current, Dictionary<string, DiscoveredMod> map,
|
||||
HashSet<string> visiting, HashSet<string> visited, List<DiscoveredMod> result,
|
||||
bool allowExistingLoadedDependencies)
|
||||
{
|
||||
if (visited.Contains(current.Info.ModId))
|
||||
return;
|
||||
|
||||
if (!visiting.Add(current.Info.ModId))
|
||||
throw new InvalidOperationException($"检测到模组循环依赖,涉及 {current.Info.ModId}");
|
||||
|
||||
foreach (var dependency in current.Info.Dependencies.OrderBy(x => x.ModId, StringComparer.Ordinal))
|
||||
{
|
||||
if (map.TryGetValue(dependency.ModId, out var target))
|
||||
{
|
||||
Visit(target, map, visiting, visited, result, allowExistingLoadedDependencies);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowExistingLoadedDependencies && LoadedMods.ContainsKey(dependency.ModId))
|
||||
continue;
|
||||
|
||||
if (!dependency.Optional)
|
||||
throw new InvalidOperationException($"模组 {current.Info.ModId} 缺少必选依赖 {dependency.ModId}");
|
||||
}
|
||||
|
||||
visiting.Remove(current.Info.ModId);
|
||||
visited.Add(current.Info.ModId);
|
||||
result.Add(current);
|
||||
}
|
||||
|
||||
private static IShrinkMod CreateInstance(DiscoveredMod discovered)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (IShrinkMod)Activator.CreateInstance(discovered.EntryType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"实例化模组 {discovered.Info.ModId} 失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunLifecycle(IEnumerable<ShrinkModHandle> handles, bool verboseLogging)
|
||||
{
|
||||
var stagedHandles = handles.ToList();
|
||||
RunPhase(stagedHandles, ShrinkModState.Constructed, verboseLogging, (mod, context) => mod.OnConstruct(context), "构造");
|
||||
RunPhase(stagedHandles, ShrinkModState.ContentRegistered, verboseLogging,
|
||||
(mod, context) => mod.OnRegisterContent(context), "注册内容");
|
||||
RegisterOptionalRuntimeIntegrations(stagedHandles, verboseLogging);
|
||||
RunPhase(stagedHandles, ShrinkModState.Initialized, verboseLogging,
|
||||
(mod, context) => mod.OnInitialize(context), "初始化");
|
||||
RunPhase(stagedHandles, ShrinkModState.Ready, verboseLogging, (mod, context) => mod.OnReady(context), "完成就绪");
|
||||
}
|
||||
|
||||
private static void RegisterOptionalRuntimeIntegrations(IEnumerable<ShrinkModHandle> handles, bool verboseLogging)
|
||||
{
|
||||
foreach (var handle in handles)
|
||||
ShrinkModOptionalRuntimeIntegration.RegisterModInstance(handle.Instance, handle.Info, verboseLogging);
|
||||
|
||||
ShrinkModOptionalRuntimeIntegration.RefreshGlobalBindings(verboseLogging);
|
||||
}
|
||||
|
||||
private static void RunPhase(IEnumerable<ShrinkModHandle> handles, ShrinkModState targetState, bool verboseLogging,
|
||||
Action<IShrinkMod, ShrinkModContext> callback, string phaseName)
|
||||
{
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
var context = new ShrinkModContext(handle.Info, RegistryManager, LoadedMods, verboseLogging);
|
||||
|
||||
try
|
||||
{
|
||||
callback(handle.Instance, context);
|
||||
handle.State = targetState;
|
||||
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModLoader] {handle.Info.ModId} 已完成阶段:{phaseName}");
|
||||
|
||||
if (targetState == ShrinkModState.Ready)
|
||||
OnModReady?.Invoke(handle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"模组 {handle.Info.ModId} 在阶段 '{phaseName}' 执行失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldScanAssembly(Assembly assembly, string[] prefixes)
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
if (name.StartsWith("Unity", StringComparison.Ordinal) ||
|
||||
name.StartsWith("System", StringComparison.Ordinal) ||
|
||||
name.StartsWith("mscorlib", StringComparison.Ordinal) ||
|
||||
name.StartsWith("netstandard", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (prefixes == null || prefixes.Length == 0)
|
||||
return true;
|
||||
|
||||
for (var i = 0; i < prefixes.Length; i++)
|
||||
{
|
||||
var prefix = prefixes[i];
|
||||
if (!string.IsNullOrWhiteSpace(prefix) &&
|
||||
name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetTypesSafely(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.Where(t => t != null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f1310a972c773f479f8e35265c63535
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c93eed41ccfddbd48b13b1791393a0c8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ShrinkModAttribute : Attribute
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string DisplayName { get; }
|
||||
public string Version { get; }
|
||||
public int LoadOrder { get; }
|
||||
public bool AutoApplyHarmonyPatches { get; set; } = true;
|
||||
|
||||
public ShrinkModAttribute(string modId, string displayName, string version, int loadOrder = 0)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("模组 ID 不能为空。", nameof(modId));
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
throw new ArgumentException("模组显示名不能为空。", nameof(displayName));
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
throw new ArgumentException("模组版本不能为空。", nameof(version));
|
||||
|
||||
ModId = modId.Trim();
|
||||
DisplayName = displayName.Trim();
|
||||
Version = version.Trim();
|
||||
LoadOrder = loadOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 976e55706c1035f4ba692957cd335f32
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class ShrinkModDependencyAttribute : Attribute
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string MinimumVersion { get; }
|
||||
public bool Optional { get; }
|
||||
|
||||
public ShrinkModDependencyAttribute(string modId, string minimumVersion = null, bool optional = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("依赖模组 ID 不能为空。", nameof(modId));
|
||||
|
||||
ModId = modId.Trim();
|
||||
MinimumVersion = string.IsNullOrWhiteSpace(minimumVersion) ? null : minimumVersion.Trim();
|
||||
Optional = optional;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4af615bbf25f3e429a341accf01b17b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class ShrinkModInfo
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string DisplayName { get; }
|
||||
public string Version { get; }
|
||||
public int LoadOrder { get; }
|
||||
public Type EntryType { get; }
|
||||
public bool AutoApplyHarmonyPatches { get; }
|
||||
public IReadOnlyList<ShrinkModDependency> Dependencies { get; }
|
||||
|
||||
public ShrinkModInfo(string modId, string displayName, string version, int loadOrder, Type entryType,
|
||||
bool autoApplyHarmonyPatches, IReadOnlyList<ShrinkModDependency> dependencies)
|
||||
{
|
||||
ModId = modId;
|
||||
DisplayName = displayName;
|
||||
Version = version;
|
||||
LoadOrder = loadOrder;
|
||||
EntryType = entryType;
|
||||
AutoApplyHarmonyPatches = autoApplyHarmonyPatches;
|
||||
Dependencies = dependencies;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DisplayName} ({ModId}@{Version})";
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class ShrinkModDependency
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string MinimumVersion { get; }
|
||||
public bool Optional { get; }
|
||||
|
||||
public ShrinkModDependency(string modId, string minimumVersion, bool optional)
|
||||
{
|
||||
ModId = modId;
|
||||
MinimumVersion = minimumVersion;
|
||||
Optional = optional;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(MinimumVersion))
|
||||
return Optional ? $"{ModId} (optional)" : ModId;
|
||||
|
||||
return Optional ? $"{ModId} >= {MinimumVersion} (optional)" : $"{ModId} >= {MinimumVersion}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6497d32d6e9e7084aac7d2c0c2996a78
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public enum ShrinkModState
|
||||
{
|
||||
Discovered = 0,
|
||||
Constructed = 1,
|
||||
ContentRegistered = 2,
|
||||
Initialized = 3,
|
||||
Ready = 4,
|
||||
Unloaded = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf0b4d15f9b6083489f0b8a954447c17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkVersionUtility
|
||||
{
|
||||
public static int Compare(string left, string right)
|
||||
{
|
||||
if (ReferenceEquals(left, right)) return 0;
|
||||
if (left == null) return -1;
|
||||
if (right == null) return 1;
|
||||
|
||||
var leftParts = left.Split('.');
|
||||
var rightParts = right.Split('.');
|
||||
var max = Math.Max(leftParts.Length, rightParts.Length);
|
||||
|
||||
for (var i = 0; i < max; i++)
|
||||
{
|
||||
var leftValue = i < leftParts.Length ? ParseSegment(leftParts[i]) : 0;
|
||||
var rightValue = i < rightParts.Length ? ParseSegment(rightParts[i]) : 0;
|
||||
|
||||
if (leftValue != rightValue)
|
||||
return leftValue.CompareTo(rightValue);
|
||||
}
|
||||
|
||||
return string.CompareOrdinal(left, right);
|
||||
}
|
||||
|
||||
private static int ParseSegment(string segment)
|
||||
{
|
||||
if (string.IsNullOrEmpty(segment))
|
||||
return 0;
|
||||
|
||||
var digits = "";
|
||||
for (var i = 0; i < segment.Length; i++)
|
||||
{
|
||||
if (!char.IsDigit(segment[i]))
|
||||
break;
|
||||
digits += segment[i];
|
||||
}
|
||||
|
||||
return int.TryParse(digits, out var value) ? value : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6fd43faddede97247997c7738c55fba6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f7e9a9f13efee44d98e217d8e6b6a70
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public interface IShrinkModNetworkTransport
|
||||
{
|
||||
bool IsServer { get; }
|
||||
bool IsClient { get; }
|
||||
|
||||
event Action<ShrinkModNetworkEnvelope> OnEnvelopeReceived;
|
||||
|
||||
void Send(ShrinkModNetworkEnvelope envelope);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70281ce2f2d7b0d41bc4206503df87ff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public static class ShrinkModNetworkManager
|
||||
{
|
||||
private sealed class HandlerRegistration
|
||||
{
|
||||
public Type PayloadType;
|
||||
public Action<ShrinkModNetworkMessageContext, object> Handler;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, HandlerRegistration> Handlers = new(StringComparer.Ordinal);
|
||||
private static IShrinkModNetworkTransport _transport;
|
||||
private static bool _enabled = true;
|
||||
private static bool _verboseLogging;
|
||||
|
||||
public static bool HasTransport => _transport != null;
|
||||
|
||||
public static void Configure(bool enabled, bool verboseLogging)
|
||||
{
|
||||
_enabled = enabled;
|
||||
_verboseLogging = verboseLogging;
|
||||
}
|
||||
|
||||
public static void SetTransport(IShrinkModNetworkTransport transport)
|
||||
{
|
||||
if (_transport != null)
|
||||
_transport.OnEnvelopeReceived -= OnEnvelopeReceived;
|
||||
|
||||
_transport = transport;
|
||||
|
||||
if (_transport != null)
|
||||
{
|
||||
_transport.OnEnvelopeReceived += OnEnvelopeReceived;
|
||||
if (_verboseLogging)
|
||||
Debug.Log("[ShrinkModNetwork] 已绑定网络传输层。");
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterHandler<T>(string modId, string channel, Action<ShrinkModNetworkMessageContext, T> handler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("模组 ID 不能为空。", nameof(modId));
|
||||
if (string.IsNullOrWhiteSpace(channel))
|
||||
throw new ArgumentException("频道名不能为空。", nameof(channel));
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
var key = BuildKey(modId, channel);
|
||||
if (Handlers.ContainsKey(key))
|
||||
throw new InvalidOperationException($"网络频道已注册:{key}");
|
||||
|
||||
Handlers.Add(key, new HandlerRegistration
|
||||
{
|
||||
PayloadType = typeof(T),
|
||||
Handler = (context, payload) => handler(context, (T)payload)
|
||||
});
|
||||
}
|
||||
|
||||
public static void SendToServer<T>(string modId, string channel, T payload, string senderPeerId = null)
|
||||
{
|
||||
SendInternal(modId, channel, payload, senderPeerId, null, ShrinkModNetworkTarget.Server);
|
||||
}
|
||||
|
||||
public static void SendToAllClients<T>(string modId, string channel, T payload, string senderPeerId = null)
|
||||
{
|
||||
SendInternal(modId, channel, payload, senderPeerId, null, ShrinkModNetworkTarget.AllClients);
|
||||
}
|
||||
|
||||
public static void SendToClient<T>(string modId, string channel, T payload, string targetPeerId, string senderPeerId = null)
|
||||
{
|
||||
SendInternal(modId, channel, payload, senderPeerId, targetPeerId, ShrinkModNetworkTarget.SpecificClient);
|
||||
}
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
Handlers.Clear();
|
||||
SetTransport(null);
|
||||
_enabled = true;
|
||||
_verboseLogging = false;
|
||||
}
|
||||
|
||||
internal static void UnregisterHandlers(string modId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
return;
|
||||
|
||||
var prefix = modId.Trim() + "::";
|
||||
var keys = new List<string>();
|
||||
foreach (var key in Handlers.Keys)
|
||||
{
|
||||
if (key.StartsWith(prefix, StringComparison.Ordinal))
|
||||
keys.Add(key);
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
Handlers.Remove(key);
|
||||
}
|
||||
|
||||
private static void SendInternal<T>(string modId, string channel, T payload, string senderPeerId, string targetPeerId,
|
||||
ShrinkModNetworkTarget target)
|
||||
{
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
if (_transport == null)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModNetwork] 当前没有传输层,消息 {modId}/{channel} 被丢弃。");
|
||||
return;
|
||||
}
|
||||
|
||||
var envelope = new ShrinkModNetworkEnvelope
|
||||
{
|
||||
ModId = modId,
|
||||
Channel = channel,
|
||||
PayloadJson = JsonConvert.SerializeObject(payload),
|
||||
SenderPeerId = senderPeerId,
|
||||
TargetPeerId = targetPeerId,
|
||||
Target = target
|
||||
};
|
||||
|
||||
_transport.Send(envelope);
|
||||
}
|
||||
|
||||
private static void OnEnvelopeReceived(ShrinkModNetworkEnvelope envelope)
|
||||
{
|
||||
if (!_enabled || envelope == null)
|
||||
return;
|
||||
|
||||
var key = BuildKey(envelope.ModId, envelope.Channel);
|
||||
if (!Handlers.TryGetValue(key, out var registration))
|
||||
{
|
||||
if (_verboseLogging)
|
||||
Debug.LogWarning($"[ShrinkModNetwork] 未找到频道处理器:{key}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var payload = JsonConvert.DeserializeObject(envelope.PayloadJson, registration.PayloadType);
|
||||
var context = new ShrinkModNetworkMessageContext(
|
||||
envelope.SenderPeerId,
|
||||
envelope.TargetPeerId,
|
||||
envelope.Target,
|
||||
_transport != null && _transport.IsServer,
|
||||
_transport != null && _transport.IsClient);
|
||||
|
||||
registration.Handler(context, payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ShrinkModNetwork] 处理频道 {key} 失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildKey(string modId, string channel)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("模组 ID 不能为空。", nameof(modId));
|
||||
if (string.IsNullOrWhiteSpace(channel))
|
||||
throw new ArgumentException("频道名不能为空。", nameof(channel));
|
||||
|
||||
return $"{modId.Trim()}::{channel.Trim()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e1949e4943d16d4db129182316c8ed3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public enum ShrinkModNetworkTarget
|
||||
{
|
||||
Server = 0,
|
||||
AllClients = 1,
|
||||
SpecificClient = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class ShrinkModNetworkEnvelope
|
||||
{
|
||||
public string ModId;
|
||||
public string Channel;
|
||||
public string PayloadJson;
|
||||
public string SenderPeerId;
|
||||
public string TargetPeerId;
|
||||
public ShrinkModNetworkTarget Target;
|
||||
}
|
||||
|
||||
public readonly struct ShrinkModNetworkMessageContext
|
||||
{
|
||||
public string SenderPeerId { get; }
|
||||
public string TargetPeerId { get; }
|
||||
public ShrinkModNetworkTarget Target { get; }
|
||||
public bool IsServer { get; }
|
||||
public bool IsClient { get; }
|
||||
|
||||
public ShrinkModNetworkMessageContext(string senderPeerId, string targetPeerId, ShrinkModNetworkTarget target,
|
||||
bool isServer, bool isClient)
|
||||
{
|
||||
SenderPeerId = senderPeerId;
|
||||
TargetPeerId = targetPeerId;
|
||||
Target = target;
|
||||
IsServer = isServer;
|
||||
IsClient = isClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 010bc5b7cb953c642b9af5eed638f83c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f988f5d97206da947a5d8210522370e2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal interface IShrinkOwnedRegistry
|
||||
{
|
||||
void RemoveOwnedEntries(string ownerModId);
|
||||
}
|
||||
|
||||
public sealed class ShrinkModRegistry<T> : IShrinkOwnedRegistry
|
||||
{
|
||||
private readonly Dictionary<string, ShrinkRegistryEntry<T>> _entries = new();
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
internal ShrinkModRegistry(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("注册表名称不能为空。", nameof(name));
|
||||
|
||||
Name = name.Trim();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries => _entries.Values.ToArray();
|
||||
|
||||
public void Register(string ownerModId, string key, T value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
throw new ArgumentException("归属模组 ID 不能为空。", nameof(ownerModId));
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("注册键不能为空。", nameof(key));
|
||||
|
||||
key = key.Trim();
|
||||
if (_entries.ContainsKey(key))
|
||||
throw new InvalidOperationException($"注册表 '{Name}' 中已存在键 '{key}'。");
|
||||
|
||||
_entries.Add(key, new ShrinkRegistryEntry<T>(ownerModId.Trim(), key, value));
|
||||
}
|
||||
|
||||
public bool Contains(string key) => !string.IsNullOrWhiteSpace(key) && _entries.ContainsKey(key.Trim());
|
||||
|
||||
public bool TryGet(string key, out T value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key) && _entries.TryGetValue(key.Trim(), out var entry))
|
||||
{
|
||||
value = entry.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetEntry(string key, out ShrinkRegistryEntry<T> entry)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
return _entries.TryGetValue(key.Trim(), out entry);
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
void IShrinkOwnedRegistry.RemoveOwnedEntries(string ownerModId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
return;
|
||||
|
||||
var keys = _entries
|
||||
.Where(pair => string.Equals(pair.Value.OwnerModId, ownerModId, StringComparison.Ordinal))
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
foreach (var key in keys)
|
||||
_entries.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ShrinkRegistryEntry<T>
|
||||
{
|
||||
public string OwnerModId { get; }
|
||||
public string Key { get; }
|
||||
public T Value { get; }
|
||||
|
||||
public ShrinkRegistryEntry(string ownerModId, string key, T value)
|
||||
{
|
||||
OwnerModId = ownerModId;
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cb75a17a6ee08f479d0be1fe145307f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModRegistryManager
|
||||
{
|
||||
private readonly Dictionary<string, object> _registries = new();
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
return (ShrinkModRegistry<T>)existing;
|
||||
|
||||
var registry = new ShrinkModRegistry<T>(name);
|
||||
_registries.Add(compositeKey, registry);
|
||||
return registry;
|
||||
}
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
{
|
||||
registry = (ShrinkModRegistry<T>)existing;
|
||||
return true;
|
||||
}
|
||||
|
||||
registry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void Clear() => _registries.Clear();
|
||||
|
||||
internal void RemoveOwnedEntries(string ownerModId)
|
||||
{
|
||||
foreach (var registry in _registries.Values)
|
||||
{
|
||||
if (registry is IShrinkOwnedRegistry ownedRegistry)
|
||||
ownedRegistry.RemoveOwnedEntries(ownerModId);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCompositeKey(Type type, string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("注册表名称不能为空。", nameof(name));
|
||||
|
||||
return $"{type.AssemblyQualifiedName}::{name.Trim()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa8f924fbddf4e544a8531969765b217
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ShrinkModFramework.Runtime",
|
||||
"rootNamespace": "ShrinkModFramework",
|
||||
"references": [
|
||||
"Newtonsoft.Json",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eac56219787481141b1f9a9f40a1af62
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user