feat(cordis): 接入上下文组合与模组事务热替换

This commit is contained in:
2026-08-16 23:20:40 +08:00
commit ad256f109b
676 changed files with 52168 additions and 0 deletions
@@ -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;
}
}
@@ -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:
@@ -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);
}
}
}
@@ -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;
}
}
}
}
@@ -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;
}
}
}
@@ -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: