chore: initialize standalone UPM package
Publish UPM package / publish (push) Failing after 1s

This commit is contained in:
2026-08-26 02:50:31 +08:00
commit 78ccae3e07
142 changed files with 6572 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using ShrinkContext;
using ShrinkEventBus;
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;
IDisposable eventBinding = null;
ctx.Effect(
() => eventBinding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
instance.GetType(), instance, handle.Info.ModId, _host.VerboseLogging),
() =>
{
eventBinding?.Dispose();
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
});
// 模组网络 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:
+262
View File
@@ -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 IReadOnlyShrinkModRegistry<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:
+68
View File
@@ -0,0 +1,68 @@
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);
}
var revisionState = ShrinkExternalModAssemblyLoader.CaptureState();
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging);
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} 个。");
return _host.Mods;
}
public static IReadOnlyShrinkModRegistry<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: