feat(packages): 内置 SDK 包并完善 ContextLoader 集成

- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档
- 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理
- 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言
- 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则
- 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护
- 新增 Network 语义扫描、模板保护和 App 组件生命周期测试
- 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试
- 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
This commit is contained in:
2026-08-18 18:06:34 +08:00
parent 517c4cf46e
commit d74c2f08ca
240 changed files with 13647 additions and 545 deletions
@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusDispatchTests
{
private class PlainEvent : EventBase
{
}
private sealed class DerivedPlainEvent : PlainEvent
{
}
[Cancelable]
private sealed class CancelableEvent : EventBase
{
}
[HasResult]
private sealed class ResultEvent : EventBase
{
}
[Test]
public void TriggerEvent_ReturnsFalseWithoutSubscribers_TrueWhenHandled()
{
var bus = EventBus.CreateBus();
Assert.IsFalse(bus.TriggerEvent(new PlainEvent()));
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
Assert.IsTrue(bus.TriggerEvent(new PlainEvent()));
}
[Test]
public void CanceledEvent_SkipsHandlersUnlessReceiveCanceled()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<CancelableEvent>(evt =>
{
order.Add("canceler");
evt.SetCanceled(true);
}, EventPriority.HIGHEST);
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
bus.RegisterEvent<CancelableEvent>(evt => order.Add($"monitor:{evt.IsCanceled}"),
EventPriority.MONITOR, receiveCanceled: true);
var evt = new CancelableEvent();
bus.TriggerEvent(evt);
Assert.IsTrue(evt.IsCanceled);
CollectionAssert.AreEqual(new[] { "canceler", "monitor:True" }, order);
}
[Test]
public void SetCanceled_OnNonCancelableEvent_ThrowsWithMessage()
{
var evt = new PlainEvent();
var ex = Assert.Throws<UnsupportedOperationException>(() => evt.SetCanceled(true));
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
}
[Test]
public void SetResult_OnEventWithoutResult_ThrowsWithMessage()
{
var evt = new PlainEvent();
var ex = Assert.Throws<InvalidOperationException>(() => evt.SetResult(EventResult.ALLOW));
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
}
[Test]
public void SetResult_OnResultEvent_PersistsResult()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<ResultEvent>(evt => evt.SetResult(EventResult.DENY), EventPriority.HIGH);
var evt = new ResultEvent();
bus.TriggerEvent(evt);
Assert.AreEqual(EventResult.DENY, evt.Result);
}
[Test]
public void ParentSubscriber_ReceivesDerivedEvent()
{
var bus = EventBus.CreateBus();
var hits = 0;
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
bus.TriggerEvent(new DerivedPlainEvent());
Assert.AreEqual(1, hits);
}
[Test]
public void ParentSubscriber_RegisteredAfterChildWasTriggered_StillReceivesChild()
{
var bus = EventBus.CreateBus();
// 先触发子事件,让子类型的监听列表先于父监听器创建(覆盖脏传播路径)
bus.TriggerEvent(new DerivedPlainEvent());
var hits = 0;
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
bus.TriggerEvent(new DerivedPlainEvent());
Assert.AreEqual(1, hits);
}
[Test]
public void PerPhaseDispatch_OnlyRunsRequestedPhase()
{
var bus = EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
var order = new List<string>();
bus.RegisterEvent<PlainEvent>(_ => order.Add("high"), EventPriority.HIGH);
bus.RegisterEvent<PlainEvent>(_ => order.Add("normal"), EventPriority.NORMAL);
bus.TriggerEvent(EventPriority.HIGH, new PlainEvent());
CollectionAssert.AreEqual(new[] { "high" }, order);
}
[Test]
public void PerPhaseDispatch_ThrowsWhenNotEnabled()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.TriggerEvent(EventPriority.HIGH, new PlainEvent()));
}
[Test]
public void TriggerEventAsync_RunsHandlersSequentiallyByPriority()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
async UniTask AsyncHandler(PlainEvent evt)
{
order.Add("async-high");
await UniTask.CompletedTask;
}
bus.RegisterEvent<PlainEvent>(AsyncHandler, EventPriority.HIGH, receiveCanceled: false);
bus.RegisterEvent<PlainEvent>(_ => order.Add("sync-normal"), EventPriority.NORMAL);
var handled = bus.TriggerEventAsync(new PlainEvent()).GetAwaiter().GetResult();
Assert.IsTrue(handled);
CollectionAssert.AreEqual(new[] { "async-high", "sync-normal" }, order);
}
[Test]
public void TriggerEventAsync_AsyncHandlerCancellation_SkipsLaterHandlers()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
async UniTask CancelingHandler(CancelableEvent evt)
{
evt.SetCanceled(true);
order.Add("async-canceler");
await UniTask.CompletedTask;
}
bus.RegisterEvent<CancelableEvent>(CancelingHandler, EventPriority.HIGHEST, receiveCanceled: false);
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
bus.TriggerEventAsync(new CancelableEvent()).GetAwaiter().GetResult();
CollectionAssert.AreEqual(new[] { "async-canceler" }, order);
}
[Test]
public void GetSubscribers_ReturnsDefensiveCopyOfDispatchSnapshot()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
var evt = new PlainEvent();
bus.TriggerEvent(evt);
var first = evt.GetSubscribers();
Assert.AreEqual(1, first.Length);
first[0] = null;
var second = evt.GetSubscribers();
Assert.IsNotNull(second[0]);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 11b7dddad0d215b47a7249e74a574105
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using System.Collections.Generic;
using NUnit.Framework;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusPriorityTests
{
private class BaseEvent : EventBase
{
}
private sealed class DerivedEvent : BaseEvent
{
}
[Test]
public void EnumPriority_ExecutesPhasesInOrder()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("LOW"), EventPriority.LOW);
bus.RegisterEvent<BaseEvent>(_ => order.Add("MONITOR"), EventPriority.MONITOR);
bus.RegisterEvent<BaseEvent>(_ => order.Add("HIGHEST"), EventPriority.HIGHEST);
bus.RegisterEvent<BaseEvent>(_ => order.Add("NORMAL"), EventPriority.NORMAL);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "HIGHEST", "NORMAL", "LOW", "MONITOR" }, order);
}
[Test]
public void NumericPriority_HigherNumberRunsFirstWithinPhase()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
// 5 与 10 都映射到 NORMAL 档
bus.RegisterEvent<BaseEvent>(_ => order.Add("p5"), 5);
bus.RegisterEvent<BaseEvent>(_ => order.Add("p10"), 10);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "p10", "p5" }, order);
}
[Test]
public void NumericPriority_TieKeepsRegistrationOrder()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("first"), 0);
bus.RegisterEvent<BaseEvent>(_ => order.Add("second"), 0);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "first", "second" }, order);
}
[Test]
public void NumericPriorityZero_MapsToNormalPhase()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<BaseEvent>(_ => { }, 0);
var subscribers = bus.GetEventSubscribers<BaseEvent>();
Assert.AreEqual(1, subscribers.Length);
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
}
[Test]
public void RegisterEvent_WithoutPriority_ResolvesToNormal()
{
var bus = EventBus.CreateBus();
void Handler(BaseEvent evt)
{
}
// 不带优先级的调用应唯一解析到枚举重载(NORMAL),不再有重载二义性
bus.RegisterEvent<BaseEvent>(Handler);
var subscribers = bus.GetEventSubscribers<BaseEvent>();
Assert.AreEqual(1, subscribers.Length);
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
}
[Test]
public void ParentAndChildHandlers_MergeByNumericPriorityWithinPhase()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent10"), 10);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child5"), 5);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child1"), 1);
bus.TriggerEvent(new DerivedEvent());
CollectionAssert.AreEqual(new[] { "parent10", "child5", "child1" }, order);
}
[Test]
public void ParentAndChildHandlers_TiePrefersChild()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent0"), 0);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child0"), 0);
bus.TriggerEvent(new DerivedEvent());
CollectionAssert.AreEqual(new[] { "child0", "parent0" }, order);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc534f0e8598cab47b393ccffc90ad98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,228 @@
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusRegistrationTests
{
private class ProbeEvent : EventBase
{
}
private sealed class PooledEvent : EventBase
{
public int Value { get; set; }
protected override void OnReset()
{
Value = 0;
}
}
private sealed class InstanceSubscriber
{
public int Hits;
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt)
{
Hits++;
}
}
private static class StaticSubscriber
{
public static int Hits;
[EventSubscribe(EventPriority.NORMAL)]
private static void OnProbe(ProbeEvent evt)
{
Hits++;
}
}
private sealed class InvalidSignatureSubscriber
{
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt, int extra)
{
}
}
[EventBusSubscriber]
private sealed class EmptySubscriber
{
}
private sealed class UnattributedSubscriber
{
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt)
{
}
}
[Test]
public void Register_InstanceScan_RegistersAndUnregisters()
{
var bus = EventBus.CreateBus();
var subscriber = new InstanceSubscriber();
bus.Register(subscriber);
Assert.IsTrue(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
bus.Unregister(subscriber);
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
}
[Test]
public void Register_StaticTypeScan_RegistersAndUnregisters()
{
var bus = EventBus.CreateBus();
StaticSubscriber.Hits = 0;
bus.Register(typeof(StaticSubscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
bus.Unregister(typeof(StaticSubscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
}
[Test]
public void Register_MethodInfo_RegistersSingleHandler()
{
var bus = EventBus.CreateBus();
StaticSubscriber.Hits = 0;
var method = typeof(StaticSubscriber).GetMethod("OnProbe",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.IsNotNull(method);
bus.Register(method);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
bus.Unregister(method);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
}
[Test]
public void Register_InvalidHandlerSignature_Throws()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.Register(new InvalidSignatureSubscriber()));
}
[Test]
public void AutoRegister_TypeWithoutSubscriberAttribute_Throws()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.AutoRegister(new UnattributedSubscriber()));
}
[Test]
public void AutoRegister_SubscriberWithoutHandlers_WarnsInsteadOfThrowing()
{
var bus = EventBus.CreateBus();
LogAssert.Expect(LogType.Warning, new Regex(@"has no \[EventSubscribe\] methods"));
Assert.DoesNotThrow(() => bus.AutoRegister(new EmptySubscriber()));
}
[Test]
public void GetEventSubscribers_ReturnsDefensiveCopy()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<ProbeEvent>(_ => { }, EventPriority.NORMAL);
var first = bus.GetEventSubscribers<ProbeEvent>();
Assert.AreEqual(1, first.Length);
first[0] = null;
var second = bus.GetEventSubscribers<ProbeEvent>();
Assert.IsNotNull(second[0]);
}
[Test]
public void UnregisterAllEventsForObject_RemovesScannedHandlers()
{
var bus = EventBus.CreateBus();
var subscriber = new InstanceSubscriber();
bus.Register(subscriber);
bus.UnregisterAllEventsForObject(subscriber);
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(0, subscriber.Hits);
bus.Register(subscriber);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
}
[Test]
public void EventPool_ReleaseResetsAndReusesInstance()
{
var evt = EventPool<PooledEvent>.Get();
evt.Value = 42;
EventPool<PooledEvent>.Release(evt);
var reused = EventPool<PooledEvent>.Get();
Assert.AreSame(evt, reused);
Assert.AreEqual(0, reused.Value);
EventPool<PooledEvent>.Release(reused);
}
[Test]
public void EventPool_DoubleRelease_DoesNotDuplicatePoolEntry()
{
var evt = EventPool<PooledEvent>.Get();
EventPool<PooledEvent>.Release(evt);
EventPool<PooledEvent>.Release(evt);
var first = EventPool<PooledEvent>.Get();
var second = EventPool<PooledEvent>.Get();
Assert.AreSame(evt, first);
Assert.AreNotSame(evt, second);
EventPool<PooledEvent>.Release(first);
EventPool<PooledEvent>.Release(second);
}
[Test]
public void EventPool_DisposeReturnsToPool()
{
PooledEvent captured;
using (var evt = EventPool<PooledEvent>.Get())
{
captured = evt;
evt.Value = 7;
}
var reused = EventPool<PooledEvent>.Get();
Assert.AreSame(captured, reused);
Assert.AreEqual(0, reused.Value);
EventPool<PooledEvent>.Release(reused);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4570cac2813143c40b9a8edeadf19057
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using NUnit.Framework;
using UnityEngine;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusSubscriptionTests
{
private sealed class SampleEvent : EventBase
{
public int Value { get; set; }
}
[SetUp]
public void SetUp()
{
EventBus.UnregisterAllEvents();
}
[TearDown]
public void TearDown()
{
EventBus.UnregisterAllEvents();
}
[Test]
public void SubscribeEvent_Dispose_RemovesOnlyOwnedSubscription()
{
var firstHits = 0;
var secondHits = 0;
using var first = EventBus.SubscribeEvent<SampleEvent>(_ => firstHits++);
using var second = EventBus.SubscribeEvent<SampleEvent>(_ => secondHits++);
EventBus.TriggerEvent(new SampleEvent());
Assert.AreEqual(1, firstHits);
Assert.AreEqual(1, secondHits);
first.Dispose();
EventBus.TriggerEvent(new SampleEvent());
Assert.AreEqual(1, firstHits);
Assert.AreEqual(2, secondHits);
}
[Test]
public void ActiveSubscriptionSnapshot_ContainsExpectedMetadata()
{
using var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => { }, EventPriority.HIGH, true);
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
Assert.AreEqual(1, snapshots.Count);
Assert.AreEqual(subscription.SubscriptionId, snapshots[0].SubscriptionId);
Assert.AreEqual(typeof(SampleEvent), snapshots[0].EventType);
Assert.AreEqual(EventPriority.HIGH, snapshots[0].Priority);
Assert.IsTrue(snapshots[0].ReceiveCanceled);
Assert.IsFalse(string.IsNullOrWhiteSpace(snapshots[0].MethodName));
}
[Test]
public void RegisterEvent_StillWorksWithoutDisposableSubscription()
{
var hits = 0;
void Handler(SampleEvent evt)
{
hits += evt.Value;
}
EventBus.RegisterEvent<SampleEvent>(Handler);
EventBus.TriggerEvent(new SampleEvent { Value = 3 });
Assert.AreEqual(3, hits);
EventBus.UnregisterEvent<SampleEvent>(Handler);
EventBus.TriggerEvent(new SampleEvent { Value = 5 });
Assert.AreEqual(3, hits);
}
[Test]
public void ActiveSubscriptionSnapshot_EmptyAfterDispose()
{
var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => Debug.Log("noop"));
subscription.Dispose();
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
Assert.AreEqual(0, snapshots.Count);
Assert.IsTrue(subscription.IsDisposed);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5ea531619ceaeb46af4f56454624ef7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
{
"name": "ShrinkEventBus.Tests",
"rootNamespace": "ShrinkEventBus.Tests",
"references": [
"ShrinkEventBus.Runtime",
"UniTask",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8c47d36b8919bbf4d9c39817eaf577a9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: