feat(mod): 完成外部 DLL 事务热替换
This commit is contained in:
@@ -2,6 +2,23 @@
|
||||
|
||||
本文件记录 `ShrinkModFramework` 在当前工作区中的包内变更。
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 新增 `ShrinkModContextHost` / `ShrinkModComponentSource`,把模组生命周期、Registry、Network handler 与 Harmony lease 纳入可逆组件事务。
|
||||
- 新增真实外部 DLL fixture 测试,覆盖失败替换恢复、损坏文件、依赖缺失、watcher 去抖与删除卸载。
|
||||
|
||||
### Changed
|
||||
|
||||
- `ShrinkModLoader` 默认使用 ContextHost;外部 DLL 以 SHA-256 作为 revision,同内容幂等。
|
||||
- watcher 监听新增、修改、删除与重命名,并把 burst 文件事件折叠为一次主线程组合提交。
|
||||
|
||||
### Fixed
|
||||
|
||||
- 新模组 apply 失败时同时恢复旧组件组合和外部 DLL revision 缓存,避免失败程序集继续被当作当前版本。
|
||||
- 删除 DLL 时同步清理程序集解析路径缓存;EditMode 创建 watcher driver 时不再调用 `DontDestroyOnLoad`。
|
||||
|
||||
## [0.1.0] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -207,7 +207,7 @@ ShrinkModLoader.LoadNewExternalMods();
|
||||
默认会扫描当前 DLL revision,并把新增、替换、删除映射为一个完整期望组合;
|
||||
变更事务失败时保留旧模组组合。设置 `useContextHost = false` 才回退为仅新增 DLL 的旧路径。
|
||||
|
||||
如果 `watchExternalModsDirectory = true`,框架还会自动监听外部模组目录并在主线程延迟触发增量加载。
|
||||
如果 `watchExternalModsDirectory = true`,框架还会监听新增、修改、删除、重命名事件,经过主线程 debouncer 后提交一次完整组合;同一 burst 内的中间坏文件不会覆盖当前有效 revision。
|
||||
|
||||
## Harmony 热补丁
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ShrinkModFramework.Tests")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4db6b958232258a439bebcb315ee304d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>把文件系统的 burst 事件折叠为一次主线程 reload。</summary>
|
||||
internal sealed class ShrinkModReloadDebouncer
|
||||
{
|
||||
private bool _pending;
|
||||
private float _reloadAt;
|
||||
|
||||
public bool IsPending => _pending;
|
||||
|
||||
public void Schedule(float now, float delaySeconds)
|
||||
{
|
||||
_pending = true;
|
||||
_reloadAt = now + Math.Max(0.05f, delaySeconds);
|
||||
}
|
||||
|
||||
public bool TryConsume(float now)
|
||||
{
|
||||
if (!_pending || now < _reloadAt)
|
||||
return false;
|
||||
|
||||
_pending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_pending = false;
|
||||
_reloadAt = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec113876ec861b64cb8391eed726108b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,10 +11,9 @@ namespace ShrinkModFramework
|
||||
private static ShrinkModRuntimeDriver _instance;
|
||||
|
||||
private readonly ConcurrentQueue<Action> _mainThreadActions = new();
|
||||
private readonly ShrinkModReloadDebouncer _externalReloadDebouncer = new();
|
||||
private FileSystemWatcher _watcher;
|
||||
private ShrinkModFrameworkSettings _settings;
|
||||
private bool _pendingExternalReload;
|
||||
private float _reloadAtRealtime;
|
||||
|
||||
public static void EnsureCreated(ShrinkModFrameworkSettings settings)
|
||||
{
|
||||
@@ -25,7 +24,8 @@ namespace ShrinkModFramework
|
||||
}
|
||||
|
||||
var go = new GameObject("ShrinkModRuntimeDriver");
|
||||
DontDestroyOnLoad(go);
|
||||
if (Application.isPlaying)
|
||||
DontDestroyOnLoad(go);
|
||||
_instance = go.AddComponent<ShrinkModRuntimeDriver>();
|
||||
_instance.ApplySettings(settings);
|
||||
}
|
||||
@@ -41,6 +41,12 @@ namespace ShrinkModFramework
|
||||
_instance._mainThreadActions.Enqueue(action);
|
||||
}
|
||||
|
||||
internal static ShrinkModRuntimeDriver InstanceForTesting => _instance;
|
||||
|
||||
internal bool HasWatcherForTesting => _watcher != null;
|
||||
|
||||
internal void PumpForTesting() => Update();
|
||||
|
||||
private void Update()
|
||||
{
|
||||
while (_mainThreadActions.TryDequeue(out var action))
|
||||
@@ -55,11 +61,9 @@ namespace ShrinkModFramework
|
||||
}
|
||||
}
|
||||
|
||||
if (!_pendingExternalReload || Time.realtimeSinceStartup < _reloadAtRealtime)
|
||||
if (!_externalReloadDebouncer.TryConsume(Time.realtimeSinceStartup))
|
||||
return;
|
||||
|
||||
_pendingExternalReload = false;
|
||||
|
||||
try
|
||||
{
|
||||
ShrinkModLoader.LoadNewExternalMods(_settings);
|
||||
@@ -117,6 +121,7 @@ namespace ShrinkModFramework
|
||||
};
|
||||
_watcher.Created += OnExternalModsChanged;
|
||||
_watcher.Changed += OnExternalModsChanged;
|
||||
_watcher.Deleted += OnExternalModsChanged;
|
||||
_watcher.Renamed += OnExternalModsRenamed;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
@@ -135,6 +140,7 @@ namespace ShrinkModFramework
|
||||
|
||||
private void OnExternalModsRenamed(object sender, RenamedEventArgs args)
|
||||
{
|
||||
ScheduleExternalReload(args.OldFullPath);
|
||||
ScheduleExternalReload(args.FullPath);
|
||||
}
|
||||
|
||||
@@ -145,9 +151,8 @@ namespace ShrinkModFramework
|
||||
|
||||
_mainThreadActions.Enqueue(() =>
|
||||
{
|
||||
_pendingExternalReload = true;
|
||||
var delay = _settings ? Mathf.Max(0.05f, _settings.externalModsReloadDelaySeconds) : 0.5f;
|
||||
_reloadAtRealtime = Time.realtimeSinceStartup + delay;
|
||||
_externalReloadDebouncer.Schedule(Time.realtimeSinceStartup, delay);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,6 +164,7 @@ namespace ShrinkModFramework
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Created -= OnExternalModsChanged;
|
||||
_watcher.Changed -= OnExternalModsChanged;
|
||||
_watcher.Deleted -= OnExternalModsChanged;
|
||||
_watcher.Renamed -= OnExternalModsRenamed;
|
||||
_watcher.Dispose();
|
||||
_watcher = null;
|
||||
|
||||
@@ -30,9 +30,20 @@ namespace ShrinkModFramework
|
||||
verboseLogging);
|
||||
}
|
||||
|
||||
var revisionState = ShrinkExternalModAssemblyLoader.CaptureState();
|
||||
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging);
|
||||
var sources = ShrinkModComponentDiscovery.Discover(settings, verboseLogging);
|
||||
await _host.ApplyAsync(sources);
|
||||
try
|
||||
{
|
||||
var sources = ShrinkModComponentDiscovery.Discover(settings, verboseLogging);
|
||||
await _host.ApplyAsync(sources);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// DLL revision discovery is part of the same composition transaction:
|
||||
// a failing replacement must not make the failed assembly current.
|
||||
ShrinkExternalModAssemblyLoader.RestoreState(revisionState);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (verboseLogging)
|
||||
UnityEngine.Debug.Log($"[ShrinkModFramework] Cordis 模组组合已提交,共 {_host.Mods.Count} 个。");
|
||||
|
||||
+49
-1
@@ -17,6 +17,14 @@ namespace ShrinkModFramework
|
||||
public Assembly Assembly;
|
||||
}
|
||||
|
||||
internal sealed class RevisionState
|
||||
{
|
||||
public Dictionary<string, ExternalAssemblyRevision> Current =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
public Dictionary<string, string> Known =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, ExternalAssemblyRevision> CurrentAssemblyRevisions =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<Assembly> ExternalAssemblyHistory = new();
|
||||
@@ -101,6 +109,12 @@ namespace ShrinkModFramework
|
||||
.ToArray())
|
||||
{
|
||||
CurrentAssemblyRevisions.Remove(missingPath);
|
||||
var assemblyName = Path.GetFileNameWithoutExtension(missingPath);
|
||||
if (KnownAssemblyFiles.TryGetValue(assemblyName, out var knownPath) &&
|
||||
string.Equals(knownPath, missingPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
KnownAssemblyFiles.Remove(assemblyName);
|
||||
}
|
||||
}
|
||||
|
||||
return CurrentAssemblyRevisions.Values
|
||||
@@ -112,6 +126,35 @@ namespace ShrinkModFramework
|
||||
internal static bool IsExternalAssembly(Assembly assembly) =>
|
||||
assembly != null && ExternalAssemblyHistory.Contains(assembly);
|
||||
|
||||
internal static RevisionState CaptureState()
|
||||
{
|
||||
return new RevisionState
|
||||
{
|
||||
Current = CurrentAssemblyRevisions.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase),
|
||||
Known = KnownAssemblyFiles.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
}
|
||||
|
||||
internal static void RestoreState(RevisionState state)
|
||||
{
|
||||
if (state == null)
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
|
||||
CurrentAssemblyRevisions.Clear();
|
||||
foreach (var pair in state.Current)
|
||||
CurrentAssemblyRevisions[pair.Key] = pair.Value;
|
||||
|
||||
KnownAssemblyFiles.Clear();
|
||||
foreach (var pair in state.Known)
|
||||
KnownAssemblyFiles[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
internal static bool TryGetCurrentRevision(Assembly assembly, out string revision)
|
||||
{
|
||||
foreach (var current in CurrentAssemblyRevisions.Values)
|
||||
@@ -138,7 +181,6 @@ namespace ShrinkModFramework
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
CurrentAssemblyRevisions.Clear();
|
||||
ExternalAssemblyHistory.Clear();
|
||||
KnownAssemblyFiles.Clear();
|
||||
if (_resolveRegistered)
|
||||
{
|
||||
@@ -147,6 +189,12 @@ namespace ShrinkModFramework
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
ResetForTesting();
|
||||
ExternalAssemblyHistory.Clear();
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace ShrinkModFramework
|
||||
OnModReady = null;
|
||||
OnAllModsReady = null;
|
||||
ShrinkModNetworkManager.ResetForDomainReload();
|
||||
ShrinkExternalModAssemblyLoader.ResetForTesting();
|
||||
ShrinkExternalModAssemblyLoader.ResetForDomainReload();
|
||||
ShrinkHarmonyPatchService.ResetForTesting();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39d24561b9507f340abccb25be811c21
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd75db4a8f23f7428ff7dc0e796a5c0
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97a7806bf8820b94e83cc2011c09bc43
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8517a178ed20f2a45b30d4c45fefe2c5
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a3fede5ca621bd4e9ecb0dc59e5925e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5af400c8aafe04428decd12e66face9
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkContext;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace ShrinkModFramework.Tests
|
||||
@@ -197,6 +201,146 @@ namespace ShrinkModFramework.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var target = Path.Combine(directory, "ExternalFixture.Mod.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
|
||||
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
var restoredGeneration = ShrinkModLoader.Mods["external.fixture"].Generation;
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V2.Failing.dll.bytes", target);
|
||||
var transaction = Assert.Throws<ShrinkModTransactionException>(() =>
|
||||
ShrinkModLoader.LoadNewExternalMods(settings));
|
||||
|
||||
Assert.IsTrue(transaction!.PreviousCompositionRestored);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
Assert.AreNotEqual(0, restoredGeneration);
|
||||
Assert.Greater(ShrinkModLoader.Mods["external.fixture"].Generation, restoredGeneration);
|
||||
var failedReplacementGeneration = ShrinkModLoader.Mods["external.fixture"].Generation;
|
||||
|
||||
File.WriteAllBytes(target, new byte[] { 0, 1, 2, 3, 4, 5 });
|
||||
LogAssert.Expect(LogType.Error,
|
||||
new Regex(@"\[ShrinkModFramework\] 加载外部 DLL 失败:.*ExternalFixture\.Mod\.dll"));
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
Assert.AreEqual(failedReplacementGeneration,
|
||||
ShrinkModLoader.Mods["external.fixture"].Generation,
|
||||
"坏 DLL 不应让失败 revision 重新成为当前 source");
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", target);
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
AssertExternalFixture("3.0.0", "v3");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllWatcher_DebouncesBurstAndHandlesDeletion()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
settings.externalModsReloadDelaySeconds = 0.05f;
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var target = Path.Combine(directory, "ExternalFixture.Mod.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
ShrinkModRuntimeDriver.EnsureCreated(settings);
|
||||
Assert.IsTrue(ShrinkModRuntimeDriver.InstanceForTesting!.HasWatcherForTesting);
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
|
||||
// Two writes arrive as one debounce window; only the final valid revision should commit.
|
||||
CopyFixture("ExternalFixture.Mod.V2.Failing.dll.bytes", target);
|
||||
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", target);
|
||||
Assert.IsTrue(PumpDriverUntil(() =>
|
||||
ShrinkModLoader.Mods.TryGetValue("external.fixture", out var handle) &&
|
||||
handle.Info.Version == "3.0.0"),
|
||||
"watcher 应在 burst 后提交最后一个有效 revision");
|
||||
AssertExternalFixture("3.0.0", "v3");
|
||||
|
||||
File.Delete(target);
|
||||
Assert.IsTrue(PumpDriverUntil(() => ShrinkModLoader.Mods.Count == 0),
|
||||
"删除 DLL 应触发当前 source 卸载");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllDependencyRemoval_RestoresPreviousComposition()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var providerPath = Path.Combine(directory, "ExternalFixture.Provider.dll");
|
||||
var consumerPath = Path.Combine(directory, "ExternalFixture.Consumer.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath);
|
||||
CopyFixture("ExternalFixture.Consumer.dll.bytes", consumerPath);
|
||||
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State);
|
||||
|
||||
File.Delete(providerPath);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
ShrinkModLoader.LoadNewExternalMods(settings));
|
||||
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count,
|
||||
"依赖导入失败发生在协调前,旧组合必须保持 active");
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State);
|
||||
|
||||
CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath);
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalReloadDebouncer_CoalescesBurstAndUsesMinimumDelay()
|
||||
{
|
||||
var debouncer = new ShrinkModReloadDebouncer();
|
||||
|
||||
debouncer.Schedule(10f, 0f);
|
||||
Assert.IsTrue(debouncer.IsPending);
|
||||
Assert.IsFalse(debouncer.TryConsume(10.049f));
|
||||
|
||||
debouncer.Schedule(10.04f, 0.5f);
|
||||
Assert.IsFalse(debouncer.TryConsume(10.539f));
|
||||
Assert.IsTrue(debouncer.TryConsume(10.54f));
|
||||
Assert.IsFalse(debouncer.IsPending);
|
||||
Assert.IsFalse(debouncer.TryConsume(11f));
|
||||
}
|
||||
|
||||
private static ShrinkModComponentSource Source(string id, string version, string revision,
|
||||
Func<IShrinkMod> factory, params ShrinkModDependency[] dependencies)
|
||||
{
|
||||
@@ -224,6 +368,75 @@ namespace ShrinkModFramework.Tests
|
||||
method!.Invoke(null, null);
|
||||
}
|
||||
|
||||
private static void ResetExternalRuntime()
|
||||
{
|
||||
if (ShrinkModRuntimeDriver.InstanceForTesting != null)
|
||||
Object.DestroyImmediate(ShrinkModRuntimeDriver.InstanceForTesting.gameObject);
|
||||
ShrinkModLoader.ResetForTesting();
|
||||
}
|
||||
|
||||
private static ShrinkModFrameworkSettings CreateExternalSettings()
|
||||
{
|
||||
var settings = ScriptableObject.CreateInstance<ShrinkModFrameworkSettings>();
|
||||
settings.useContextHost = true;
|
||||
settings.enableExternalDllMods = true;
|
||||
settings.autoCreateExternalModsDirectory = false;
|
||||
settings.externalModsFolderName = "ShrinkModFrameworkTests_" + Guid.NewGuid().ToString("N");
|
||||
settings.assemblyNamePrefixes = new[] { "ExternalFixture." };
|
||||
settings.enableHarmonyPatching = false;
|
||||
settings.enableNetworkSync = false;
|
||||
settings.verboseLogging = false;
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static void CleanupExternalRuntime(ShrinkModFrameworkSettings settings, string directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShrinkModRuntimeDriver.InstanceForTesting != null)
|
||||
Object.DestroyImmediate(ShrinkModRuntimeDriver.InstanceForTesting.gameObject);
|
||||
ShrinkModLoader.ResetForTesting();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
Directory.Delete(directory, true);
|
||||
Object.DestroyImmediate(settings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyFixture(string fixtureName, string targetPath)
|
||||
{
|
||||
var fixturePath = Path.Combine(Application.dataPath, "Modules", "ShrinkModFramework",
|
||||
"Tests", "Fixtures", fixtureName);
|
||||
File.Copy(fixturePath, targetPath, true);
|
||||
}
|
||||
|
||||
private static void AssertExternalFixture(string version, string registryValue)
|
||||
{
|
||||
Assert.IsTrue(ShrinkModLoader.Mods.TryGetValue("external.fixture", out var handle));
|
||||
Assert.AreEqual(version, handle!.Info.Version);
|
||||
Assert.AreEqual(ShrinkModState.Ready, handle.State);
|
||||
|
||||
var registry = ShrinkModLoader.GetOrCreateRegistry<string>("external.fixture");
|
||||
Assert.IsTrue(registry.TryGet("external.fixture:version", out var value));
|
||||
Assert.AreEqual(registryValue, value);
|
||||
}
|
||||
|
||||
private static bool PumpDriverUntil(Func<bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
ShrinkModRuntimeDriver.InstanceForTesting?.PumpForTesting();
|
||||
if (predicate())
|
||||
return true;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
ShrinkModRuntimeDriver.InstanceForTesting?.PumpForTesting();
|
||||
return predicate();
|
||||
}
|
||||
|
||||
private sealed class DelegateMod : IShrinkMod
|
||||
{
|
||||
private readonly Action<ShrinkModContext>? _onConstruct;
|
||||
|
||||
Reference in New Issue
Block a user