This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
name: Publish UPM package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Fetch tagged revision
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
ref="${{ gitea.sha }}"
|
||||||
|
test -n "$ref"
|
||||||
|
git init .
|
||||||
|
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git"
|
||||||
|
git fetch --depth=1 origin "$ref"
|
||||||
|
git checkout --detach FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Validate immutable release version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
tag="$(git describe --exact-match --tags HEAD)"
|
||||||
|
version="$(node -p "require('./package.json').version")"
|
||||||
|
test "$tag" = "v$version"
|
||||||
|
npm pack --dry-run
|
||||||
|
|
||||||
|
- name: Publish to ShrinkSDK registry
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
: "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
|
||||||
|
npmrc="$HOME/.npmrc"
|
||||||
|
cleanup() { rm -f "$npmrc"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
printf '%s\n' \
|
||||||
|
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
|
||||||
|
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
|
||||||
|
npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Verify standalone Unity package
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
editmode:
|
||||||
|
runs-on: unity-2022.3.62f3
|
||||||
|
container:
|
||||||
|
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
|
||||||
|
volumes:
|
||||||
|
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
|
||||||
|
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
|
||||||
|
steps:
|
||||||
|
- name: Fetch selected revision
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
ref="${{ gitea.sha }}"
|
||||||
|
git init .
|
||||||
|
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git"
|
||||||
|
git fetch --depth=1 origin "$ref"
|
||||||
|
git checkout --detach FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Run package EditMode tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
|
||||||
|
test -n "$unity_bin"
|
||||||
|
"$unity_bin" \
|
||||||
|
-batchmode \
|
||||||
|
-nographics \
|
||||||
|
-quit \
|
||||||
|
-projectPath "$PWD/Development~/UnityProject" \
|
||||||
|
-runTests \
|
||||||
|
-testPlatform EditMode \
|
||||||
|
-testResults "$PWD/TestResults/editmode.xml" \
|
||||||
|
-logFile "$PWD/TestResults/unity.log"
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# This .gitignore file should be placed at the root of your Unity project directory
|
||||||
|
#
|
||||||
|
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
|
||||||
|
#
|
||||||
|
.utmp/
|
||||||
|
/[Ll]ibrary/
|
||||||
|
/[Tt]emp/
|
||||||
|
/[Oo]bj/
|
||||||
|
/[Bb]uild/
|
||||||
|
/[Bb]uilds/
|
||||||
|
/[Ll]ogs/
|
||||||
|
/[Uu]ser[Ss]ettings/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
|
||||||
|
*.blend1
|
||||||
|
*.blend1.meta
|
||||||
|
|
||||||
|
# MemoryCaptures can get excessive in size.
|
||||||
|
# They also could contain extremely sensitive data
|
||||||
|
/[Mm]emoryCaptures/
|
||||||
|
|
||||||
|
# Recordings can get excessive in size
|
||||||
|
/[Rr]ecordings/
|
||||||
|
|
||||||
|
# Uncomment this line if you wish to ignore the asset store tools plugin
|
||||||
|
# /[Aa]ssets/AssetStoreTools*
|
||||||
|
|
||||||
|
# Autogenerated Jetbrains Rider plugin
|
||||||
|
/[Aa]ssets/Plugins/Editor/JetBrains*
|
||||||
|
# Jetbrains Rider personal-layer settings
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# Visual Studio cache directory
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# Gradle cache directory
|
||||||
|
.gradle/
|
||||||
|
|
||||||
|
# Autogenerated VS/MD/Consulo solution and project files
|
||||||
|
ExportedObj/
|
||||||
|
.consulo/
|
||||||
|
*.csproj
|
||||||
|
*.unityproj
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.tmp
|
||||||
|
*.user
|
||||||
|
*.userprefs
|
||||||
|
*.pidb
|
||||||
|
*.booproj
|
||||||
|
*.svd
|
||||||
|
*.pdb
|
||||||
|
*.mdb
|
||||||
|
*.opendb
|
||||||
|
*.VC.db
|
||||||
|
|
||||||
|
# Unity3D generated meta files
|
||||||
|
*.pidb.meta
|
||||||
|
*.pdb.meta
|
||||||
|
*.mdb.meta
|
||||||
|
|
||||||
|
# Unity3D generated file on crash reports
|
||||||
|
sysinfo.txt
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Builds
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.unitypackage
|
||||||
|
*.unitypackage.meta
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# Crashlytics generated file
|
||||||
|
crashlytics-build.properties
|
||||||
|
|
||||||
|
# TestRunner generated files
|
||||||
|
InitTestScene*.unity*
|
||||||
|
|
||||||
|
# Addressables default ignores, before user customizations
|
||||||
|
/ServerData
|
||||||
|
/[Aa]ssets/StreamingAssets/aa*
|
||||||
|
/[Aa]ssets/AddressableAssetsData/link.xml*
|
||||||
|
/[Aa]ssets/Addressables_Temp*
|
||||||
|
# By default, Addressables content builds will generate addressables_content_state.bin
|
||||||
|
# files in platform-specific subfolders, for example:
|
||||||
|
# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin
|
||||||
|
/[Aa]ssets/AddressableAssetsData/*/*.bin*
|
||||||
|
|
||||||
|
# Visual Scripting auto-generated files
|
||||||
|
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
|
||||||
|
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
|
||||||
|
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
|
||||||
|
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
|
||||||
|
|
||||||
|
# Auto-generated scenes by play mode tests
|
||||||
|
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
|
||||||
|
/Development~/UnityProject/[Ll]ibrary/
|
||||||
|
/Development~/UnityProject/[Tt]emp/
|
||||||
|
/Development~/UnityProject/[Oo]bj/
|
||||||
|
/Development~/UnityProject/[Ll]ogs/
|
||||||
|
/Development~/UnityProject/[Uu]ser[Ss]ettings/
|
||||||
|
/Development~/UnityProject/TestResults/
|
||||||
|
/Tools~/**/[Bb]in/
|
||||||
|
/Tools~/**/[Oo]bj/
|
||||||
|
*.user
|
||||||
|
*.DotSettings.user
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.git/
|
||||||
|
.gitea/
|
||||||
|
Development~/
|
||||||
|
Tools~/
|
||||||
|
*.csproj
|
||||||
|
*.sln
|
||||||
|
*.user
|
||||||
|
*.DotSettings.user
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
ShrinkEventBus 2.0 Benchmark Report
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Date: 2026-08-24
|
||||||
|
Unity: 2022.3.62f3
|
||||||
|
Platform: WindowsEditor, Play Mode
|
||||||
|
Host: independent ShrinkEventBusHost, Inline scheduler
|
||||||
|
Diagnostics: independent Host; detailed global capture does not observe it
|
||||||
|
Final run: iter=10,000,000 mass=1,000,000 reg=5,000, three rounds
|
||||||
|
|
||||||
|
Median results
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Post / 0 handlers
|
||||||
|
129,282,649 ops/s
|
||||||
|
|
||||||
|
Post / 1 handler
|
||||||
|
67,249,586 ops/s
|
||||||
|
|
||||||
|
Post / 8 handlers, struct payload
|
||||||
|
35,024,798 ops/s
|
||||||
|
|
||||||
|
Post / 8 handlers, class payload
|
||||||
|
25,321,199 ops/s
|
||||||
|
|
||||||
|
Post / 30 handlers
|
||||||
|
10,823,612 ops/s
|
||||||
|
|
||||||
|
Post / canceled skips 10
|
||||||
|
9,029,517 ops/s
|
||||||
|
|
||||||
|
PostAsync / 1 async handler
|
||||||
|
5,909,446 ops/s
|
||||||
|
|
||||||
|
Generated Attach + Dispose
|
||||||
|
674,291 ops/s
|
||||||
|
|
||||||
|
Struct hot path allocation
|
||||||
|
0 B / 10,000,000 Post operations
|
||||||
|
|
||||||
|
Comparison
|
||||||
|
----------
|
||||||
|
|
||||||
|
Supplied MessagePipe chart, class payload, 8 handlers:
|
||||||
|
25,639,260 publish/s
|
||||||
|
|
||||||
|
ShrinkEventBus 2.0, class payload, 8 handlers:
|
||||||
|
25,321,199 publish/s median
|
||||||
|
|
||||||
|
ShrinkEventBus 2.0, struct payload, 8 handlers:
|
||||||
|
35,024,798 publish/s median
|
||||||
|
|
||||||
|
Supplied ShrinkEventBus 1.3.0 sample:
|
||||||
|
0 handlers: 8,971,378 -> 129,282,649 ops/s
|
||||||
|
1 handler: 6,132,242 -> 67,249,586 ops/s
|
||||||
|
30 handlers: 258,805 -> 10,823,612 ops/s
|
||||||
|
canceled skips 10: 1,894,776 -> 9,029,517 ops/s
|
||||||
|
1 async handler: 1,168,634 -> 5,909,446 ops/s
|
||||||
|
instance scan/register: 76,353 -> 674,291 generated Attach/s
|
||||||
|
|
||||||
|
Implementation notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
- Inline buses use a dedicated implementation so the JIT can inline the channel dispatch path.
|
||||||
|
- Non-cancelable, all-sync channels build a priority-ordered multicast dispatcher when subscriptions change.
|
||||||
|
- ShrinkPostResult stores Accepted/Handled/Canceled/Failure in one 32-bit value.
|
||||||
|
- Generic bus slots and handler snapshots are published with release semantics and read without a publish lock.
|
||||||
|
- Mixed sync/async channels, cancelable events, queued schedulers and PostAsync retain the full semantic path.
|
||||||
|
|
||||||
|
Results are local editor measurements, not cross-device guarantees. The supplied MessagePipe chart was
|
||||||
|
produced by a different runtime and machine; the class-payload comparison only establishes the same
|
||||||
|
order of throughput. Recheck on the target Player and hardware before release.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6e379caaacb945699a297286798bc4e2
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2cc73e57d6ec4457a425843f734b6a70
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
╔══════════════════════════════════════════════════╗
|
||||||
|
║ ShrinkEventBus Benchmark Report ║
|
||||||
|
║ iter= 1000000 mass= 10000 reg= 5000 scan=1000 ║
|
||||||
|
╚══════════════════════════════════════════════════╝
|
||||||
|
基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启)
|
||||||
|
采集于 1.3.0(EventBase 瘦身后;Unity 2022.3.62f3, Editor Play Mode)
|
||||||
|
|
||||||
|
▶ TriggerEvent × 无订阅者(基线)
|
||||||
|
总耗时 : 111.466 ms
|
||||||
|
单次 : 0.1115 μs/op
|
||||||
|
吞吐量 : 8971378 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × 单订阅者同步
|
||||||
|
总耗时 : 163.073 ms
|
||||||
|
单次 : 0.1631 μs/op
|
||||||
|
吞吐量 : 6132242 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × 父事件监听子事件
|
||||||
|
总耗时 : 171.424 ms
|
||||||
|
单次 : 0.1714 μs/op
|
||||||
|
吞吐量 : 5833479 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × 30 订阅者同步
|
||||||
|
总耗时 : 38.639 ms
|
||||||
|
单次 : 3.8639 μs/op
|
||||||
|
吞吐量 : 258805 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent(Phase=HIGH) × 3 phase 监听
|
||||||
|
总耗时 : 164.197 ms
|
||||||
|
单次 : 0.1642 μs/op
|
||||||
|
吞吐量 : 6090238 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEventAsync × 单订阅者异步
|
||||||
|
总耗时 : 0.856 ms
|
||||||
|
单次 : 0.8557 μs/op
|
||||||
|
吞吐量 : 1168634 ops/s
|
||||||
|
|
||||||
|
▶ RegisterEvent(delegate)
|
||||||
|
总耗时 : 14.418 ms
|
||||||
|
单次 : 2.8836 μs/op
|
||||||
|
吞吐量 : 346791 ops/s
|
||||||
|
|
||||||
|
▶ UnregisterEvent(delegate)
|
||||||
|
总耗时 : 3.427 ms
|
||||||
|
单次 : 0.6853 μs/op
|
||||||
|
吞吐量 : 1459130 ops/s
|
||||||
|
|
||||||
|
▶ Register/Unregister(instance scan)
|
||||||
|
总耗时 : 13.097 ms
|
||||||
|
单次 : 13.0971 μs/op
|
||||||
|
吞吐量 : 76353 ops/s
|
||||||
|
|
||||||
|
▶ Register/Unregister(static type scan)
|
||||||
|
总耗时 : 9.337 ms
|
||||||
|
单次 : 9.3374 μs/op
|
||||||
|
吞吐量 : 107096 ops/s
|
||||||
|
|
||||||
|
▶ Register/Unregister(MethodInfo)
|
||||||
|
总耗时 : 6.513 ms
|
||||||
|
单次 : 6.5134 μs/op
|
||||||
|
吞吐量 : 153530 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × new() [GC Gen0= 1]
|
||||||
|
总耗时 : 266.536 ms
|
||||||
|
单次 : 0.2665 μs/op
|
||||||
|
吞吐量 : 3751843 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × Pool.Get() [GC Gen0= 0]
|
||||||
|
总耗时 : 242.015 ms
|
||||||
|
单次 : 0.2420 μs/op
|
||||||
|
吞吐量 : 4131982 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × 已取消事件跳过(10 订阅者)
|
||||||
|
总耗时 : 52.777 ms
|
||||||
|
单次 : 0.5278 μs/op
|
||||||
|
吞吐量 : 1894776 ops/s
|
||||||
|
|
||||||
|
══════════════════════════════════════════════════
|
||||||
|
全部测试完成
|
||||||
|
|
||||||
|
对比 1.1.x 旧样本:new() 路径 7684ms / GC Gen0=6457 → 266ms / GC Gen0=1
|
||||||
|
(EventBase 不再每实例分配内置 ListenerList,new() 路径 GC 基本归零,
|
||||||
|
对象池从“高频必需”降级为可选优化。)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 87ccd659dba774447b9d6941761b8300
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ShrinkEventBus.Benchmark",
|
||||||
|
"rootNamespace": "ShrinkEventBus.Benchmark",
|
||||||
|
"references": [
|
||||||
|
"ShrinkEventBus.Runtime",
|
||||||
|
"UniTask"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4488cfe64b374f30b8081ce2f10b1ed5
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.Benchmark
|
||||||
|
{
|
||||||
|
public static class ShrinkEventBusBenchmark
|
||||||
|
{
|
||||||
|
private readonly struct BenchmarkEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public BenchmarkEvent(int value) => Value = value;
|
||||||
|
public int Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BenchmarkClassEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CanceledBenchmarkEvent : IShrinkCancelableEvent
|
||||||
|
{
|
||||||
|
public bool IsCanceled { get; private set; } = true;
|
||||||
|
public void SetCanceled(bool value) => IsCanceled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class OneHandler
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:one")]
|
||||||
|
private void H01(BenchmarkEvent value) => Consume(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class EightHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H01(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H02(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H03(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H04(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H05(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H06(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H07(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight")] private void H08(BenchmarkEvent value) => Consume(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class EightClassHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H01(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H02(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H03(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H04(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H05(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H06(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H07(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:eight-class")] private void H08(BenchmarkClassEvent value) => Consume(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class ThirtyHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H01(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H02(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H03(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H04(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H05(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H06(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H07(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H08(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H09(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H10(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H11(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H12(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H13(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H14(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H15(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H16(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H17(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H18(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H19(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H20(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H21(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H22(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H23(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H24(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H25(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H26(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H27(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H28(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H29(BenchmarkEvent value) => Consume(value);
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:thirty")] private void H30(BenchmarkEvent value) => Consume(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class CanceledHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H01(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H02(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H03(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H04(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H05(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H06(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H07(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H08(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H09(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:canceled")] private void H10(CanceledBenchmarkEvent value) => _sink++;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class AsyncHandler
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:async")]
|
||||||
|
private UniTask Handle(BenchmarkEvent value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Consume(value);
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
private sealed class RegistrationHandler
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Bus = "benchmark:registration")]
|
||||||
|
private void Handle(BenchmarkEvent value) => Consume(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int _sink;
|
||||||
|
|
||||||
|
public static string Run(int iterations = 1_000_000, int massIterations = 10_000,
|
||||||
|
int registrationIterations = 5_000)
|
||||||
|
{
|
||||||
|
var keys = new[]
|
||||||
|
{
|
||||||
|
new ShrinkBusKey("benchmark", "empty"),
|
||||||
|
new ShrinkBusKey("benchmark", "one"),
|
||||||
|
new ShrinkBusKey("benchmark", "eight"),
|
||||||
|
new ShrinkBusKey("benchmark", "eight-class"),
|
||||||
|
new ShrinkBusKey("benchmark", "thirty"),
|
||||||
|
new ShrinkBusKey("benchmark", "canceled"),
|
||||||
|
new ShrinkBusKey("benchmark", "async"),
|
||||||
|
new ShrinkBusKey("benchmark", "registration")
|
||||||
|
};
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var empty = host.CreateBus(keys[0], ShrinkBusOptions.Inline());
|
||||||
|
var one = host.CreateBus(keys[1], ShrinkBusOptions.Inline());
|
||||||
|
var eight = host.CreateBus(keys[2], ShrinkBusOptions.Inline());
|
||||||
|
var eightClass = host.CreateBus(keys[3], ShrinkBusOptions.Inline());
|
||||||
|
var thirty = host.CreateBus(keys[4], ShrinkBusOptions.Inline());
|
||||||
|
var canceled = host.CreateBus(keys[5], ShrinkBusOptions.Inline());
|
||||||
|
var asyncBus = host.CreateBus(keys[6], ShrinkBusOptions.Inline());
|
||||||
|
host.CreateBus(keys[7], ShrinkBusOptions.Inline());
|
||||||
|
|
||||||
|
using var oneBinding = host.Attach(new OneHandler());
|
||||||
|
using var eightBinding = host.Attach(new EightHandlers());
|
||||||
|
using var eightClassBinding = host.Attach(new EightClassHandlers());
|
||||||
|
using var thirtyBinding = host.Attach(new ThirtyHandlers());
|
||||||
|
using var canceledBinding = host.Attach(new CanceledHandlers());
|
||||||
|
using var asyncBinding = host.Attach(new AsyncHandler());
|
||||||
|
|
||||||
|
var report = new StringBuilder(2048);
|
||||||
|
report.AppendLine("ShrinkEventBus 2.0 Benchmark Report");
|
||||||
|
report.AppendLine($"iter={iterations} mass={massIterations} reg={registrationIterations}");
|
||||||
|
report.AppendLine($"Unity {Application.unityVersion}, {Application.platform}, PlayMode={Application.isPlaying}");
|
||||||
|
report.AppendLine();
|
||||||
|
|
||||||
|
var value = new BenchmarkEvent(1);
|
||||||
|
var classValue = new BenchmarkClassEvent { Value = 1 };
|
||||||
|
var canceledValue = new CanceledBenchmarkEvent();
|
||||||
|
Warmup(empty, one, eight, eightClass, thirty, canceled,
|
||||||
|
value, classValue, canceledValue);
|
||||||
|
|
||||||
|
MeasurePost(report, "Post / 0 handlers", iterations, empty, in value);
|
||||||
|
MeasurePost(report, "Post / 1 handler", iterations, one, in value);
|
||||||
|
MeasurePost(report, "Post / 8 handlers", massIterations, eight, in value);
|
||||||
|
MeasurePost(report, "Post / 8 handlers class", massIterations, eightClass, in classValue);
|
||||||
|
MeasurePost(report, "Post / 30 handlers", massIterations, thirty, in value);
|
||||||
|
MeasurePost(report, "Post / canceled skips 10", massIterations,
|
||||||
|
canceled, in canceledValue);
|
||||||
|
MeasureAsync(report, "PostAsync / 1 async handler", massIterations,
|
||||||
|
() => asyncBus.PostAsync(value));
|
||||||
|
Measure(report, "Generated Attach + Dispose", registrationIterations, () =>
|
||||||
|
{
|
||||||
|
host.Attach(new RegistrationHandler()).Dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
one.Post(in value);
|
||||||
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
report.AppendLine($"Post / struct hot path allocation : {allocated} B / {iterations} ops");
|
||||||
|
report.AppendLine($"sink={_sink}");
|
||||||
|
|
||||||
|
var result = report.ToString();
|
||||||
|
UnityEngine.Debug.Log(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Warmup(IShrinkEventBus empty, IShrinkEventBus one,
|
||||||
|
IShrinkEventBus eight, IShrinkEventBus eightClass, IShrinkEventBus thirty,
|
||||||
|
IShrinkEventBus canceled, BenchmarkEvent value, BenchmarkClassEvent classValue,
|
||||||
|
CanceledBenchmarkEvent canceledValue)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10_000; i++)
|
||||||
|
{
|
||||||
|
empty.Post(in value);
|
||||||
|
one.Post(in value);
|
||||||
|
eight.Post(in value);
|
||||||
|
eightClass.Post(classValue);
|
||||||
|
thirty.Post(in value);
|
||||||
|
canceled.Post(canceledValue);
|
||||||
|
}
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Measure(StringBuilder report, string name, int iterations, Action action)
|
||||||
|
{
|
||||||
|
var gen0 = GC.CollectionCount(0);
|
||||||
|
var beforeBytes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
action();
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds,
|
||||||
|
GC.GetAllocatedBytesForCurrentThread() - beforeBytes,
|
||||||
|
GC.CollectionCount(0) - gen0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MeasurePost<TEvent>(StringBuilder report, string name, int iterations,
|
||||||
|
IShrinkEventBus bus, in TEvent eventData) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var gen0 = GC.CollectionCount(0);
|
||||||
|
var beforeBytes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
bus.Post(in eventData);
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds,
|
||||||
|
GC.GetAllocatedBytesForCurrentThread() - beforeBytes,
|
||||||
|
GC.CollectionCount(0) - gen0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MeasureAsync(StringBuilder report, string name, int iterations,
|
||||||
|
Func<UniTask<ShrinkPostResult>> action)
|
||||||
|
{
|
||||||
|
var gen0 = GC.CollectionCount(0);
|
||||||
|
var beforeBytes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
action().GetAwaiter().GetResult();
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppendResult(report, name, iterations, stopwatch.Elapsed.TotalMilliseconds,
|
||||||
|
GC.GetAllocatedBytesForCurrentThread() - beforeBytes,
|
||||||
|
GC.CollectionCount(0) - gen0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendResult(StringBuilder report, string name, int iterations,
|
||||||
|
double milliseconds, long allocatedBytes, int gen0Collections)
|
||||||
|
{
|
||||||
|
var microseconds = milliseconds * 1000d / iterations;
|
||||||
|
var throughput = iterations / (milliseconds / 1000d);
|
||||||
|
report.AppendLine($"{name,-34} {milliseconds,10:F3} ms {microseconds,9:F4} us/op " +
|
||||||
|
$"{throughput,12:F0} ops/s alloc={allocatedBytes} B gen0={gen0Collections}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Consume(BenchmarkEvent value)
|
||||||
|
{
|
||||||
|
_sink = unchecked(_sink + value.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Consume(BenchmarkClassEvent value)
|
||||||
|
{
|
||||||
|
_sink = unchecked(_sink + value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 76b9239259544977bd27221d3607aed1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 新增统一 `IShrinkEvent` 契约、`ShrinkEventSubscriber` / `ShrinkSubscribe` 特性与 `Post` / `PostAsync` 入口。
|
||||||
|
- 新增命名多 Bus:Game、Scene、Mod、World、Server 及自定义 `ShrinkBusKey`。
|
||||||
|
- Bus 创建时可选择 Inline、Unity MainThread、DedicatedThread 或 TaskPool 调度器,并支持有界队列、溢出策略、最大并发与关闭排空。
|
||||||
|
- Unity 中异步 handler 与调度优先使用 UniTask;新增 `ShrinkMonoEventScope` 管理普通 MonoBehaviour 的绑定生命周期。
|
||||||
|
- ILPostProcessor 为特性标记的普通 C# / Mono 类型生成 `IShrinkGeneratedSubscriber` 强类型绑定,发布热路径不使用反射调用。
|
||||||
|
- 新增可选 `com.cneicy.shrink-eventbus-entities` 包,Burst Job 通过 NativeQueue writer/playback 进入同一 Bus。
|
||||||
|
- 新增 `Tools~/DotNet/ShrinkEventBus.Core` ValueTask 运行时与 Roslyn incremental generator,纯 .NET 宿主无需 Unity 或 UniTask。
|
||||||
|
- EventBus 可视化调试器使用 UI Toolkit 重写,新增 Bus 导航、实时指标、分发耗时、线程/执行模式/结果详情、问题筛选、跟随和 CSV 导出;详细采样只在窗口主动采集时启用。
|
||||||
|
- 新增独立 Play Mode benchmark,覆盖 0/1/8/30 handler、取消、异步和生成绑定开销。
|
||||||
|
- Inline Bus 使用专用运行时实现,非取消全同步 channel 在订阅变化时生成有序 multicast dispatcher;`ShrinkPostResult` 压缩为 32-bit 状态,保持 API 不变。
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- 删除 `EventBase`、`EventBusSubscriber` / `EventSubscribe`、`RegisterEvent` / `SubscribeEvent` / `TriggerEvent`、手工 Delegate 注册和旧反射通道。
|
||||||
|
- 所有事件改为实现 `IShrinkEvent`;取消和结果分别实现 `IShrinkCancelableEvent`、`IShrinkResultEvent<TResult>`。
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-06-12
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- 移除 `EventBase.GetListenerList()`。事件对象不再内置 `ListenerList`,改为持有派发时的快照数组;调试请使用 `GetSubscribers()`(现在返回拷贝)。
|
||||||
|
- 数字优先级 `0` 现在映射到 `NORMAL`(原为 `LOW`),与枚举重载的默认值保持一致。
|
||||||
|
- 所有 int 数字优先级重载(`RegisterEvent` / `SubscribeEvent`)不再提供默认值 `0`,必须显式传入数字——消除与枚举重载之间的重载二义性。不带优先级的调用现在唯一解析到枚举重载(`NORMAL`)。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 织入前会检查类型自身及基类链上是否存在实例 `[EventSubscribe]` 方法,没有的类型不再织入,避免在 `Awake` 中抛出注册异常。
|
||||||
|
- `AutoRegister`(含 Mod 框架反射路径)遇到没有任何匹配 `[EventSubscribe]` 方法的类型时,改为输出警告并跳过,不再抛异常;显式 `Register()` 仍保持严格抛错。
|
||||||
|
- `ListenerList` 父子链脏标记传播与快照重建之间的竞态:同一总线内所有监听列表(含父子链)现共用一把锁。
|
||||||
|
- `RegisterCore` 的 check-then-act 竞态(并发下同一实例可能被注册两次)。
|
||||||
|
- `EventPool.Release` 的双重归还竞态;对象池增加容量上限(128),超出直接丢弃。
|
||||||
|
- 父子事件同 phase 内的 handler 顺序:现在按数字优先级跨父子稳定归并(平局时子类型 handler 在前),数字优先级不再只在单层内生效。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 派发热路径重构:事件对象的监听者快照从“逐个带锁二分插入内置 ListenerList”改为一次数组引用赋值;`EventId` 改为懒生成;事件元数据缓存改用 `ConcurrentDictionary`。每个事件实例的构造分配显著减少。
|
||||||
|
- 删除死代码 `EventCache<T>`(README 此前描述的“泛型静态缓存热路径”自实例化总线改造后已不存在)。
|
||||||
|
- 恢复模块内 `CodeGen/` 作为可选本地织入管线:命中 `ShrinkShared.CodeGen` 覆盖范围的程序集继续由共享管线优先处理,其余只引用 `ShrinkEventBus.Runtime` 的程序集改由本地 `EventBusILPostProcessor` 兜底,避免 `[EventBusSubscriber]` 自动注册失效。
|
||||||
|
- `GetEventSubscribers` / `GetAllSubscribersSnapshot` / `EventBase.GetSubscribers` 返回内部快照的拷贝,外部修改不再影响派发顺序。
|
||||||
|
- `SetPhase` / `IsCanceled` / `Result` 的校验异常现在携带具体错误消息。
|
||||||
|
- ILPP 的诊断列表与程序集解析器改为调用内局部状态,消除共享实例并发隐患。
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-05-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `MonoBehaviour` 自动注册/反注册的 IL 织入已并入共享 `ShrinkShared.CodeGen` 管线;当前工作区同时保留 `ShrinkEventBus` 本地 `CodeGen/` 作为可选兜底,以兼容纯 `ShrinkEventBus.Runtime` 引用程序集。
|
||||||
|
- 保留现有 `[EventBusSubscriber]` 用法与自动生命周期行为,不要求业务层改写订阅代码。
|
||||||
|
|
||||||
|
## [1.1.6] - 2026-05-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 运行时总线升级为“默认静态门面 + 可实例化 `IShrinkEventBus`/Builder”双层结构,支持异常策略、事件类型校验、按 phase 分发。
|
||||||
|
- 手动注册能力从纯 delegate 扩展为 `object / Type / MethodInfo` 三类入口,并在注册阶段严格校验签名、静态/实例模式和事件参数类型。
|
||||||
|
- `ListenerList` 改为按 phase 分桶 + 快照缓存模型,并支持父事件监听子事件。
|
||||||
|
- 事件查看器不再反射私有字典,改走公开快照接口。
|
||||||
|
- `EventBusBenchmark` 扩展为覆盖实例 bus、扫描注册、继承监听、phase 分发、对象池与取消事件跳过等关键场景。
|
||||||
|
|
||||||
|
## [1.1.5] - 2026-04-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 修正 `PrepareForDispatch()`,现在只重置派发期状态,不再误清业务载荷。
|
||||||
|
- 修正同步 `TriggerEvent(...)` 遇到异步 handler 时的行为,改为向异步处理器分发脱离原对象的事件快照,避免直接复用原事件对象。
|
||||||
|
- `OnEventTriggered` 与编辑器追踪钩子改为在派发完成后触发,同时覆盖无监听者事件。
|
||||||
|
- `ShrinkNetworkEventBusBridge` 的异步转发改为先克隆事件,降低桥接层状态污染风险。
|
||||||
|
- 事件查看器的实时日志改为快照模型,支持关键词过滤、按“有监听者 / 无监听者”筛选、同类事件折叠聚合显示。
|
||||||
|
- 编辑器菜单迁移到 `ShrinkSDK/事件总线/事件查看器`。
|
||||||
|
- README 同步更新新的调试入口与同步/异步派发语义说明。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9b2928da4a2426743bd762f5ebb9c232
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c2bfff1573c10da47ae73a47b8841279
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c297548ebc9165f448111a35488d368e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Mono.Cecil;
|
||||||
|
using Mono.Cecil.Cil;
|
||||||
|
using Mono.Cecil.Rocks;
|
||||||
|
using Mono.Cecil.Pdb;
|
||||||
|
using Unity.CompilationPipeline.Common.Diagnostics;
|
||||||
|
using Unity.CompilationPipeline.Common.ILPostProcessing;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.CodeGen
|
||||||
|
{
|
||||||
|
public sealed class EventBusILPostProcessor : ILPostProcessor
|
||||||
|
{
|
||||||
|
private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime";
|
||||||
|
public override ILPostProcessor GetInstance() => this;
|
||||||
|
|
||||||
|
public override bool WillProcess(ICompiledAssembly compiledAssembly)
|
||||||
|
{
|
||||||
|
if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
|
||||||
|
{
|
||||||
|
var diagnostics = new List<DiagnosticMessage>();
|
||||||
|
if (!WillProcess(compiledAssembly))
|
||||||
|
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics);
|
||||||
|
|
||||||
|
var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
|
||||||
|
var module = assemblyDefinition.MainModule;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var generatedSubscriberType = FindType(module, "ShrinkEventBus.ShrinkEventSubscriberAttribute", RuntimeAssemblyName);
|
||||||
|
var generatedSubscribeType = FindType(module, "ShrinkEventBus.ShrinkSubscribeAttribute", RuntimeAssemblyName);
|
||||||
|
if (generatedSubscriberType == null || generatedSubscribeType == null)
|
||||||
|
return GetResult(assemblyDefinition, diagnostics);
|
||||||
|
|
||||||
|
foreach (var type in GetAllTypes(module.Types)
|
||||||
|
.Where(type => !type.IsInterface && !type.IsAbstract)
|
||||||
|
.Where(type => HasAttribute(type, generatedSubscriberType))
|
||||||
|
.Where(type => type.Methods.Any(method =>
|
||||||
|
!method.IsStatic && HasAttribute(method, generatedSubscribeType))))
|
||||||
|
{
|
||||||
|
InjectGeneratedBinding(type, module, generatedSubscribeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var staticSubscriberTypes = GetAllTypes(module.Types)
|
||||||
|
.Where(type => HasAttribute(type, generatedSubscriberType))
|
||||||
|
.Where(type => type.Methods.Any(method =>
|
||||||
|
method.IsStatic && HasAttribute(method, generatedSubscribeType)))
|
||||||
|
.ToArray();
|
||||||
|
if (staticSubscriberTypes.Length > 0)
|
||||||
|
InjectStaticBootstrap(module, staticSubscriberTypes, generatedSubscribeType);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
diagnostics.Add(new DiagnosticMessage
|
||||||
|
{
|
||||||
|
DiagnosticType = DiagnosticType.Error,
|
||||||
|
MessageData = $"[ShrinkEventBus.CodeGen] {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetResult(assemblyDefinition, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReferencesAssembly(ICompiledAssembly compiledAssembly, string assemblyName)
|
||||||
|
{
|
||||||
|
return compiledAssembly.References.Any(reference =>
|
||||||
|
string.Equals(Path.GetFileNameWithoutExtension(reference), assemblyName, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference expectedAttributeType)
|
||||||
|
{
|
||||||
|
return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TypeDefinition> GetAllTypes(IEnumerable<TypeDefinition> roots)
|
||||||
|
{
|
||||||
|
foreach (var type in roots)
|
||||||
|
{
|
||||||
|
yield return type;
|
||||||
|
foreach (var nested in GetAllTypes(type.NestedTypes))
|
||||||
|
yield return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InjectGeneratedBinding(TypeDefinition type, ModuleDefinition module,
|
||||||
|
TypeReference subscribeAttributeType)
|
||||||
|
{
|
||||||
|
var generatedInterface = FindType(module,
|
||||||
|
"ShrinkEventBus.IShrinkGeneratedSubscriber", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("IShrinkGeneratedSubscriber was not found.");
|
||||||
|
if (type.Interfaces.Any(item => item.InterfaceType.FullName == generatedInterface.FullName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var resolverType = FindType(module, "ShrinkEventBus.IShrinkBusResolver", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("IShrinkBusResolver was not found.");
|
||||||
|
var busKeyType = FindType(module, "ShrinkEventBus.ShrinkBusKey", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkBusKey was not found.");
|
||||||
|
var bindingType = FindType(module, "ShrinkEventBus.ShrinkEventBinding", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkEventBinding was not found.");
|
||||||
|
var bindingHelperType = FindType(module, "ShrinkEventBus.ShrinkGeneratedBinding", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkGeneratedBinding was not found.");
|
||||||
|
var priorityType = FindType(module, "ShrinkEventBus.ShrinkEventPriority", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkEventPriority was not found.");
|
||||||
|
var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found.");
|
||||||
|
|
||||||
|
var nullableBusKey = new GenericInstanceType(module.ImportReference(typeof(Nullable<>)));
|
||||||
|
nullableBusKey.GenericArguments.Add(busKeyType);
|
||||||
|
var disposableType = module.ImportReference(typeof(IDisposable));
|
||||||
|
var bindingDefinition = bindingType.Resolve()
|
||||||
|
?? throw new InvalidOperationException("ShrinkEventBinding could not be resolved.");
|
||||||
|
var bindingCtor = module.ImportReference(bindingDefinition.Methods.Single(method =>
|
||||||
|
method.IsConstructor && method.Parameters.Count == 0));
|
||||||
|
var bindingAdd = module.ImportReference(bindingDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "Add" && method.Parameters.Count == 1));
|
||||||
|
|
||||||
|
var helperDefinition = bindingHelperType.Resolve()
|
||||||
|
?? throw new InvalidOperationException("ShrinkGeneratedBinding could not be resolved.");
|
||||||
|
var syncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "Subscribe" && method.HasGenericParameters));
|
||||||
|
var asyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "SubscribeAsync" && method.HasGenericParameters));
|
||||||
|
var legacyAsyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "SubscribeAsyncLegacy" && method.HasGenericParameters));
|
||||||
|
|
||||||
|
var interfaceDefinition = generatedInterface.Resolve()
|
||||||
|
?? throw new InvalidOperationException("IShrinkGeneratedSubscriber could not be resolved.");
|
||||||
|
var interfaceMethod = module.ImportReference(interfaceDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "AttachGenerated"));
|
||||||
|
|
||||||
|
var generatedMethod = new MethodDefinition(
|
||||||
|
"ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated",
|
||||||
|
MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig |
|
||||||
|
MethodAttributes.NewSlot | MethodAttributes.Virtual,
|
||||||
|
disposableType);
|
||||||
|
generatedMethod.Parameters.Add(new ParameterDefinition("resolver", ParameterAttributes.None, resolverType));
|
||||||
|
generatedMethod.Parameters.Add(new ParameterDefinition("defaultBus", ParameterAttributes.Optional,
|
||||||
|
nullableBusKey));
|
||||||
|
generatedMethod.Overrides.Add(interfaceMethod);
|
||||||
|
generatedMethod.Body.InitLocals = true;
|
||||||
|
var bindingLocal = new VariableDefinition(bindingType);
|
||||||
|
generatedMethod.Body.Variables.Add(bindingLocal);
|
||||||
|
var il = generatedMethod.Body.GetILProcessor();
|
||||||
|
il.Emit(OpCodes.Newobj, bindingCtor);
|
||||||
|
il.Emit(OpCodes.Stloc, bindingLocal);
|
||||||
|
|
||||||
|
var classDefaultBus = ReadStringProperty(
|
||||||
|
type.CustomAttributes.First(attribute =>
|
||||||
|
attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"),
|
||||||
|
"DefaultBus");
|
||||||
|
|
||||||
|
foreach (var handler in type.Methods.Where(method =>
|
||||||
|
!method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray())
|
||||||
|
{
|
||||||
|
EmitGeneratedSubscription(il, module, type, handler,
|
||||||
|
handler.CustomAttributes.First(attribute =>
|
||||||
|
attribute.AttributeType.FullName == subscribeAttributeType.FullName),
|
||||||
|
classDefaultBus, bindingLocal, bindingAdd, syncSubscribe, asyncSubscribe,
|
||||||
|
legacyAsyncSubscribe, asyncHandlerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
il.Emit(OpCodes.Ldloc, bindingLocal);
|
||||||
|
il.Emit(OpCodes.Ret);
|
||||||
|
type.Interfaces.Add(new InterfaceImplementation(generatedInterface));
|
||||||
|
type.Methods.Add(generatedMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitGeneratedSubscription(ILProcessor il, ModuleDefinition module,
|
||||||
|
TypeDefinition ownerType, MethodDefinition handler, CustomAttribute attribute,
|
||||||
|
string classDefaultBus, VariableDefinition bindingLocal, MethodReference bindingAdd,
|
||||||
|
MethodReference syncSubscribe, MethodReference asyncSubscribe,
|
||||||
|
MethodReference legacyAsyncSubscribe, TypeReference asyncHandlerType)
|
||||||
|
{
|
||||||
|
if (handler.Parameters.Count is < 1 or > 2)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken.");
|
||||||
|
|
||||||
|
var eventType = module.ImportReference(handler.Parameters[0].ParameterType);
|
||||||
|
var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("IShrinkEvent was not found.");
|
||||||
|
if (!ImplementsInterface(eventType, eventInterface.FullName))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent.");
|
||||||
|
|
||||||
|
var bus = ReadStringProperty(attribute, "Bus");
|
||||||
|
if (string.IsNullOrWhiteSpace(bus))
|
||||||
|
bus = classDefaultBus;
|
||||||
|
var priority = ReadIntProperty(attribute, "Priority", 2);
|
||||||
|
var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0);
|
||||||
|
var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false);
|
||||||
|
|
||||||
|
MethodReference openSubscribe;
|
||||||
|
TypeReference delegateType;
|
||||||
|
if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1)
|
||||||
|
{
|
||||||
|
openSubscribe = syncSubscribe;
|
||||||
|
delegateType = MakeGenericType(module, typeof(Action<>), eventType);
|
||||||
|
}
|
||||||
|
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
|
||||||
|
handler.Parameters.Count == 1)
|
||||||
|
{
|
||||||
|
openSubscribe = legacyAsyncSubscribe;
|
||||||
|
var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask")
|
||||||
|
?? module.ImportReference(handler.ReturnType);
|
||||||
|
delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType);
|
||||||
|
}
|
||||||
|
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
|
||||||
|
handler.Parameters.Count == 2 &&
|
||||||
|
handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName)
|
||||||
|
{
|
||||||
|
openSubscribe = asyncSubscribe;
|
||||||
|
var closedAsyncHandler = new GenericInstanceType(asyncHandlerType);
|
||||||
|
closedAsyncHandler.GenericArguments.Add(eventType);
|
||||||
|
delegateType = closedAsyncHandler;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Unsupported [ShrinkSubscribe] signature: {handler.FullName}. Use void(T), UniTask(T), or UniTask(T, CancellationToken).");
|
||||||
|
}
|
||||||
|
|
||||||
|
var closedSubscribe = new GenericInstanceMethod(openSubscribe);
|
||||||
|
closedSubscribe.GenericArguments.Add(eventType);
|
||||||
|
var delegateCtor = MakeDelegateConstructor(module, delegateType);
|
||||||
|
|
||||||
|
il.Emit(OpCodes.Ldloc, bindingLocal);
|
||||||
|
il.Emit(OpCodes.Ldarg_1);
|
||||||
|
il.Emit(OpCodes.Ldarg_2);
|
||||||
|
il.Emit(OpCodes.Ldstr, bus ?? string.Empty);
|
||||||
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
|
il.Emit(OpCodes.Ldftn, module.ImportReference(handler));
|
||||||
|
il.Emit(OpCodes.Newobj, delegateCtor);
|
||||||
|
il.Emit(OpCodes.Ldc_I4, priority);
|
||||||
|
il.Emit(OpCodes.Ldc_I4, numericPriority);
|
||||||
|
il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
|
||||||
|
il.Emit(OpCodes.Call, closedSubscribe);
|
||||||
|
il.Emit(OpCodes.Callvirt, bindingAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InjectStaticBootstrap(ModuleDefinition module,
|
||||||
|
IReadOnlyList<TypeDefinition> subscriberTypes, TypeReference subscribeAttributeType)
|
||||||
|
{
|
||||||
|
var registryType = FindType(module, "ShrinkEventBus.ShrinkStaticBindingRegistry", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkStaticBindingRegistry was not found.");
|
||||||
|
var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found.");
|
||||||
|
var registryDefinition = registryType.Resolve()
|
||||||
|
?? throw new InvalidOperationException("ShrinkStaticBindingRegistry could not be resolved.");
|
||||||
|
var syncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "Register" && method.HasGenericParameters));
|
||||||
|
var asyncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "RegisterAsync" && method.HasGenericParameters));
|
||||||
|
var legacyAsyncRegister = module.ImportReference(registryDefinition.Methods.Single(method =>
|
||||||
|
method.Name == "RegisterAsyncLegacy" && method.HasGenericParameters));
|
||||||
|
|
||||||
|
var bootstrap = new TypeDefinition("ShrinkEventBus.Generated",
|
||||||
|
"ShrinkGeneratedStaticBindings",
|
||||||
|
TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic,
|
||||||
|
module.TypeSystem.Object);
|
||||||
|
module.Types.Add(bootstrap);
|
||||||
|
|
||||||
|
var register = new MethodDefinition("Register",
|
||||||
|
MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig,
|
||||||
|
module.TypeSystem.Void);
|
||||||
|
bootstrap.Methods.Add(register);
|
||||||
|
var il = register.Body.GetILProcessor();
|
||||||
|
|
||||||
|
foreach (var type in subscriberTypes)
|
||||||
|
{
|
||||||
|
var classDefaultBus = ReadStringProperty(
|
||||||
|
type.CustomAttributes.First(attribute =>
|
||||||
|
attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"),
|
||||||
|
"DefaultBus");
|
||||||
|
foreach (var handler in type.Methods.Where(method =>
|
||||||
|
method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray())
|
||||||
|
{
|
||||||
|
EmitStaticGeneratedSubscription(il, module, handler,
|
||||||
|
handler.CustomAttributes.First(attribute =>
|
||||||
|
attribute.AttributeType.FullName == subscribeAttributeType.FullName),
|
||||||
|
classDefaultBus, syncRegister, asyncRegister,
|
||||||
|
legacyAsyncRegister, asyncHandlerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
il.Emit(OpCodes.Ret);
|
||||||
|
InjectModuleInitializer(module, register);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitStaticGeneratedSubscription(ILProcessor il, ModuleDefinition module,
|
||||||
|
MethodDefinition handler, CustomAttribute attribute, string classDefaultBus,
|
||||||
|
MethodReference syncRegister, MethodReference asyncRegister,
|
||||||
|
MethodReference legacyAsyncRegister, TypeReference asyncHandlerType)
|
||||||
|
{
|
||||||
|
if (handler.Parameters.Count is < 1 or > 2)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken.");
|
||||||
|
|
||||||
|
var eventType = module.ImportReference(handler.Parameters[0].ParameterType);
|
||||||
|
var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName)
|
||||||
|
?? throw new InvalidOperationException("IShrinkEvent was not found.");
|
||||||
|
if (!ImplementsInterface(eventType, eventInterface.FullName))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent.");
|
||||||
|
|
||||||
|
var bus = ReadStringProperty(attribute, "Bus");
|
||||||
|
if (string.IsNullOrWhiteSpace(bus))
|
||||||
|
bus = classDefaultBus;
|
||||||
|
var priority = ReadIntProperty(attribute, "Priority", 2);
|
||||||
|
var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0);
|
||||||
|
var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false);
|
||||||
|
|
||||||
|
MethodReference openRegister;
|
||||||
|
TypeReference delegateType;
|
||||||
|
if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1)
|
||||||
|
{
|
||||||
|
openRegister = syncRegister;
|
||||||
|
delegateType = MakeGenericType(module, typeof(Action<>), eventType);
|
||||||
|
}
|
||||||
|
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
|
||||||
|
handler.Parameters.Count == 1)
|
||||||
|
{
|
||||||
|
openRegister = legacyAsyncRegister;
|
||||||
|
var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask")
|
||||||
|
?? module.ImportReference(handler.ReturnType);
|
||||||
|
delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType);
|
||||||
|
}
|
||||||
|
else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" &&
|
||||||
|
handler.Parameters.Count == 2 &&
|
||||||
|
handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName)
|
||||||
|
{
|
||||||
|
openRegister = asyncRegister;
|
||||||
|
var closedAsyncHandler = new GenericInstanceType(asyncHandlerType);
|
||||||
|
closedAsyncHandler.GenericArguments.Add(eventType);
|
||||||
|
delegateType = closedAsyncHandler;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Unsupported static [ShrinkSubscribe] signature: {handler.FullName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var closedRegister = new GenericInstanceMethod(openRegister);
|
||||||
|
closedRegister.GenericArguments.Add(eventType);
|
||||||
|
var delegateCtor = MakeDelegateConstructor(module, delegateType);
|
||||||
|
var handlerBridge = CreateStaticHandlerBridge(module, handler);
|
||||||
|
|
||||||
|
il.Emit(OpCodes.Ldstr, bus ?? string.Empty);
|
||||||
|
il.Emit(OpCodes.Ldnull);
|
||||||
|
il.Emit(OpCodes.Ldftn, module.ImportReference(handlerBridge));
|
||||||
|
il.Emit(OpCodes.Newobj, delegateCtor);
|
||||||
|
il.Emit(OpCodes.Ldc_I4, priority);
|
||||||
|
il.Emit(OpCodes.Ldc_I4, numericPriority);
|
||||||
|
il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
|
||||||
|
il.Emit(OpCodes.Call, closedRegister);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodDefinition CreateStaticHandlerBridge(ModuleDefinition module,
|
||||||
|
MethodDefinition handler)
|
||||||
|
{
|
||||||
|
var bridge = new MethodDefinition(
|
||||||
|
$"ShrinkEventBus.GeneratedStaticHandler_{handler.MetadataToken.ToInt32():X8}",
|
||||||
|
MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig,
|
||||||
|
module.ImportReference(handler.ReturnType));
|
||||||
|
for (var i = 0; i < handler.Parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var parameter = handler.Parameters[i];
|
||||||
|
bridge.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes,
|
||||||
|
module.ImportReference(parameter.ParameterType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var il = bridge.Body.GetILProcessor();
|
||||||
|
for (var i = 0; i < bridge.Parameters.Count; i++)
|
||||||
|
il.Emit(OpCodes.Ldarg, bridge.Parameters[i]);
|
||||||
|
il.Emit(OpCodes.Call, module.ImportReference(handler));
|
||||||
|
il.Emit(OpCodes.Ret);
|
||||||
|
handler.DeclaringType.Methods.Add(bridge);
|
||||||
|
return bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InjectModuleInitializer(ModuleDefinition module, MethodReference register)
|
||||||
|
{
|
||||||
|
var moduleType = module.Types.First(type => type.Name == "<Module>");
|
||||||
|
var initializer = moduleType.Methods.FirstOrDefault(method => method.Name == ".cctor");
|
||||||
|
if (initializer == null)
|
||||||
|
{
|
||||||
|
initializer = new MethodDefinition(".cctor",
|
||||||
|
MethodAttributes.Private | MethodAttributes.Static |
|
||||||
|
MethodAttributes.HideBySig | MethodAttributes.SpecialName |
|
||||||
|
MethodAttributes.RTSpecialName,
|
||||||
|
module.TypeSystem.Void);
|
||||||
|
initializer.Body.GetILProcessor().Emit(OpCodes.Ret);
|
||||||
|
moduleType.Methods.Add(initializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var processor = initializer.Body.GetILProcessor();
|
||||||
|
processor.InsertBefore(initializer.Body.Instructions[0],
|
||||||
|
processor.Create(OpCodes.Call, register));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenericInstanceType MakeGenericType(ModuleDefinition module, Type openType,
|
||||||
|
params TypeReference[] arguments)
|
||||||
|
{
|
||||||
|
var result = new GenericInstanceType(module.ImportReference(openType));
|
||||||
|
foreach (var argument in arguments)
|
||||||
|
result.GenericArguments.Add(argument);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodReference MakeDelegateConstructor(ModuleDefinition module, TypeReference delegateType)
|
||||||
|
{
|
||||||
|
var ctor = new MethodReference(".ctor", module.TypeSystem.Void, delegateType)
|
||||||
|
{
|
||||||
|
HasThis = true,
|
||||||
|
CallingConvention = MethodCallingConvention.Default
|
||||||
|
};
|
||||||
|
ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object));
|
||||||
|
ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.IntPtr));
|
||||||
|
return ctor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ImplementsInterface(TypeReference type, string interfaceFullName)
|
||||||
|
{
|
||||||
|
TypeDefinition? current;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
current = type.Resolve();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (current.Interfaces.Any(item => item.InterfaceType.FullName == interfaceFullName))
|
||||||
|
return true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
current = current.BaseType?.Resolve();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadStringProperty(CustomAttribute attribute, string name)
|
||||||
|
{
|
||||||
|
foreach (var property in attribute.Properties)
|
||||||
|
{
|
||||||
|
if (property.Name == name)
|
||||||
|
return property.Argument.Value as string ?? string.Empty;
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadIntProperty(CustomAttribute attribute, string name, int defaultValue)
|
||||||
|
{
|
||||||
|
foreach (var property in attribute.Properties)
|
||||||
|
{
|
||||||
|
if (property.Name == name)
|
||||||
|
return Convert.ToInt32(property.Argument.Value);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReadBoolProperty(CustomAttribute attribute, string name, bool defaultValue)
|
||||||
|
{
|
||||||
|
foreach (var property in attribute.Properties)
|
||||||
|
{
|
||||||
|
if (property.Name == name)
|
||||||
|
return Convert.ToBoolean(property.Argument.Value);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName)
|
||||||
|
{
|
||||||
|
var resolved = Type.GetType($"{fullName}, {assemblyName}", false);
|
||||||
|
return resolved == null ? null : module.ImportReference(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
|
||||||
|
{
|
||||||
|
var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly);
|
||||||
|
var readerParameters = new ReaderParameters
|
||||||
|
{
|
||||||
|
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
|
||||||
|
SymbolReaderProvider = new PdbReaderProvider(),
|
||||||
|
AssemblyResolver = assemblyResolver,
|
||||||
|
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
|
||||||
|
ReadingMode = ReadingMode.Immediate
|
||||||
|
};
|
||||||
|
|
||||||
|
var assemblyDefinition = AssemblyDefinition.ReadAssembly(
|
||||||
|
new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()),
|
||||||
|
readerParameters);
|
||||||
|
assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
|
||||||
|
return assemblyDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List<DiagnosticMessage> diagnostics)
|
||||||
|
{
|
||||||
|
var pe = new MemoryStream();
|
||||||
|
var pdb = new MemoryStream();
|
||||||
|
assemblyDefinition.Write(pe, new WriterParameters
|
||||||
|
{
|
||||||
|
SymbolWriterProvider = new PdbWriterProvider(),
|
||||||
|
SymbolStream = pdb,
|
||||||
|
WriteSymbols = true
|
||||||
|
});
|
||||||
|
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f611b284dae01a46bc990b87b8bca90
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using Mono.Cecil;
|
||||||
|
using Unity.CompilationPipeline.Common.ILPostProcessing;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.CodeGen
|
||||||
|
{
|
||||||
|
public sealed class PostProcessorAssemblyResolver : IAssemblyResolver
|
||||||
|
{
|
||||||
|
private readonly string[] _references;
|
||||||
|
private readonly Dictionary<string, AssemblyDefinition> _cache = new();
|
||||||
|
private AssemblyDefinition? _self;
|
||||||
|
|
||||||
|
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
|
||||||
|
{
|
||||||
|
_references = compiledAssembly.References;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition)
|
||||||
|
{
|
||||||
|
_self = assemblyDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssemblyDefinition? Resolve(AssemblyNameReference name)
|
||||||
|
{
|
||||||
|
return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
||||||
|
{
|
||||||
|
lock (_cache)
|
||||||
|
{
|
||||||
|
if (name.Name == _self?.Name.Name)
|
||||||
|
return _self;
|
||||||
|
|
||||||
|
var path = FindPath(name);
|
||||||
|
if (path == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var key = $"{path}{File.GetLastWriteTime(path)}";
|
||||||
|
if (_cache.TryGetValue(key, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
parameters.AssemblyResolver = this;
|
||||||
|
var ms = ReadFileWithRetry(path);
|
||||||
|
|
||||||
|
var pdbPath = Path.ChangeExtension(path, ".pdb");
|
||||||
|
if (File.Exists(pdbPath))
|
||||||
|
parameters.SymbolStream = ReadFileWithRetry(pdbPath);
|
||||||
|
|
||||||
|
var assembly = AssemblyDefinition.ReadAssembly(ms, parameters);
|
||||||
|
_cache[key] = assembly;
|
||||||
|
return assembly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? FindPath(AssemblyNameReference name)
|
||||||
|
{
|
||||||
|
foreach (var reference in _references)
|
||||||
|
{
|
||||||
|
if (Path.GetFileNameWithoutExtension(reference) == name.Name)
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirs = new HashSet<string>();
|
||||||
|
foreach (var reference in _references)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(reference);
|
||||||
|
if (dir != null)
|
||||||
|
dirs.Add(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dir in dirs)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(dir, $"{name.Name}.dll");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemoryStream ReadFileWithRetry(string path, int retries = 5)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < retries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new MemoryStream(File.ReadAllBytes(path));
|
||||||
|
}
|
||||||
|
catch (IOException) when (i < retries - 1)
|
||||||
|
{
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException($"[ShrinkEventBus.CodeGen] 无法读取文件: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aeea50f82bab4ee2833c671d801c95e3
|
||||||
|
timeCreated: 1772908894
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Mono.Cecil;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.CodeGen
|
||||||
|
{
|
||||||
|
public sealed class PostProcessorReflectionImporter : DefaultReflectionImporter
|
||||||
|
{
|
||||||
|
private const string CoreLibName = "System.Private.CoreLib";
|
||||||
|
private readonly AssemblyNameReference? _corlib;
|
||||||
|
|
||||||
|
public PostProcessorReflectionImporter(ModuleDefinition module)
|
||||||
|
: base(module)
|
||||||
|
{
|
||||||
|
_corlib = module.AssemblyReferences.FirstOrDefault(
|
||||||
|
assembly => assembly.Name is "mscorlib" or "netstandard");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override AssemblyNameReference ImportReference(AssemblyName reference)
|
||||||
|
{
|
||||||
|
if (_corlib != null && reference.Name == CoreLibName)
|
||||||
|
return _corlib;
|
||||||
|
|
||||||
|
return base.ImportReference(reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider
|
||||||
|
{
|
||||||
|
public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
|
||||||
|
{
|
||||||
|
return new PostProcessorReflectionImporter(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0a6c2401798b42f0bbcde72796d2fa9c
|
||||||
|
timeCreated: 1772908906
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "Unity.ShrinkEventBus.CodeGen",
|
||||||
|
"rootNamespace": "ShrinkEventBus.CodeGen",
|
||||||
|
"references": [
|
||||||
|
"ShrinkEventBus.Runtime"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": true,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"Mono.Cecil.dll",
|
||||||
|
"Mono.Cecil.Mdb.dll",
|
||||||
|
"Mono.Cecil.Pdb.dll",
|
||||||
|
"Mono.Cecil.Rocks.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": false,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 91161d4c2ae3ae74c9184ba33d9aa52e
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Ll]ibrary/
|
||||||
|
[Tt]emp/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]ogs/
|
||||||
|
[Uu]ser[Ss]ettings/
|
||||||
|
TestResults/
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"scopedRegistries": [
|
||||||
|
{
|
||||||
|
"name": "ShrinkSDK",
|
||||||
|
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
|
||||||
|
"scopes": [
|
||||||
|
"com.cneicy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.test-framework": "1.1.33",
|
||||||
|
"com.cneicy.shrink-eventbus": "file:../../.."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
m_EditorVersion: 2022.3.62f3
|
||||||
|
m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8bf98940dc144bd2bffd55ce21295bbd
|
||||||
|
timeCreated: 1772878399
|
||||||
@@ -0,0 +1,906 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.UIElements;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.Editor
|
||||||
|
{
|
||||||
|
public sealed class EventBusViewerWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private sealed class BusRow
|
||||||
|
{
|
||||||
|
public string Key = string.Empty;
|
||||||
|
public string FilterKey = string.Empty;
|
||||||
|
public string Scheduler = string.Empty;
|
||||||
|
public string Mode = string.Empty;
|
||||||
|
public string Queue = string.Empty;
|
||||||
|
public string Overflow = string.Empty;
|
||||||
|
public int Subscribers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EventRow
|
||||||
|
{
|
||||||
|
public long Sequence;
|
||||||
|
public DateTime TimestampUtc;
|
||||||
|
public string Bus = string.Empty;
|
||||||
|
public string EventType = string.Empty;
|
||||||
|
public string ShortEventType = string.Empty;
|
||||||
|
public string Scheduler = string.Empty;
|
||||||
|
public string Mode = string.Empty;
|
||||||
|
public int ThreadId;
|
||||||
|
public double DurationMicroseconds;
|
||||||
|
public bool IsAsync;
|
||||||
|
public bool Accepted;
|
||||||
|
public bool Handled;
|
||||||
|
public bool Canceled;
|
||||||
|
public ShrinkPostFailure Failure;
|
||||||
|
|
||||||
|
public string Status => Failure != ShrinkPostFailure.None
|
||||||
|
? FormatFailure(Failure)
|
||||||
|
: Canceled
|
||||||
|
? "已取消"
|
||||||
|
: Handled
|
||||||
|
? "已处理"
|
||||||
|
: "无处理器";
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int MaxEvents = 5000;
|
||||||
|
private const int MaxPendingEvents = 20000;
|
||||||
|
private static readonly Color SuccessColor = new(0.32f, 0.72f, 0.48f);
|
||||||
|
private static readonly Color WarningColor = new(0.94f, 0.68f, 0.24f);
|
||||||
|
private static readonly Color ErrorColor = new(0.95f, 0.36f, 0.34f);
|
||||||
|
private static readonly Color MutedColor = new(0.58f, 0.61f, 0.66f);
|
||||||
|
|
||||||
|
private readonly object _eventGate = new();
|
||||||
|
private readonly Queue<EventRow> _pendingEvents = new();
|
||||||
|
private readonly List<EventRow> _events = new();
|
||||||
|
private readonly List<EventRow> _filteredEvents = new();
|
||||||
|
private readonly List<BusRow> _buses = new();
|
||||||
|
|
||||||
|
private ListView? _busList;
|
||||||
|
private ListView? _eventList;
|
||||||
|
private Label? _eventEmptyState;
|
||||||
|
private ToolbarSearchField? _search;
|
||||||
|
private ToolbarToggle? _captureToggle;
|
||||||
|
private ToolbarToggle? _problemOnlyToggle;
|
||||||
|
private ToolbarToggle? _autoScrollToggle;
|
||||||
|
private Label? _captureState;
|
||||||
|
private Label? _status;
|
||||||
|
private Label? _busDescription;
|
||||||
|
private Label? _busCountMetric;
|
||||||
|
private Label? _handlerCountMetric;
|
||||||
|
private Label? _capturedMetric;
|
||||||
|
private Label? _rateMetric;
|
||||||
|
private Label? _latencyMetric;
|
||||||
|
private Label? _problemMetric;
|
||||||
|
private Label? _detailTitle;
|
||||||
|
private Label? _detailTime;
|
||||||
|
private Label? _detailBus;
|
||||||
|
private Label? _detailExecution;
|
||||||
|
private Label? _detailThread;
|
||||||
|
private Label? _detailDuration;
|
||||||
|
private Label? _detailResult;
|
||||||
|
|
||||||
|
private string _selectedBus = string.Empty;
|
||||||
|
private long _selectedEventSequence;
|
||||||
|
private long _nextSequence;
|
||||||
|
private long _droppedCaptureCount;
|
||||||
|
private double _nextRefresh;
|
||||||
|
private bool _captureRequested = true;
|
||||||
|
private bool _diagnosticsSubscribed;
|
||||||
|
|
||||||
|
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
|
||||||
|
private static void Open()
|
||||||
|
{
|
||||||
|
var window = GetWindow<EventBusViewerWindow>();
|
||||||
|
window.titleContent = new GUIContent("事件总线 2.0");
|
||||||
|
window.minSize = new Vector2(980f, 560f);
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
SetDiagnosticsSubscription(_captureRequested);
|
||||||
|
EditorApplication.update += Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
SetDiagnosticsSubscription(false);
|
||||||
|
EditorApplication.update -= Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateGUI()
|
||||||
|
{
|
||||||
|
rootVisualElement.Clear();
|
||||||
|
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
||||||
|
rootVisualElement.style.backgroundColor = EditorGUIUtility.isProSkin
|
||||||
|
? new Color(0.105f, 0.112f, 0.125f)
|
||||||
|
: new Color(0.88f, 0.89f, 0.91f);
|
||||||
|
|
||||||
|
rootVisualElement.Add(BuildToolbar());
|
||||||
|
rootVisualElement.Add(BuildMetricsBand());
|
||||||
|
|
||||||
|
var mainSplit = new TwoPaneSplitView(0, 330f, TwoPaneSplitViewOrientation.Horizontal);
|
||||||
|
mainSplit.style.flexGrow = 1f;
|
||||||
|
mainSplit.Add(BuildBusPane());
|
||||||
|
mainSplit.Add(BuildEventWorkspace());
|
||||||
|
rootVisualElement.Add(mainSplit);
|
||||||
|
|
||||||
|
_status = new Label();
|
||||||
|
_status.style.height = 24f;
|
||||||
|
_status.style.paddingLeft = 10f;
|
||||||
|
_status.style.paddingTop = 4f;
|
||||||
|
_status.style.borderTopWidth = 1f;
|
||||||
|
_status.style.borderTopColor = BorderColor();
|
||||||
|
_status.style.color = MutedColor;
|
||||||
|
rootVisualElement.Add(_status);
|
||||||
|
|
||||||
|
RefreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildToolbar()
|
||||||
|
{
|
||||||
|
var toolbar = new Toolbar();
|
||||||
|
toolbar.style.height = 34f;
|
||||||
|
|
||||||
|
var title = new Label("ShrinkEventBus");
|
||||||
|
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
title.style.fontSize = 13f;
|
||||||
|
title.style.marginLeft = 4f;
|
||||||
|
title.style.marginRight = 8f;
|
||||||
|
toolbar.Add(title);
|
||||||
|
|
||||||
|
_captureState = new Label();
|
||||||
|
_captureState.style.minWidth = 48f;
|
||||||
|
_captureState.style.height = 18f;
|
||||||
|
_captureState.style.marginRight = 10f;
|
||||||
|
_captureState.style.paddingLeft = 6f;
|
||||||
|
_captureState.style.paddingRight = 6f;
|
||||||
|
_captureState.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
_captureState.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
_captureState.style.fontSize = 9f;
|
||||||
|
toolbar.Add(_captureState);
|
||||||
|
|
||||||
|
_search = new ToolbarSearchField { tooltip = "按总线标识或事件类型筛选" };
|
||||||
|
_search.style.width = 260f;
|
||||||
|
_search.RegisterValueChangedCallback(_ => ApplyFilter());
|
||||||
|
toolbar.Add(_search);
|
||||||
|
|
||||||
|
_problemOnlyToggle = new ToolbarToggle { text = "仅问题" };
|
||||||
|
_problemOnlyToggle.tooltip = "只显示已取消、已拒绝或失败的发布";
|
||||||
|
_problemOnlyToggle.RegisterValueChangedCallback(_ => ApplyFilter());
|
||||||
|
toolbar.Add(_problemOnlyToggle);
|
||||||
|
|
||||||
|
_autoScrollToggle = new ToolbarToggle { text = "跟随", value = true };
|
||||||
|
_autoScrollToggle.tooltip = "自动保持最新事件可见";
|
||||||
|
toolbar.Add(_autoScrollToggle);
|
||||||
|
|
||||||
|
_captureToggle = new ToolbarToggle { text = "捕获", value = _captureRequested };
|
||||||
|
_captureToggle.tooltip = "启用详细分发采样";
|
||||||
|
_captureToggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
_captureRequested = evt.newValue;
|
||||||
|
SetDiagnosticsSubscription(evt.newValue);
|
||||||
|
UpdateCaptureState();
|
||||||
|
});
|
||||||
|
toolbar.Add(_captureToggle);
|
||||||
|
|
||||||
|
var spacer = new VisualElement();
|
||||||
|
spacer.style.flexGrow = 1f;
|
||||||
|
toolbar.Add(spacer);
|
||||||
|
|
||||||
|
toolbar.Add(new ToolbarButton(RefreshAll) { text = "刷新", tooltip = "刷新总线状态" });
|
||||||
|
toolbar.Add(new ToolbarButton(ExportCsv) { text = "导出", tooltip = "将当前可见事件导出为 CSV" });
|
||||||
|
toolbar.Add(new ToolbarButton(ClearEvents) { text = "清空", tooltip = "清空已捕获事件" });
|
||||||
|
|
||||||
|
UpdateCaptureState();
|
||||||
|
return toolbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildMetricsBand()
|
||||||
|
{
|
||||||
|
var band = new VisualElement();
|
||||||
|
band.style.height = 58f;
|
||||||
|
band.style.flexDirection = FlexDirection.Row;
|
||||||
|
band.style.borderBottomWidth = 1f;
|
||||||
|
band.style.borderBottomColor = BorderColor();
|
||||||
|
band.style.backgroundColor = PanelColor();
|
||||||
|
|
||||||
|
_busCountMetric = AddMetric(band, "总线");
|
||||||
|
_handlerCountMetric = AddMetric(band, "处理器");
|
||||||
|
_capturedMetric = AddMetric(band, "已捕获");
|
||||||
|
_rateMetric = AddMetric(band, "事件 / 秒");
|
||||||
|
_latencyMetric = AddMetric(band, "平均分发耗时");
|
||||||
|
_problemMetric = AddMetric(band, "问题");
|
||||||
|
return band;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label AddMetric(VisualElement parent, string caption)
|
||||||
|
{
|
||||||
|
var block = new VisualElement();
|
||||||
|
block.style.flexGrow = 1f;
|
||||||
|
block.style.flexBasis = 0f;
|
||||||
|
block.style.paddingLeft = 12f;
|
||||||
|
block.style.paddingTop = 6f;
|
||||||
|
block.style.borderRightWidth = 1f;
|
||||||
|
block.style.borderRightColor = BorderColor();
|
||||||
|
|
||||||
|
var captionLabel = new Label(caption);
|
||||||
|
captionLabel.style.fontSize = 9f;
|
||||||
|
captionLabel.style.color = MutedColor;
|
||||||
|
block.Add(captionLabel);
|
||||||
|
|
||||||
|
var value = new Label("0");
|
||||||
|
value.style.fontSize = 17f;
|
||||||
|
value.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
value.style.marginTop = 1f;
|
||||||
|
block.Add(value);
|
||||||
|
parent.Add(block);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildBusPane()
|
||||||
|
{
|
||||||
|
var pane = new VisualElement();
|
||||||
|
pane.style.flexGrow = 1f;
|
||||||
|
pane.style.backgroundColor = PanelColor();
|
||||||
|
pane.Add(BuildBusHeader());
|
||||||
|
|
||||||
|
_busList = new ListView(_buses, 30f, MakeBusRow, BindBusRow)
|
||||||
|
{
|
||||||
|
selectionType = SelectionType.Single,
|
||||||
|
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
|
||||||
|
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
|
||||||
|
};
|
||||||
|
_busList.style.flexGrow = 1f;
|
||||||
|
_busList.selectionChanged += selection =>
|
||||||
|
{
|
||||||
|
var selected = selection.OfType<BusRow>().FirstOrDefault();
|
||||||
|
if (selected == null)
|
||||||
|
return;
|
||||||
|
_selectedBus = selected.FilterKey;
|
||||||
|
UpdateBusDescription(selected);
|
||||||
|
ApplyFilter();
|
||||||
|
};
|
||||||
|
pane.Add(_busList);
|
||||||
|
|
||||||
|
_busDescription = new Label();
|
||||||
|
_busDescription.style.minHeight = 48f;
|
||||||
|
_busDescription.style.paddingLeft = 10f;
|
||||||
|
_busDescription.style.paddingRight = 8f;
|
||||||
|
_busDescription.style.paddingTop = 7f;
|
||||||
|
_busDescription.style.paddingBottom = 6f;
|
||||||
|
_busDescription.style.borderTopWidth = 1f;
|
||||||
|
_busDescription.style.borderTopColor = BorderColor();
|
||||||
|
_busDescription.style.color = MutedColor;
|
||||||
|
_busDescription.style.whiteSpace = WhiteSpace.Normal;
|
||||||
|
pane.Add(_busDescription);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildEventWorkspace()
|
||||||
|
{
|
||||||
|
var split = new TwoPaneSplitView(1, 185f, TwoPaneSplitViewOrientation.Vertical);
|
||||||
|
split.style.flexGrow = 1f;
|
||||||
|
split.Add(BuildEventPane());
|
||||||
|
split.Add(BuildDetailPane());
|
||||||
|
return split;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildEventPane()
|
||||||
|
{
|
||||||
|
var pane = new VisualElement();
|
||||||
|
pane.style.flexGrow = 1f;
|
||||||
|
pane.Add(BuildEventHeader());
|
||||||
|
|
||||||
|
_eventList = new ListView(_filteredEvents, 28f, MakeEventRow, BindEventRow)
|
||||||
|
{
|
||||||
|
selectionType = SelectionType.Single,
|
||||||
|
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
|
||||||
|
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight
|
||||||
|
};
|
||||||
|
_eventList.style.flexGrow = 1f;
|
||||||
|
_eventList.selectionChanged += selection =>
|
||||||
|
{
|
||||||
|
var selected = selection.OfType<EventRow>().FirstOrDefault();
|
||||||
|
if (selected == null)
|
||||||
|
return;
|
||||||
|
_selectedEventSequence = selected.Sequence;
|
||||||
|
ShowEventDetails(selected);
|
||||||
|
};
|
||||||
|
pane.Add(_eventList);
|
||||||
|
|
||||||
|
_eventEmptyState = new Label("暂无事件");
|
||||||
|
_eventEmptyState.style.flexGrow = 1f;
|
||||||
|
_eventEmptyState.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
_eventEmptyState.style.color = MutedColor;
|
||||||
|
pane.Add(_eventEmptyState);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildDetailPane()
|
||||||
|
{
|
||||||
|
var pane = new VisualElement();
|
||||||
|
pane.style.flexGrow = 1f;
|
||||||
|
pane.style.backgroundColor = PanelColor();
|
||||||
|
pane.style.borderTopWidth = 1f;
|
||||||
|
pane.style.borderTopColor = BorderColor();
|
||||||
|
|
||||||
|
_detailTitle = new Label("未选择事件");
|
||||||
|
_detailTitle.style.fontSize = 13f;
|
||||||
|
_detailTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
_detailTitle.style.marginLeft = 12f;
|
||||||
|
_detailTitle.style.marginTop = 9f;
|
||||||
|
_detailTitle.style.marginBottom = 7f;
|
||||||
|
pane.Add(_detailTitle);
|
||||||
|
|
||||||
|
var grid = new VisualElement();
|
||||||
|
grid.style.flexDirection = FlexDirection.Row;
|
||||||
|
grid.style.flexWrap = Wrap.Wrap;
|
||||||
|
grid.style.paddingLeft = 8f;
|
||||||
|
grid.style.paddingRight = 8f;
|
||||||
|
_detailTime = AddDetailField(grid, "时间");
|
||||||
|
_detailBus = AddDetailField(grid, "总线");
|
||||||
|
_detailExecution = AddDetailField(grid, "执行方式");
|
||||||
|
_detailThread = AddDetailField(grid, "线程");
|
||||||
|
_detailDuration = AddDetailField(grid, "分发耗时");
|
||||||
|
_detailResult = AddDetailField(grid, "结果");
|
||||||
|
pane.Add(grid);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label AddDetailField(VisualElement grid, string caption)
|
||||||
|
{
|
||||||
|
var field = new VisualElement();
|
||||||
|
field.style.width = Length.Percent(33.333f);
|
||||||
|
field.style.minWidth = 190f;
|
||||||
|
field.style.paddingLeft = 5f;
|
||||||
|
field.style.paddingRight = 5f;
|
||||||
|
field.style.paddingBottom = 8f;
|
||||||
|
|
||||||
|
var captionLabel = new Label(caption);
|
||||||
|
captionLabel.style.fontSize = 9f;
|
||||||
|
captionLabel.style.color = MutedColor;
|
||||||
|
field.Add(captionLabel);
|
||||||
|
|
||||||
|
var value = new Label("-");
|
||||||
|
value.style.marginTop = 2f;
|
||||||
|
value.style.overflow = Overflow.Hidden;
|
||||||
|
value.style.whiteSpace = WhiteSpace.Normal;
|
||||||
|
field.Add(value);
|
||||||
|
grid.Add(field);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement BuildBusHeader()
|
||||||
|
{
|
||||||
|
var header = BuildHeaderContainer("总线");
|
||||||
|
var columns = BuildColumnRow();
|
||||||
|
columns.Add(MakeHeaderCell("标识", 1f));
|
||||||
|
columns.Add(MakeHeaderCell("调度器", 0f, 92f));
|
||||||
|
columns.Add(MakeHeaderCell("处理器", 0f, 58f, TextAnchor.MiddleRight));
|
||||||
|
header.Add(columns);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement BuildEventHeader()
|
||||||
|
{
|
||||||
|
var header = BuildHeaderContainer("事件流");
|
||||||
|
var columns = BuildColumnRow();
|
||||||
|
columns.Add(MakeHeaderCell("时间", 0f, 92f));
|
||||||
|
columns.Add(MakeHeaderCell("总线", 0f, 132f));
|
||||||
|
columns.Add(MakeHeaderCell("事件", 1f));
|
||||||
|
columns.Add(MakeHeaderCell("方式", 0f, 54f));
|
||||||
|
columns.Add(MakeHeaderCell("线程", 0f, 56f, TextAnchor.MiddleRight));
|
||||||
|
columns.Add(MakeHeaderCell("耗时", 0f, 82f, TextAnchor.MiddleRight));
|
||||||
|
columns.Add(MakeHeaderCell("结果", 0f, 90f));
|
||||||
|
header.Add(columns);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement BuildHeaderContainer(string title)
|
||||||
|
{
|
||||||
|
var header = new VisualElement();
|
||||||
|
header.style.height = 52f;
|
||||||
|
header.style.borderBottomWidth = 1f;
|
||||||
|
header.style.borderBottomColor = BorderColor();
|
||||||
|
header.style.backgroundColor = HeaderColor();
|
||||||
|
var titleLabel = new Label(title);
|
||||||
|
titleLabel.style.height = 27f;
|
||||||
|
titleLabel.style.paddingLeft = 10f;
|
||||||
|
titleLabel.style.paddingTop = 7f;
|
||||||
|
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
titleLabel.style.fontSize = 10f;
|
||||||
|
header.Add(titleLabel);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement BuildColumnRow()
|
||||||
|
{
|
||||||
|
var row = new VisualElement();
|
||||||
|
row.style.height = 24f;
|
||||||
|
row.style.flexDirection = FlexDirection.Row;
|
||||||
|
row.style.alignItems = Align.Center;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label MakeHeaderCell(string text, float grow, float width = 0f,
|
||||||
|
TextAnchor alignment = TextAnchor.MiddleLeft)
|
||||||
|
{
|
||||||
|
var label = MakeCell(string.Empty, grow, width, alignment);
|
||||||
|
label.text = text;
|
||||||
|
label.style.fontSize = 9f;
|
||||||
|
label.style.color = MutedColor;
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement MakeBusRow()
|
||||||
|
{
|
||||||
|
var row = new VisualElement { name = "bus-row" };
|
||||||
|
row.style.flexDirection = FlexDirection.Row;
|
||||||
|
row.style.alignItems = Align.Center;
|
||||||
|
row.Add(MakeCell("key", 1f));
|
||||||
|
row.Add(MakeCell("scheduler", 0f, 92f));
|
||||||
|
row.Add(MakeCell("handlers", 0f, 58f, TextAnchor.MiddleRight));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindBusRow(VisualElement element, int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _buses.Count)
|
||||||
|
return;
|
||||||
|
var item = _buses[index];
|
||||||
|
element.Q<Label>("key").text = item.Key;
|
||||||
|
element.Q<Label>("scheduler").text = item.Scheduler;
|
||||||
|
element.Q<Label>("handlers").text = item.Subscribers.ToString(CultureInfo.InvariantCulture);
|
||||||
|
element.tooltip = item.FilterKey.Length == 0
|
||||||
|
? "汇总视图"
|
||||||
|
: $"{item.Mode} | 队列 {item.Queue} | 溢出策略 {item.Overflow}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement MakeEventRow()
|
||||||
|
{
|
||||||
|
var row = new VisualElement { name = "event-row" };
|
||||||
|
row.style.flexDirection = FlexDirection.Row;
|
||||||
|
row.style.alignItems = Align.Center;
|
||||||
|
row.Add(MakeCell("time", 0f, 92f));
|
||||||
|
row.Add(MakeCell("bus", 0f, 132f));
|
||||||
|
row.Add(MakeCell("type", 1f));
|
||||||
|
row.Add(MakeCell("kind", 0f, 54f));
|
||||||
|
row.Add(MakeCell("thread", 0f, 56f, TextAnchor.MiddleRight));
|
||||||
|
row.Add(MakeCell("latency", 0f, 82f, TextAnchor.MiddleRight));
|
||||||
|
row.Add(MakeCell("status", 0f, 90f));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindEventRow(VisualElement element, int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _filteredEvents.Count)
|
||||||
|
return;
|
||||||
|
var item = _filteredEvents[index];
|
||||||
|
element.Q<Label>("time").text = item.TimestampUtc.ToLocalTime().ToString("HH:mm:ss.fff");
|
||||||
|
element.Q<Label>("bus").text = item.Bus;
|
||||||
|
element.Q<Label>("type").text = item.ShortEventType;
|
||||||
|
element.Q<Label>("kind").text = item.IsAsync ? "异步" : "同步";
|
||||||
|
element.Q<Label>("thread").text = "线程 " + item.ThreadId;
|
||||||
|
element.Q<Label>("latency").text = FormatDuration(item.DurationMicroseconds);
|
||||||
|
var status = element.Q<Label>("status");
|
||||||
|
status.text = item.Status;
|
||||||
|
status.style.color = item.Failure != ShrinkPostFailure.None
|
||||||
|
? ErrorColor
|
||||||
|
: item.Canceled
|
||||||
|
? WarningColor
|
||||||
|
: item.Handled
|
||||||
|
? SuccessColor
|
||||||
|
: MutedColor;
|
||||||
|
element.tooltip = $"{item.EventType}\n{item.Scheduler} / {item.Mode} / 线程 {item.ThreadId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label MakeCell(string name, float grow, float width = 0f,
|
||||||
|
TextAnchor alignment = TextAnchor.MiddleLeft)
|
||||||
|
{
|
||||||
|
var label = new Label { name = name };
|
||||||
|
label.style.flexGrow = grow;
|
||||||
|
label.style.minWidth = 0f;
|
||||||
|
label.style.paddingLeft = 6f;
|
||||||
|
label.style.paddingRight = 4f;
|
||||||
|
label.style.unityTextAlign = alignment;
|
||||||
|
label.style.overflow = Overflow.Hidden;
|
||||||
|
label.style.whiteSpace = WhiteSpace.NoWrap;
|
||||||
|
if (width > 0f)
|
||||||
|
{
|
||||||
|
label.style.width = width;
|
||||||
|
label.style.flexShrink = 0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
label.style.flexShrink = 1f;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordEvent(ShrinkEventTrace trace)
|
||||||
|
{
|
||||||
|
var row = new EventRow
|
||||||
|
{
|
||||||
|
Sequence = Interlocked.Increment(ref _nextSequence),
|
||||||
|
TimestampUtc = trace.TimestampUtc,
|
||||||
|
Bus = trace.BusKey.Id,
|
||||||
|
EventType = trace.EventType.FullName ?? trace.EventType.Name,
|
||||||
|
ShortEventType = trace.EventType.Name,
|
||||||
|
Scheduler = FormatScheduler(trace.Scheduler),
|
||||||
|
Mode = FormatDispatchMode(trace.DispatchMode),
|
||||||
|
ThreadId = trace.ThreadId,
|
||||||
|
DurationMicroseconds = trace.ElapsedTimestampTicks * (1_000_000d / Stopwatch.Frequency),
|
||||||
|
IsAsync = trace.IsAsync,
|
||||||
|
Accepted = trace.Result.Accepted,
|
||||||
|
Handled = trace.Result.Handled,
|
||||||
|
Canceled = trace.Result.Canceled,
|
||||||
|
Failure = trace.Result.Failure
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
if (_pendingEvents.Count >= MaxPendingEvents)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _droppedCaptureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pendingEvents.Enqueue(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
if (EditorApplication.timeSinceStartup < _nextRefresh)
|
||||||
|
return;
|
||||||
|
_nextRefresh = EditorApplication.timeSinceStartup + 0.15;
|
||||||
|
DrainEvents();
|
||||||
|
RefreshBuses();
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshAll()
|
||||||
|
{
|
||||||
|
DrainEvents();
|
||||||
|
RefreshBuses();
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshBuses()
|
||||||
|
{
|
||||||
|
var snapshot = EventBus.Snapshot()
|
||||||
|
.OrderBy(item => item.Key.Id, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
var totalSubscribers = snapshot.Sum(item => item.Subscribers);
|
||||||
|
|
||||||
|
_buses.Clear();
|
||||||
|
_buses.Add(new BusRow
|
||||||
|
{
|
||||||
|
Key = "全部总线",
|
||||||
|
FilterKey = string.Empty,
|
||||||
|
Scheduler = "混合",
|
||||||
|
Mode = "混合",
|
||||||
|
Queue = "-",
|
||||||
|
Overflow = "-",
|
||||||
|
Subscribers = totalSubscribers
|
||||||
|
});
|
||||||
|
foreach (var bus in snapshot)
|
||||||
|
{
|
||||||
|
_buses.Add(new BusRow
|
||||||
|
{
|
||||||
|
Key = bus.Key.Id,
|
||||||
|
FilterKey = bus.Key.Id,
|
||||||
|
Scheduler = FormatScheduler(bus.Options.Scheduler),
|
||||||
|
Mode = FormatDispatchMode(bus.Options.DispatchMode),
|
||||||
|
Queue = bus.Options.QueueCapacity.ToString(CultureInfo.InvariantCulture),
|
||||||
|
Overflow = FormatOverflowPolicy(bus.Options.OverflowPolicy),
|
||||||
|
Subscribers = bus.Subscribers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_busList?.RefreshItems();
|
||||||
|
var selectedIndex = Math.Max(0, _buses.FindIndex(item => item.FilterKey == _selectedBus));
|
||||||
|
if (_busList != null && _busList.selectedIndex != selectedIndex)
|
||||||
|
_busList.selectedIndex = selectedIndex;
|
||||||
|
if (selectedIndex < _buses.Count)
|
||||||
|
UpdateBusDescription(_buses[selectedIndex]);
|
||||||
|
|
||||||
|
if (_busCountMetric != null)
|
||||||
|
_busCountMetric.text = snapshot.Length.ToString(CultureInfo.InvariantCulture);
|
||||||
|
if (_handlerCountMetric != null)
|
||||||
|
_handlerCountMetric.text = totalSubscribers.ToString("N0", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrainEvents()
|
||||||
|
{
|
||||||
|
lock (_eventGate)
|
||||||
|
{
|
||||||
|
while (_pendingEvents.Count > 0)
|
||||||
|
_events.Add(_pendingEvents.Dequeue());
|
||||||
|
}
|
||||||
|
if (_events.Count > MaxEvents)
|
||||||
|
_events.RemoveRange(0, _events.Count - MaxEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var query = _search?.value?.Trim() ?? string.Empty;
|
||||||
|
var problemOnly = _problemOnlyToggle?.value == true;
|
||||||
|
_filteredEvents.Clear();
|
||||||
|
for (var i = _events.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var item = _events[i];
|
||||||
|
if (_selectedBus.Length > 0 && !string.Equals(item.Bus, _selectedBus, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
if (problemOnly && item.Failure == ShrinkPostFailure.None && !item.Canceled && item.Accepted)
|
||||||
|
continue;
|
||||||
|
if (query.Length > 0 &&
|
||||||
|
item.Bus.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||||
|
item.EventType.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
continue;
|
||||||
|
_filteredEvents.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_eventList != null)
|
||||||
|
{
|
||||||
|
var hasVisibleEvents = _filteredEvents.Count > 0;
|
||||||
|
_eventList.style.display = hasVisibleEvents ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
if (hasVisibleEvents)
|
||||||
|
_eventList.RefreshItems();
|
||||||
|
}
|
||||||
|
if (_eventEmptyState != null)
|
||||||
|
_eventEmptyState.style.display = _filteredEvents.Count == 0
|
||||||
|
? DisplayStyle.Flex
|
||||||
|
: DisplayStyle.None;
|
||||||
|
RestoreEventSelection();
|
||||||
|
UpdateMetrics();
|
||||||
|
UpdateStatus();
|
||||||
|
if (_autoScrollToggle?.value == true && _filteredEvents.Count > 0 && _selectedEventSequence == 0)
|
||||||
|
_eventList?.ScrollToItem(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreEventSelection()
|
||||||
|
{
|
||||||
|
if (_eventList == null || _selectedEventSequence == 0)
|
||||||
|
return;
|
||||||
|
var index = _filteredEvents.FindIndex(item => item.Sequence == _selectedEventSequence);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
_selectedEventSequence = 0;
|
||||||
|
_eventList.ClearSelection();
|
||||||
|
ClearEventDetails();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_eventList.selectedIndex != index)
|
||||||
|
_eventList.selectedIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMetrics()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var recent = 0;
|
||||||
|
var problems = 0;
|
||||||
|
var duration = 0d;
|
||||||
|
for (var i = 0; i < _filteredEvents.Count; i++)
|
||||||
|
{
|
||||||
|
var item = _filteredEvents[i];
|
||||||
|
if ((now - item.TimestampUtc).TotalSeconds <= 1d)
|
||||||
|
recent++;
|
||||||
|
if (item.Failure != ShrinkPostFailure.None || item.Canceled || !item.Accepted)
|
||||||
|
problems++;
|
||||||
|
duration += item.DurationMicroseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_capturedMetric != null)
|
||||||
|
_capturedMetric.text = _events.Count.ToString("N0", CultureInfo.InvariantCulture);
|
||||||
|
if (_rateMetric != null)
|
||||||
|
_rateMetric.text = recent.ToString("N0", CultureInfo.InvariantCulture);
|
||||||
|
if (_latencyMetric != null)
|
||||||
|
_latencyMetric.text = _filteredEvents.Count == 0
|
||||||
|
? "0 微秒"
|
||||||
|
: FormatDuration(duration / _filteredEvents.Count);
|
||||||
|
if (_problemMetric != null)
|
||||||
|
{
|
||||||
|
_problemMetric.text = problems.ToString("N0", CultureInfo.InvariantCulture);
|
||||||
|
_problemMetric.style.color = problems > 0 ? ErrorColor : SuccessColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStatus()
|
||||||
|
{
|
||||||
|
if (_status == null)
|
||||||
|
return;
|
||||||
|
var dropped = Interlocked.Read(ref _droppedCaptureCount);
|
||||||
|
var scope = _selectedBus.Length == 0 ? "全部总线" : _selectedBus;
|
||||||
|
_status.text = $"可见 {_filteredEvents.Count:N0} 条 / 保留 {_events.Count:N0} 条 | {scope} | 上限 {MaxEvents:N0} 条" +
|
||||||
|
(dropped > 0 ? $" | 捕获溢出 {dropped:N0} 条" : string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBusDescription(BusRow row)
|
||||||
|
{
|
||||||
|
if (_busDescription == null)
|
||||||
|
return;
|
||||||
|
_busDescription.text = row.FilterKey.Length == 0
|
||||||
|
? $"汇总 | {row.Subscribers:N0} 个处理器"
|
||||||
|
: $"{row.Scheduler} / {row.Mode}\n队列 {row.Queue} | 溢出策略 {row.Overflow} | {row.Subscribers:N0} 个处理器";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowEventDetails(EventRow item)
|
||||||
|
{
|
||||||
|
if (_detailTitle == null)
|
||||||
|
return;
|
||||||
|
_detailTitle.text = item.EventType;
|
||||||
|
_detailTitle.tooltip = item.EventType;
|
||||||
|
if (_detailTime != null)
|
||||||
|
_detailTime.text = item.TimestampUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||||
|
if (_detailBus != null)
|
||||||
|
_detailBus.text = item.Bus;
|
||||||
|
if (_detailExecution != null)
|
||||||
|
_detailExecution.text = $"{(item.IsAsync ? "异步" : "同步")} | {item.Scheduler} / {item.Mode}";
|
||||||
|
if (_detailThread != null)
|
||||||
|
_detailThread.text = "托管线程 " + item.ThreadId;
|
||||||
|
if (_detailDuration != null)
|
||||||
|
_detailDuration.text = item.DurationMicroseconds.ToString("N3", CultureInfo.InvariantCulture) + " 微秒";
|
||||||
|
if (_detailResult != null)
|
||||||
|
{
|
||||||
|
_detailResult.text = $"{item.Status} | 已接收={FormatBoolean(item.Accepted)} 已处理={FormatBoolean(item.Handled)} 已取消={FormatBoolean(item.Canceled)}";
|
||||||
|
_detailResult.style.color = item.Failure != ShrinkPostFailure.None
|
||||||
|
? ErrorColor
|
||||||
|
: item.Canceled
|
||||||
|
? WarningColor
|
||||||
|
: SuccessColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEventDetails()
|
||||||
|
{
|
||||||
|
if (_detailTitle != null)
|
||||||
|
_detailTitle.text = "未选择事件";
|
||||||
|
foreach (var label in new[]
|
||||||
|
{
|
||||||
|
_detailTime, _detailBus, _detailExecution, _detailThread, _detailDuration, _detailResult
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (label != null)
|
||||||
|
{
|
||||||
|
label.text = "-";
|
||||||
|
label.style.color = StyleKeyword.Null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDiagnosticsSubscription(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == _diagnosticsSubscribed)
|
||||||
|
return;
|
||||||
|
if (enabled)
|
||||||
|
EventBus.DetailedPosted += RecordEvent;
|
||||||
|
else
|
||||||
|
EventBus.DetailedPosted -= RecordEvent;
|
||||||
|
_diagnosticsSubscribed = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCaptureState()
|
||||||
|
{
|
||||||
|
if (_captureState == null)
|
||||||
|
return;
|
||||||
|
_captureState.text = _diagnosticsSubscribed ? "实时" : "已暂停";
|
||||||
|
_captureState.style.color = _diagnosticsSubscribed ? SuccessColor : WarningColor;
|
||||||
|
_captureState.style.backgroundColor = _diagnosticsSubscribed
|
||||||
|
? new Color(0.12f, 0.28f, 0.19f)
|
||||||
|
: new Color(0.32f, 0.25f, 0.11f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEvents()
|
||||||
|
{
|
||||||
|
lock (_eventGate)
|
||||||
|
_pendingEvents.Clear();
|
||||||
|
_events.Clear();
|
||||||
|
_filteredEvents.Clear();
|
||||||
|
_selectedEventSequence = 0;
|
||||||
|
Interlocked.Exchange(ref _droppedCaptureCount, 0);
|
||||||
|
_eventList?.ClearSelection();
|
||||||
|
ClearEventDetails();
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExportCsv()
|
||||||
|
{
|
||||||
|
var path = EditorUtility.SaveFilePanel("导出 EventBus 跟踪记录", string.Empty,
|
||||||
|
$"ShrinkEventBus-{DateTime.Now:yyyyMMdd-HHmmss}.csv", "csv");
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var builder = new StringBuilder(Math.Max(256, _filteredEvents.Count * 128));
|
||||||
|
builder.AppendLine("序号,UTC时间,总线,事件类型,调度器,分发模式,执行方式,线程ID,耗时微秒,已接收,已处理,已取消,失败原因");
|
||||||
|
for (var i = _filteredEvents.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var item = _filteredEvents[i];
|
||||||
|
builder.Append(item.Sequence).Append(',')
|
||||||
|
.Append(item.TimestampUtc.ToString("O")).Append(',')
|
||||||
|
.Append(Csv(item.Bus)).Append(',')
|
||||||
|
.Append(Csv(item.EventType)).Append(',')
|
||||||
|
.Append(item.Scheduler).Append(',')
|
||||||
|
.Append(item.Mode).Append(',')
|
||||||
|
.Append(item.IsAsync ? "异步" : "同步").Append(',')
|
||||||
|
.Append(item.ThreadId).Append(',')
|
||||||
|
.Append(item.DurationMicroseconds.ToString("F3", CultureInfo.InvariantCulture)).Append(',')
|
||||||
|
.Append(FormatBoolean(item.Accepted)).Append(',')
|
||||||
|
.Append(FormatBoolean(item.Handled)).Append(',')
|
||||||
|
.Append(FormatBoolean(item.Canceled)).Append(',')
|
||||||
|
.Append(FormatFailure(item.Failure)).AppendLine();
|
||||||
|
}
|
||||||
|
File.WriteAllText(path, builder.ToString(), new UTF8Encoding(false));
|
||||||
|
EditorUtility.RevealInFinder(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Csv(string value) =>
|
||||||
|
'"' + value.Replace("\"", "\"\"") + '"';
|
||||||
|
|
||||||
|
private static string FormatDuration(double microseconds) => microseconds < 1000d
|
||||||
|
? microseconds.ToString("N2", CultureInfo.InvariantCulture) + " 微秒"
|
||||||
|
: (microseconds / 1000d).ToString("N2", CultureInfo.InvariantCulture) + " 毫秒";
|
||||||
|
|
||||||
|
private static string FormatBoolean(bool value) => value ? "是" : "否";
|
||||||
|
|
||||||
|
private static string FormatScheduler(ShrinkBusSchedulerKind scheduler) => scheduler switch
|
||||||
|
{
|
||||||
|
ShrinkBusSchedulerKind.Inline => "当前线程",
|
||||||
|
ShrinkBusSchedulerKind.MainThread => "主线程",
|
||||||
|
ShrinkBusSchedulerKind.DedicatedThread => "专属线程",
|
||||||
|
ShrinkBusSchedulerKind.TaskPool => "任务池",
|
||||||
|
_ => scheduler.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatDispatchMode(ShrinkDispatchMode mode) => mode switch
|
||||||
|
{
|
||||||
|
ShrinkDispatchMode.Ordered => "有序",
|
||||||
|
ShrinkDispatchMode.Parallel => "并行",
|
||||||
|
_ => mode.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatOverflowPolicy(ShrinkQueueOverflowPolicy policy) => policy switch
|
||||||
|
{
|
||||||
|
ShrinkQueueOverflowPolicy.Reject => "拒绝",
|
||||||
|
ShrinkQueueOverflowPolicy.DropNewest => "丢弃最新",
|
||||||
|
ShrinkQueueOverflowPolicy.DropOldest => "丢弃最旧",
|
||||||
|
ShrinkQueueOverflowPolicy.Wait => "等待",
|
||||||
|
_ => policy.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatFailure(ShrinkPostFailure failure) => failure switch
|
||||||
|
{
|
||||||
|
ShrinkPostFailure.None => "无",
|
||||||
|
ShrinkPostFailure.BusStopped => "总线已停止",
|
||||||
|
ShrinkPostFailure.QueueFull => "队列已满",
|
||||||
|
ShrinkPostFailure.Canceled => "已取消",
|
||||||
|
ShrinkPostFailure.HandlerException => "处理器异常",
|
||||||
|
ShrinkPostFailure.InvalidEvent => "无效事件",
|
||||||
|
_ => failure.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color PanelColor() => EditorGUIUtility.isProSkin
|
||||||
|
? new Color(0.13f, 0.137f, 0.15f)
|
||||||
|
: new Color(0.94f, 0.95f, 0.96f);
|
||||||
|
|
||||||
|
private static Color HeaderColor() => EditorGUIUtility.isProSkin
|
||||||
|
? new Color(0.16f, 0.168f, 0.183f)
|
||||||
|
: new Color(0.82f, 0.84f, 0.87f);
|
||||||
|
|
||||||
|
private static Color BorderColor() => EditorGUIUtility.isProSkin
|
||||||
|
? new Color(0.24f, 0.25f, 0.28f)
|
||||||
|
: new Color(0.66f, 0.68f, 0.72f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 16e1b18e2a2748e494f2ba648b4697a8
|
||||||
|
timeCreated: 1772878417
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ShrinkEventBus.Editor",
|
||||||
|
"rootNamespace": "ShrinkEventBus.Editor",
|
||||||
|
"references": [
|
||||||
|
"ShrinkEventBus.Runtime",
|
||||||
|
"UniTask"
|
||||||
|
],
|
||||||
|
"includePlatforms": ["Editor"],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"autoReferenced": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f0a89bdb25764f1f8a8e4dcc915abe39
|
||||||
|
timeCreated: 1772900825
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Eicy
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a726eae075081a24785e6f3f4a4efe6d
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# ShrinkEventBus 2.0
|
||||||
|
|
||||||
|
ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。
|
||||||
|
|
||||||
|
## 核心契约
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IShrinkEvent { }
|
||||||
|
|
||||||
|
public interface IShrinkCancelableEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
bool IsCanceled { get; }
|
||||||
|
void SetCanceled(bool value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkResultEvent<TResult> : IShrinkEvent
|
||||||
|
{
|
||||||
|
TResult Result { get; }
|
||||||
|
void SetResult(TResult result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
事件可以是 class、struct 或 unmanaged struct,不需要继承 SDK 基类。
|
||||||
|
|
||||||
|
## 多 Bus
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var gameBus = EventBus.GetOrCreateBus(
|
||||||
|
ShrinkBusKey.Game,
|
||||||
|
ShrinkBusOptions.MainThread());
|
||||||
|
|
||||||
|
var modBus = EventBus.CreateBus(
|
||||||
|
ShrinkBusKey.Mod("com.example.mod"),
|
||||||
|
ShrinkBusOptions.DedicatedThread());
|
||||||
|
|
||||||
|
var workerBus = EventBus.CreateBus(
|
||||||
|
new ShrinkBusKey("worker", "pathfinding"),
|
||||||
|
ShrinkBusOptions.TaskPool(maxConcurrency: 4));
|
||||||
|
```
|
||||||
|
|
||||||
|
标准 Key 包括 `Game`、`Server`、`Scene(id)`、`Mod(id)` 和 `World(id)`。调度器在 Bus 创建时确定,之后不可修改:
|
||||||
|
|
||||||
|
| Scheduler | 语义 |
|
||||||
|
|---|---|
|
||||||
|
| `Inline` | 在发布线程立即执行 |
|
||||||
|
| `MainThread` | Unity PlayerLoop 主线程;已在主线程时直调 |
|
||||||
|
| `DedicatedThread` | 有界队列、专属线程、Ordered 串行 |
|
||||||
|
| `TaskPool` | 有界队列、显式最大并发;Parallel 忽略优先级顺序 |
|
||||||
|
|
||||||
|
`Post` 表示投递:同线程同步完成,跨线程入队后返回。`PostAsync` 等待全部 handler 完成,并传播取消与异常。需要结果或最终取消状态时使用 `PostAsync`。
|
||||||
|
|
||||||
|
## 唯一订阅入口
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ShrinkEventSubscriber(
|
||||||
|
OwnerId = "com.example.mod",
|
||||||
|
DefaultBus = "mod:com.example.mod")]
|
||||||
|
public sealed class PlayerHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe(Priority = ShrinkEventPriority.High)]
|
||||||
|
private void OnJoined(PlayerJoinedEvent value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private async UniTask OnLoaded(
|
||||||
|
PlayerLoadedEvent value,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await UniTask.Yield(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bus 解析顺序:方法 `Bus`、类型 `DefaultBus`、`Attach` 传入默认 Bus、否则 `game`。
|
||||||
|
|
||||||
|
普通对象由宿主管理生命周期:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var binding = EventBus.Attach(new PlayerHandlers());
|
||||||
|
```
|
||||||
|
|
||||||
|
MonoBehaviour 不需要继承 SDK 基类。可自行在 `OnEnable/OnDisable` 中 Attach/Dispose,也可以在 GameObject 上添加 `ShrinkMonoEventScope`,由它统一绑定同对象或子层级中的生成 subscriber。
|
||||||
|
|
||||||
|
静态类型同样只使用特性:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
public static class GlobalHandlers
|
||||||
|
{
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private static void OnStartup(GameStartupEvent value) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。
|
||||||
|
|
||||||
|
## Unity 与纯 .NET
|
||||||
|
|
||||||
|
Unity 包依赖 UniTask:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
纯 .NET 实现在 `Tools~/DotNet/ShrinkEventBus.Core`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。
|
||||||
|
|
||||||
|
## ECS/Burst
|
||||||
|
|
||||||
|
`com.cneicy.shrink-eventbus-entities` 提供:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ShrinkEcsEventWriter<TEvent>
|
||||||
|
ShrinkEcsEventQueue<TEvent>.Playback(IShrinkEventBus bus)
|
||||||
|
```
|
||||||
|
|
||||||
|
Burst Job 只写 unmanaged 事实事件到 NativeQueue;Playback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。
|
||||||
|
|
||||||
|
## 队列与关闭
|
||||||
|
|
||||||
|
`ShrinkBusOptions` 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 `Reject`;`DropNewest/DropOldest` 只适用于明确允许丢失的数据;`Wait` 只由 `PostAsync` 使用。
|
||||||
|
|
||||||
|
## UI Toolkit 调试器
|
||||||
|
|
||||||
|
菜单:`ShrinkSDK/事件总线/事件查看器`。
|
||||||
|
|
||||||
|
窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、`ShrinkPostResult`、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 `Capture` 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。
|
||||||
|
|
||||||
|
它只观察静态 `EventBus` 宿主管理的 Bus,不捕获独立 `ShrinkEventBusHost`;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。
|
||||||
|
|
||||||
|
## Benchmark
|
||||||
|
|
||||||
|
实现位于 `Benchmark/ShrinkEventBusBenchmark.cs`,入口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ShrinkEventBusBenchmark.Run(
|
||||||
|
iterations: 1_000_000,
|
||||||
|
massIterations: 10_000,
|
||||||
|
registrationIterations: 5_000);
|
||||||
|
```
|
||||||
|
|
||||||
|
2026-08-24 在 Unity `2022.3.62f3`、Windows Editor Play Mode、独立 Inline Host 下三轮中位数(全局调试采样不观察该 Host):
|
||||||
|
|
||||||
|
| 场景 | 2.0 ops/s | 1.3.0 基线 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 0 handler | 129,282,649 | 8,971,378 |
|
||||||
|
| 1 handler | 67,249,586 | 6,132,242 |
|
||||||
|
| 8 handlers, struct | 35,024,798 | 无同口径数据 |
|
||||||
|
| 8 handlers, class | 25,321,199 | MessagePipe 图示 25,639,260 |
|
||||||
|
| 30 handlers | 10,823,612 | 258,805 |
|
||||||
|
| canceled skips 10 | 9,029,517 | 1,894,776 |
|
||||||
|
| 1 async handler | 5,909,446 | 1,168,634 |
|
||||||
|
| generated Attach + Dispose | 674,291 | instance scan 76,353 |
|
||||||
|
|
||||||
|
同步 struct `Post` 热路径实测为 `0 B / 10,000,000 ops`;全局门面在没有 Network bridge 对象观察者时也不会为 struct 事件装箱。MessagePipe 图来自不同机器和 .NET benchmark,class 结果只能说明已达到同一吞吐量级,不能作为跨设备胜负结论。当前 Windows x64 Development IL2CPP Player 已构建成功;仍应在目标平台 Player/设备上复测吞吐。
|
||||||
|
|
||||||
|
## 设计边界
|
||||||
|
|
||||||
|
- handler 不单独选择线程;需要不同线程亲和性时发布到另一个 Bus。
|
||||||
|
- Parallel 分发不承诺优先级顺序。
|
||||||
|
- 同步 `Post` 遇到异步 handler 时是 fire-and-forget;需要等待使用 `PostAsync`。
|
||||||
|
- 外部 Mod DLL 必须携带生成合同;没有合同的生产 DLL 不自动反射注册。
|
||||||
|
- Editor 诊断是可选观察层,不参与正式无诊断热路径。
|
||||||
|
|
||||||
|
架构审计与来源说明见 `Docs/JustAnyProjectArchitectureAudit.md` 和根目录 `DESIGN.md`。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e08e301c95a34847a1071cd96bba1eeb
|
||||||
|
timeCreated: 1772900157
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c410edd1d6b644b498d39ff5e1fbc98b
|
||||||
|
timeCreated: 1772900792
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
|
||||||
|
[assembly: InternalsVisibleTo("ShrinkEventBus.Editor")]
|
||||||
|
[assembly: InternalsVisibleTo("ShrinkEventBus.Tests")]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 43e85f78c24f1d94ba7d06a4fa37e937
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
internal readonly struct ShrinkEventTrace
|
||||||
|
{
|
||||||
|
public ShrinkEventTrace(DateTime timestampUtc, Type eventType, ShrinkBusKey busKey,
|
||||||
|
ShrinkBusOptions options, int threadId, long elapsedTimestampTicks,
|
||||||
|
ShrinkPostResult result, bool isAsync)
|
||||||
|
{
|
||||||
|
TimestampUtc = timestampUtc;
|
||||||
|
EventType = eventType;
|
||||||
|
BusKey = busKey;
|
||||||
|
Scheduler = options.Scheduler;
|
||||||
|
DispatchMode = options.DispatchMode;
|
||||||
|
ThreadId = threadId;
|
||||||
|
ElapsedTimestampTicks = elapsedTimestampTicks;
|
||||||
|
Result = result;
|
||||||
|
IsAsync = isAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime TimestampUtc { get; }
|
||||||
|
public Type EventType { get; }
|
||||||
|
public ShrinkBusKey BusKey { get; }
|
||||||
|
public ShrinkBusSchedulerKind Scheduler { get; }
|
||||||
|
public ShrinkDispatchMode DispatchMode { get; }
|
||||||
|
public int ThreadId { get; }
|
||||||
|
public long ElapsedTimestampTicks { get; }
|
||||||
|
public ShrinkPostResult Result { get; }
|
||||||
|
public bool IsAsync { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EventBus
|
||||||
|
{
|
||||||
|
private sealed class GlobalBusResolver : IShrinkBusResolver
|
||||||
|
{
|
||||||
|
public IShrinkEventBus GetBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
if (TryGetBus(key, out var bus))
|
||||||
|
return bus;
|
||||||
|
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
|
||||||
|
EventBus.TryGetBus(key, out bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> Buses = new();
|
||||||
|
internal static readonly IShrinkBusResolver Resolver = new GlobalBusResolver();
|
||||||
|
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
|
||||||
|
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
|
||||||
|
private static Action<ShrinkEventTrace>? _detailedPosted;
|
||||||
|
|
||||||
|
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
_posted += value;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
_posted -= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasPostedObservers
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => Volatile.Read(ref _posted) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static event Action<ShrinkEventTrace> DetailedPosted
|
||||||
|
{
|
||||||
|
add => _detailedPosted += value;
|
||||||
|
remove => _detailedPosted -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasDetailedPostedObservers
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => Volatile.Read(ref _detailedPosted) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IShrinkEventBus Default => DefaultBus;
|
||||||
|
|
||||||
|
public static IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
var bus = BuildBus(key, options);
|
||||||
|
if (!Buses.TryAdd(key, bus))
|
||||||
|
{
|
||||||
|
bus.Dispose();
|
||||||
|
throw new InvalidOperationException($"Bus '{key}' is already registered.");
|
||||||
|
}
|
||||||
|
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
var bus = Buses.GetOrAdd(key, busKey => BuildBus(busKey, options));
|
||||||
|
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
|
||||||
|
Buses.TryGetValue(key, out bus!);
|
||||||
|
|
||||||
|
public static bool RemoveBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
if (key == ShrinkBusKey.Game || !Buses.TryRemove(key, out var bus))
|
||||||
|
return false;
|
||||||
|
ShrinkStaticBindingRegistry.DetachBus(key);
|
||||||
|
bus.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShrinkPostResult Post<TEvent>(in TEvent eventData)
|
||||||
|
where TEvent : IShrinkEvent => DefaultBus.Post(in eventData);
|
||||||
|
|
||||||
|
public static UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
where TEvent : IShrinkEvent => DefaultBus.PostAsync(eventData, cancellationToken);
|
||||||
|
|
||||||
|
public static IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
if (target is not IShrinkGeneratedSubscriber generated)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
|
||||||
|
return generated.AttachGenerated(Resolver, defaultBus ?? ShrinkBusKey.Game);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IReadOnlyList<(ShrinkBusKey Key, ShrinkBusOptions Options, int Subscribers)> Snapshot()
|
||||||
|
{
|
||||||
|
var snapshot = new List<(ShrinkBusKey, ShrinkBusOptions, int)>(Buses.Count);
|
||||||
|
foreach (var pair in Buses)
|
||||||
|
{
|
||||||
|
var subscribers = pair.Value is ShrinkEventBusInstance instance
|
||||||
|
? instance.SubscriberCount
|
||||||
|
: 0;
|
||||||
|
snapshot.Add((pair.Key, pair.Value.Options, subscribers));
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IShrinkEventBus CreateDefaultBus()
|
||||||
|
{
|
||||||
|
var bus = BuildBus(ShrinkBusKey.Game, ShrinkBusOptions.MainThread());
|
||||||
|
Buses[ShrinkBusKey.Game] = bus;
|
||||||
|
ShrinkStaticBindingRegistry.AttachForBus(ShrinkBusKey.Game, Resolver);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IShrinkEventBus BuildBus(ShrinkBusKey key, ShrinkBusOptions options) =>
|
||||||
|
new ShrinkEventBusBuilder()
|
||||||
|
.WithKey(key)
|
||||||
|
.WithOptions(options)
|
||||||
|
.WithPostObserver(NotifyPostedObject)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void NotifyPostedObject(IShrinkEvent eventData, Type eventType, ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
_posted?.Invoke(eventData, eventType, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
|
||||||
|
{
|
||||||
|
var observers = Volatile.Read(ref _detailedPosted);
|
||||||
|
if (observers == null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
observers(trace);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b995a65fd1324255a5ade1ae1f0adf32
|
||||||
|
timeCreated: 1760099076
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public enum EventResult
|
||||||
|
{
|
||||||
|
DEFAULT,
|
||||||
|
ALLOW,
|
||||||
|
DENY
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5213a68ce8f433a98f272904a5d7bd2
|
||||||
|
timeCreated: 1760098791
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public interface IShrinkEventBus : IDisposable
|
||||||
|
{
|
||||||
|
ShrinkBusKey Key { get; }
|
||||||
|
ShrinkBusOptions Options { get; }
|
||||||
|
|
||||||
|
ShrinkPostResult Post<TEvent>(in TEvent eventData)
|
||||||
|
where TEvent : IShrinkEvent;
|
||||||
|
|
||||||
|
UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
where TEvent : IShrinkEvent;
|
||||||
|
|
||||||
|
IDisposable Attach(object target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cb69f308f5c727542ac602709b02c628
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
internal interface IShrinkBusScheduler : IDisposable
|
||||||
|
{
|
||||||
|
bool IsOnSchedulerThread { get; }
|
||||||
|
bool TryPost(Action action);
|
||||||
|
UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken);
|
||||||
|
UniTask ShutdownAsync(bool drain, TimeSpan timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkQueueFullException : InvalidOperationException
|
||||||
|
{
|
||||||
|
public ShrinkQueueFullException(string schedulerName)
|
||||||
|
: base($"Scheduler queue '{schedulerName}' is full.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ShrinkBusSchedulerFactory
|
||||||
|
{
|
||||||
|
public static IShrinkBusScheduler Create(ShrinkBusOptions options, string name)
|
||||||
|
{
|
||||||
|
return options.Scheduler switch
|
||||||
|
{
|
||||||
|
ShrinkBusSchedulerKind.MainThread => new ShrinkMainThreadScheduler(name, options),
|
||||||
|
ShrinkBusSchedulerKind.DedicatedThread => new ShrinkDedicatedThreadScheduler(name, options),
|
||||||
|
ShrinkBusSchedulerKind.TaskPool => new ShrinkTaskPoolScheduler(name, options),
|
||||||
|
_ => new ShrinkInlineScheduler()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkSchedulerWorkItem
|
||||||
|
{
|
||||||
|
private readonly UniTaskCompletionSource? _completion;
|
||||||
|
|
||||||
|
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
|
||||||
|
{
|
||||||
|
Action = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
|
if (awaitable)
|
||||||
|
_completion = new UniTaskCompletionSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<UniTask> Action { get; }
|
||||||
|
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
|
||||||
|
|
||||||
|
public void Complete() => _completion?.TrySetResult();
|
||||||
|
public void Fail(Exception exception) => _completion?.TrySetException(exception);
|
||||||
|
public void Cancel(CancellationToken cancellationToken) => _completion?.TrySetCanceled(cancellationToken);
|
||||||
|
public void Drop(string schedulerName) =>
|
||||||
|
_completion?.TrySetException(new ShrinkQueueFullException(schedulerName));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkSchedulerQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ConcurrentQueue<ShrinkSchedulerWorkItem> _queue = new();
|
||||||
|
private readonly SemaphoreSlim _slots;
|
||||||
|
private readonly ShrinkQueueOverflowPolicy _overflowPolicy;
|
||||||
|
private readonly string _name;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public ShrinkSchedulerQueue(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_overflowPolicy = options.OverflowPolicy;
|
||||||
|
_slots = new SemaphoreSlim(options.QueueCapacity, options.QueueCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty => _queue.IsEmpty;
|
||||||
|
|
||||||
|
public bool TryEnqueue(ShrinkSchedulerWorkItem item)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_slots.Wait(0))
|
||||||
|
{
|
||||||
|
if (_overflowPolicy != ShrinkQueueOverflowPolicy.DropOldest ||
|
||||||
|
!_queue.TryDequeue(out var dropped))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dropped.Drop(_name);
|
||||||
|
_slots.Release();
|
||||||
|
if (!_slots.Wait(0))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queue.Enqueue(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask EnqueueAsync(ShrinkSchedulerWorkItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
throw new ObjectDisposedException(_name);
|
||||||
|
|
||||||
|
if (_overflowPolicy == ShrinkQueueOverflowPolicy.Wait)
|
||||||
|
{
|
||||||
|
await _slots.WaitAsync(cancellationToken).AsUniTask(useCurrentSynchronizationContext: false);
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
{
|
||||||
|
_slots.Release();
|
||||||
|
throw new ObjectDisposedException(_name);
|
||||||
|
}
|
||||||
|
_queue.Enqueue(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryEnqueue(item))
|
||||||
|
throw new ShrinkQueueFullException(_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryDequeue(out ShrinkSchedulerWorkItem item)
|
||||||
|
{
|
||||||
|
if (!_queue.TryDequeue(out item!))
|
||||||
|
return false;
|
||||||
|
_slots.Release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DropPending()
|
||||||
|
{
|
||||||
|
while (_queue.TryDequeue(out var item))
|
||||||
|
{
|
||||||
|
_slots.Release();
|
||||||
|
item.Drop(_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
DropPending();
|
||||||
|
_slots.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ShrinkSchedulerWorkItemRunner
|
||||||
|
{
|
||||||
|
public static async UniTask RunAsync(ShrinkSchedulerWorkItem item)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await item.Action();
|
||||||
|
item.Complete();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
item.Cancel(ex.CancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
item.Fail(ex);
|
||||||
|
ShrinkEventDiagnostics.LogException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RunBlocking(ShrinkSchedulerWorkItem item)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
item.Action().GetAwaiter().GetResult();
|
||||||
|
item.Complete();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
item.Cancel(ex.CancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
item.Fail(ex);
|
||||||
|
ShrinkEventDiagnostics.LogException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkInlineScheduler : IShrinkBusScheduler
|
||||||
|
{
|
||||||
|
public bool IsOnSchedulerThread => true;
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask ShutdownAsync(bool drain, TimeSpan timeout) => UniTask.CompletedTask;
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkMainThreadScheduler : IShrinkBusScheduler
|
||||||
|
{
|
||||||
|
private readonly ShrinkSchedulerQueue _queue;
|
||||||
|
private int _pumpScheduled;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public ShrinkMainThreadScheduler(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_queue = new ShrinkSchedulerQueue(name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => PlayerLoopHelper.IsMainThread;
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return false;
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new ShrinkSchedulerWorkItem(() =>
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}, awaitable: false);
|
||||||
|
if (!_queue.TryEnqueue(item))
|
||||||
|
return false;
|
||||||
|
SchedulePump();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkMainThreadScheduler));
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||||
|
await _queue.EnqueueAsync(item, cancellationToken);
|
||||||
|
SchedulePump();
|
||||||
|
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _disposed, 1);
|
||||||
|
if (!drain)
|
||||||
|
_queue.DropPending();
|
||||||
|
else
|
||||||
|
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
|
||||||
|
_queue, () => Volatile.Read(ref _pumpScheduled) != 0, timeout);
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SchedulePump()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _pumpScheduled, 1) != 0)
|
||||||
|
return;
|
||||||
|
UniTask.Void(PumpAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTaskVoid PumpAsync()
|
||||||
|
{
|
||||||
|
await UniTask.SwitchToMainThread();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_queue.TryDequeue(out var item))
|
||||||
|
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _pumpScheduled, 0);
|
||||||
|
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
|
||||||
|
SchedulePump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkTaskPoolScheduler : IShrinkBusScheduler
|
||||||
|
{
|
||||||
|
private readonly ShrinkSchedulerQueue _queue;
|
||||||
|
private readonly int _maxConcurrency;
|
||||||
|
private int _workers;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public ShrinkTaskPoolScheduler(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_queue = new ShrinkSchedulerQueue(name, options);
|
||||||
|
_maxConcurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => false;
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return false;
|
||||||
|
var item = new ShrinkSchedulerWorkItem(() =>
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}, awaitable: false);
|
||||||
|
if (!_queue.TryEnqueue(item))
|
||||||
|
return false;
|
||||||
|
ScheduleWorkers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkTaskPoolScheduler));
|
||||||
|
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||||
|
await _queue.EnqueueAsync(item, cancellationToken);
|
||||||
|
ScheduleWorkers();
|
||||||
|
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _disposed, 1);
|
||||||
|
if (!drain)
|
||||||
|
_queue.DropPending();
|
||||||
|
else
|
||||||
|
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
|
||||||
|
_queue, () => Volatile.Read(ref _workers) != 0, timeout);
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScheduleWorkers()
|
||||||
|
{
|
||||||
|
while (!_queue.IsEmpty)
|
||||||
|
{
|
||||||
|
var current = Volatile.Read(ref _workers);
|
||||||
|
if (current >= _maxConcurrency ||
|
||||||
|
Interlocked.CompareExchange(ref _workers, current + 1, current) != current)
|
||||||
|
return;
|
||||||
|
UniTask.Void(WorkerAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTaskVoid WorkerAsync()
|
||||||
|
{
|
||||||
|
await UniTask.SwitchToThreadPool();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_queue.TryDequeue(out var item))
|
||||||
|
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _workers);
|
||||||
|
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
|
||||||
|
ScheduleWorkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkDedicatedThreadScheduler : IShrinkBusScheduler
|
||||||
|
{
|
||||||
|
private readonly ShrinkSchedulerQueue _queue;
|
||||||
|
private readonly AutoResetEvent _signal = new(false);
|
||||||
|
private readonly ManualResetEventSlim _stopped = new(false);
|
||||||
|
private readonly Thread _thread;
|
||||||
|
private int _disposed;
|
||||||
|
private int _threadId;
|
||||||
|
private int _shutdownTimedOut;
|
||||||
|
private int _waitHandlesDisposed;
|
||||||
|
|
||||||
|
public ShrinkDedicatedThreadScheduler(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_queue = new ShrinkSchedulerQueue(name, options);
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = $"ShrinkBus:{name}"
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return false;
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new ShrinkSchedulerWorkItem(() =>
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}, awaitable: false);
|
||||||
|
if (!_queue.TryEnqueue(item))
|
||||||
|
return false;
|
||||||
|
_signal.Set();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkDedicatedThreadScheduler));
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||||
|
await _queue.EnqueueAsync(item, cancellationToken);
|
||||||
|
_signal.Set();
|
||||||
|
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _disposed, 1);
|
||||||
|
if (!drain)
|
||||||
|
_queue.DropPending();
|
||||||
|
_signal.Set();
|
||||||
|
var waitMs = timeout == Timeout.InfiniteTimeSpan
|
||||||
|
? Timeout.Infinite
|
||||||
|
: Math.Max(0, (int)Math.Min(int.MaxValue, timeout.TotalMilliseconds));
|
||||||
|
var stopped = await UniTask.RunOnThreadPool(() => _stopped.Wait(waitMs));
|
||||||
|
if (!stopped)
|
||||||
|
{
|
||||||
|
_queue.DropPending();
|
||||||
|
Volatile.Write(ref _shutdownTimedOut, 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_stopped.IsSet)
|
||||||
|
DisposeWaitHandles();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DisposeWaitHandles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_queue.DropPending();
|
||||||
|
Volatile.Write(ref _shutdownTimedOut, 1);
|
||||||
|
_signal.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (Volatile.Read(ref _disposed) == 0 || !_queue.IsEmpty)
|
||||||
|
{
|
||||||
|
if (!_queue.TryDequeue(out var item))
|
||||||
|
{
|
||||||
|
_signal.WaitOne(50);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ShrinkSchedulerWorkItemRunner.RunBlocking(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_stopped.Set();
|
||||||
|
if (Volatile.Read(ref _shutdownTimedOut) != 0)
|
||||||
|
DisposeWaitHandles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeWaitHandles()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _waitHandlesDisposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_queue.Dispose();
|
||||||
|
_signal.Dispose();
|
||||||
|
_stopped.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ShrinkEventDiagnostics
|
||||||
|
{
|
||||||
|
public static void LogException(Exception exception)
|
||||||
|
{
|
||||||
|
#if UNITY_5_3_OR_NEWER
|
||||||
|
UnityEngine.Debug.LogException(exception);
|
||||||
|
#else
|
||||||
|
Trace.TraceError(exception.ToString());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ShrinkSchedulerShutdown
|
||||||
|
{
|
||||||
|
public static async UniTask WaitUntilDrainedAsync(ShrinkSchedulerQueue queue,
|
||||||
|
Func<bool> hasActiveWork, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
var started = Stopwatch.StartNew();
|
||||||
|
while (!queue.IsEmpty || hasActiveWork())
|
||||||
|
{
|
||||||
|
if (timeout != Timeout.InfiniteTimeSpan && started.Elapsed >= timeout)
|
||||||
|
{
|
||||||
|
queue.DropPending();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await UniTask.Delay(1, ignoreTimeScale: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8d5885f09c9a4de89cd95de44433bb17
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class ShrinkEventSubscriberAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string OwnerId { get; set; } = string.Empty;
|
||||||
|
public string DefaultBus { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class ShrinkSubscribeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Bus { get; set; } = string.Empty;
|
||||||
|
public ShrinkEventPriority Priority { get; set; } = ShrinkEventPriority.Normal;
|
||||||
|
public int NumericPriority { get; set; }
|
||||||
|
public bool ReceiveCanceled { get; set; }
|
||||||
|
|
||||||
|
public ShrinkSubscribeAttribute() { }
|
||||||
|
|
||||||
|
public ShrinkSubscribeAttribute(ShrinkEventPriority priority, bool receiveCanceled = false)
|
||||||
|
{
|
||||||
|
Priority = priority;
|
||||||
|
ReceiveCanceled = receiveCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkSubscribeAttribute(int priority)
|
||||||
|
{
|
||||||
|
NumericPriority = priority;
|
||||||
|
Priority = priority switch
|
||||||
|
{
|
||||||
|
>= 100 => ShrinkEventPriority.Highest,
|
||||||
|
>= 50 => ShrinkEventPriority.High,
|
||||||
|
>= 0 => ShrinkEventPriority.Normal,
|
||||||
|
>= -50 => ShrinkEventPriority.Low,
|
||||||
|
_ => ShrinkEventPriority.Lowest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
|
||||||
|
public sealed class ShrinkEventAttribute : Attribute
|
||||||
|
{
|
||||||
|
public ShrinkDispatchMode Dispatch { get; set; } = ShrinkDispatchMode.Ordered;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 32e8ae06929942288c57cd8c89b3d3a8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public interface IShrinkBusResolver
|
||||||
|
{
|
||||||
|
IShrinkEventBus GetBus(ShrinkBusKey key);
|
||||||
|
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkGeneratedSubscriber
|
||||||
|
{
|
||||||
|
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkEventBinding : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<IDisposable> _items = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Add(IDisposable subscription)
|
||||||
|
{
|
||||||
|
if (subscription == null)
|
||||||
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
subscription.Dispose();
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
|
||||||
|
}
|
||||||
|
_items.Add(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
for (var i = _items.Count - 1; i >= 0; i--)
|
||||||
|
_items[i].Dispose();
|
||||||
|
_items.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ShrinkStaticBindingRegistry
|
||||||
|
{
|
||||||
|
private sealed class Entry
|
||||||
|
{
|
||||||
|
public long Id;
|
||||||
|
public ShrinkBusKey Key;
|
||||||
|
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly List<Entry> Entries = new();
|
||||||
|
private static readonly Dictionary<long, IDisposable> Bindings = new();
|
||||||
|
private static long _nextId;
|
||||||
|
|
||||||
|
public static void Register(ShrinkBusKey key,
|
||||||
|
Func<IShrinkBusResolver, IDisposable> factory)
|
||||||
|
{
|
||||||
|
if (factory == null)
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var entry = new Entry
|
||||||
|
{
|
||||||
|
Id = Interlocked.Increment(ref _nextId),
|
||||||
|
Key = key,
|
||||||
|
Factory = factory
|
||||||
|
};
|
||||||
|
Entries.Add(entry);
|
||||||
|
TryAttachLocked(entry, EventBus.Resolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Register<TEvent>(string bus, Action<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||||
|
where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var key = ShrinkBusKey.Parse(bus);
|
||||||
|
Register(key, resolver => ShrinkGeneratedBinding.Subscribe(
|
||||||
|
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterAsync<TEvent>(string bus, ShrinkAsyncEventHandler<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||||
|
where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var key = ShrinkBusKey.Parse(bus);
|
||||||
|
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsync(
|
||||||
|
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterAsyncLegacy<TEvent>(string bus, Func<TEvent, UniTask> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||||
|
where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var key = ShrinkBusKey.Parse(bus);
|
||||||
|
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsyncLegacy(
|
||||||
|
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void AttachForBus(ShrinkBusKey key, IShrinkBusResolver resolver)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Entries.Count; i++)
|
||||||
|
{
|
||||||
|
if (Entries[i].Key == key)
|
||||||
|
TryAttachLocked(Entries[i], resolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void DetachBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Entries.Count; i++)
|
||||||
|
{
|
||||||
|
var entry = Entries[i];
|
||||||
|
if (entry.Key != key || !Bindings.TryGetValue(entry.Id, out var binding))
|
||||||
|
continue;
|
||||||
|
Bindings.Remove(entry.Id);
|
||||||
|
binding.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryAttachLocked(Entry entry, IShrinkBusResolver resolver)
|
||||||
|
{
|
||||||
|
if (Bindings.ContainsKey(entry.Id) || !resolver.TryGetBus(entry.Key, out _))
|
||||||
|
return;
|
||||||
|
Bindings.Add(entry.Id, entry.Factory(resolver));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public IEnumerable<IShrinkEventBus> Buses => _buses.Values;
|
||||||
|
|
||||||
|
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
|
||||||
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
var bus = new ShrinkEventBusBuilder()
|
||||||
|
.WithKey(key)
|
||||||
|
.WithOptions(options)
|
||||||
|
.Build();
|
||||||
|
if (!_buses.TryAdd(key, bus))
|
||||||
|
{
|
||||||
|
if (bus is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
throw new InvalidOperationException($"Bus '{key}' is already registered.");
|
||||||
|
}
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, Func<ShrinkBusOptions> optionsFactory)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
|
||||||
|
if (optionsFactory == null)
|
||||||
|
throw new ArgumentNullException(nameof(optionsFactory));
|
||||||
|
|
||||||
|
return _buses.GetOrAdd(key, busKey =>
|
||||||
|
new ShrinkEventBusBuilder()
|
||||||
|
.WithKey(busKey)
|
||||||
|
.WithOptions(optionsFactory())
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IShrinkEventBus GetBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
if (_buses.TryGetValue(key, out var bus))
|
||||||
|
return bus;
|
||||||
|
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) => _buses.TryGetValue(key, out bus!);
|
||||||
|
|
||||||
|
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
|
||||||
|
if (target is IShrinkGeneratedSubscriber generated)
|
||||||
|
return generated.AttachGenerated(this, defaultBus);
|
||||||
|
|
||||||
|
var bus = GetBus(defaultBus ?? ShrinkBusKey.Game);
|
||||||
|
return bus.Attach(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
if (!_buses.TryRemove(key, out var bus))
|
||||||
|
return false;
|
||||||
|
if (bus is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
foreach (var bus in _buses.Values)
|
||||||
|
{
|
||||||
|
if (bus is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
_buses.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask ShutdownAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
foreach (var bus in _buses.Values)
|
||||||
|
{
|
||||||
|
if (bus is ShrinkEventBusInstance instance)
|
||||||
|
await instance.ShutdownAsync();
|
||||||
|
else
|
||||||
|
bus.Dispose();
|
||||||
|
}
|
||||||
|
_buses.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ShrinkGeneratedBinding
|
||||||
|
{
|
||||||
|
public static IShrinkBusResolver RuntimeResolver => EventBus.Resolver;
|
||||||
|
|
||||||
|
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
|
||||||
|
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var bus = ResolveBus(resolver, defaultBus, configuredBus);
|
||||||
|
if (bus is not ShrinkEventBusInstance instance)
|
||||||
|
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
|
||||||
|
return instance.SubscribeGenerated(handler,
|
||||||
|
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var bus = ResolveBus(resolver, defaultBus, configuredBus);
|
||||||
|
if (bus is not ShrinkEventBusInstance instance)
|
||||||
|
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
|
||||||
|
return instance.SubscribeGenerated(handler,
|
||||||
|
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDisposable SubscribeAsyncLegacy<TEvent>(IShrinkBusResolver resolver,
|
||||||
|
ShrinkBusKey? defaultBus, string? configuredBus, object? owner,
|
||||||
|
Func<TEvent, UniTask> handler, ShrinkEventPriority priority,
|
||||||
|
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler));
|
||||||
|
return SubscribeAsync<TEvent>(resolver, defaultBus, configuredBus, owner,
|
||||||
|
(eventData, _) => handler(eventData), priority, numericPriority, receiveCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IShrinkEventBus ResolveBus(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus)
|
||||||
|
{
|
||||||
|
if (resolver == null)
|
||||||
|
throw new ArgumentNullException(nameof(resolver));
|
||||||
|
var key = !string.IsNullOrWhiteSpace(configuredBus)
|
||||||
|
? ShrinkBusKey.Parse(configuredBus)
|
||||||
|
: defaultBus ?? ShrinkBusKey.Game;
|
||||||
|
return resolver.GetBus(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f19286886fb64b3ba7bfbd010c540913
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "ShrinkEventBus.Runtime",
|
||||||
|
"rootNamespace": "ShrinkEventBus",
|
||||||
|
"references": [
|
||||||
|
"UniTask"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f9a7e109b2ecd4946ba9202d2288af15
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public sealed class ShrinkEventBusBuilder
|
||||||
|
{
|
||||||
|
internal ShrinkBusKey Key { get; private set; } = ShrinkBusKey.Game;
|
||||||
|
internal ShrinkBusOptions Options { get; private set; } = ShrinkBusOptions.Inline();
|
||||||
|
internal Action<IShrinkEvent, Type, ShrinkBusKey>? PostObserver { get; private set; }
|
||||||
|
|
||||||
|
public ShrinkEventBusBuilder WithKey(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkEventBusBuilder WithOptions(ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IShrinkEventBus Build() => Options.Scheduler == ShrinkBusSchedulerKind.Inline
|
||||||
|
? new ShrinkInlineEventBusInstance(this)
|
||||||
|
: new ShrinkEventBusInstance(this);
|
||||||
|
|
||||||
|
internal ShrinkEventBusBuilder WithPostObserver(
|
||||||
|
Action<IShrinkEvent, Type, ShrinkBusKey> observer)
|
||||||
|
{
|
||||||
|
PostObserver = observer ?? throw new ArgumentNullException(nameof(observer));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aad8b06ecb53b8a4998fd9944673e537
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public delegate UniTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
|
||||||
|
|
||||||
|
internal interface IShrinkEventChannel
|
||||||
|
{
|
||||||
|
int Count { get; }
|
||||||
|
bool RemoveSubscription(long subscriptionId);
|
||||||
|
void Clear();
|
||||||
|
void DetachSlot(int slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ShrinkEventChannelSlots<TEvent> where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static ShrinkEventChannel<TEvent>?[] _slots =
|
||||||
|
Array.Empty<ShrinkEventChannel<TEvent>?>();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ShrinkEventChannel<TEvent>? Get(int slot)
|
||||||
|
{
|
||||||
|
var snapshot = _slots;
|
||||||
|
return (uint)slot < (uint)snapshot.Length ? snapshot[slot] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Set(int slot, ShrinkEventChannel<TEvent> channel)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var current = _slots;
|
||||||
|
var length = current.Length;
|
||||||
|
if (length <= slot)
|
||||||
|
{
|
||||||
|
length = Math.Max(4, length);
|
||||||
|
while (length <= slot)
|
||||||
|
length *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = new ShrinkEventChannel<TEvent>?[length];
|
||||||
|
Array.Copy(current, next, current.Length);
|
||||||
|
next[slot] = channel;
|
||||||
|
Volatile.Write(ref _slots, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Clear(int slot, ShrinkEventChannel<TEvent> channel)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var current = _slots;
|
||||||
|
if ((uint)slot >= (uint)current.Length || !ReferenceEquals(current[slot], channel))
|
||||||
|
return;
|
||||||
|
var next = (ShrinkEventChannel<TEvent>?[])current.Clone();
|
||||||
|
next[slot] = null;
|
||||||
|
Volatile.Write(ref _slots, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkEventChannel<TEvent> : IShrinkEventChannel
|
||||||
|
where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
private static readonly bool SupportsCancellation =
|
||||||
|
typeof(IShrinkCancelableEvent).IsAssignableFrom(typeof(TEvent));
|
||||||
|
|
||||||
|
private sealed class HandlerEntry
|
||||||
|
{
|
||||||
|
public long SubscriptionId;
|
||||||
|
public long RegistrationOrder;
|
||||||
|
public Action<TEvent>? SyncHandler;
|
||||||
|
public ShrinkAsyncEventHandler<TEvent>? AsyncHandler;
|
||||||
|
public ShrinkSubscribeDescriptor Descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly List<HandlerEntry> _entries = new();
|
||||||
|
private HandlerEntry[] _snapshot = Array.Empty<HandlerEntry>();
|
||||||
|
private Action<TEvent>? _syncDispatcher;
|
||||||
|
|
||||||
|
public int Count => _snapshot.Length;
|
||||||
|
|
||||||
|
public void Add(long subscriptionId, long registrationOrder, Action<TEvent> handler,
|
||||||
|
ShrinkSubscribeDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Add(new HandlerEntry
|
||||||
|
{
|
||||||
|
SubscriptionId = subscriptionId,
|
||||||
|
RegistrationOrder = registrationOrder,
|
||||||
|
SyncHandler = handler,
|
||||||
|
Descriptor = descriptor
|
||||||
|
});
|
||||||
|
RebuildSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(long subscriptionId, long registrationOrder,
|
||||||
|
ShrinkAsyncEventHandler<TEvent> handler, ShrinkSubscribeDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Add(new HandlerEntry
|
||||||
|
{
|
||||||
|
SubscriptionId = subscriptionId,
|
||||||
|
RegistrationOrder = registrationOrder,
|
||||||
|
AsyncHandler = handler,
|
||||||
|
Descriptor = descriptor
|
||||||
|
});
|
||||||
|
RebuildSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ShrinkPostResult Post(in TEvent eventData)
|
||||||
|
{
|
||||||
|
var syncDispatcher = _syncDispatcher;
|
||||||
|
if (syncDispatcher != null)
|
||||||
|
{
|
||||||
|
syncDispatcher(eventData);
|
||||||
|
return new ShrinkPostResult(true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = _snapshot;
|
||||||
|
var handled = false;
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (entry.SyncHandler != null)
|
||||||
|
entry.SyncHandler(eventData);
|
||||||
|
else if (entry.AsyncHandler != null)
|
||||||
|
entry.AsyncHandler(eventData, CancellationToken.None).Forget(ShrinkEventDiagnostics.LogException);
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode dispatchMode,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshot = _snapshot;
|
||||||
|
var handled = false;
|
||||||
|
if (dispatchMode == ShrinkDispatchMode.Parallel)
|
||||||
|
{
|
||||||
|
var tasks = new List<UniTask>(snapshot.Length);
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (entry.SyncHandler != null)
|
||||||
|
entry.SyncHandler(eventData);
|
||||||
|
else if (entry.AsyncHandler != null)
|
||||||
|
tasks.Add(entry.AsyncHandler(eventData, cancellationToken));
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
await UniTask.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (entry.SyncHandler != null)
|
||||||
|
entry.SyncHandler(eventData);
|
||||||
|
else if (entry.AsyncHandler != null)
|
||||||
|
await entry.AsyncHandler(eventData, cancellationToken);
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveSubscription(long subscriptionId)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var removed = _entries.RemoveAll(entry => entry.SubscriptionId == subscriptionId) > 0;
|
||||||
|
RebuildSnapshot();
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Clear();
|
||||||
|
_snapshot = Array.Empty<HandlerEntry>();
|
||||||
|
_syncDispatcher = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DetachSlot(int slot) => ShrinkEventChannelSlots<TEvent>.Clear(slot, this);
|
||||||
|
|
||||||
|
private void RebuildSnapshot()
|
||||||
|
{
|
||||||
|
_entries.Sort(static (left, right) =>
|
||||||
|
{
|
||||||
|
var leftOrder = left.Descriptor.NumericPriority == 0
|
||||||
|
? (int)left.Descriptor.Priority * 1000
|
||||||
|
: -left.Descriptor.NumericPriority;
|
||||||
|
var rightOrder = right.Descriptor.NumericPriority == 0
|
||||||
|
? (int)right.Descriptor.Priority * 1000
|
||||||
|
: -right.Descriptor.NumericPriority;
|
||||||
|
var priority = leftOrder.CompareTo(rightOrder);
|
||||||
|
return priority != 0
|
||||||
|
? priority
|
||||||
|
: left.RegistrationOrder.CompareTo(right.RegistrationOrder);
|
||||||
|
});
|
||||||
|
_snapshot = _entries.ToArray();
|
||||||
|
|
||||||
|
Action<TEvent>? dispatcher = null;
|
||||||
|
if (!SupportsCancellation)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
var handler = _snapshot[i].SyncHandler;
|
||||||
|
if (handler == null)
|
||||||
|
{
|
||||||
|
dispatcher = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dispatcher += handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _syncDispatcher, dispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsCanceled(TEvent eventData)
|
||||||
|
{
|
||||||
|
if (!SupportsCancellation)
|
||||||
|
return false;
|
||||||
|
return ((IShrinkCancelableEvent)(object)eventData!).IsCanceled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
internal class ShrinkEventBusInstance : IShrinkEventBus
|
||||||
|
{
|
||||||
|
private sealed class SingleBusResolver : IShrinkBusResolver
|
||||||
|
{
|
||||||
|
private readonly IShrinkEventBus _bus;
|
||||||
|
|
||||||
|
public SingleBusResolver(IShrinkEventBus bus) => _bus = bus;
|
||||||
|
|
||||||
|
public IShrinkEventBus GetBus(ShrinkBusKey key) => key == _bus.Key
|
||||||
|
? _bus
|
||||||
|
: throw new KeyNotFoundException($"Bus '{key}' is not available from '{_bus.Key}'.");
|
||||||
|
|
||||||
|
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus)
|
||||||
|
{
|
||||||
|
bus = _bus;
|
||||||
|
return key == _bus.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EventSubscription : IDisposable
|
||||||
|
{
|
||||||
|
private ShrinkEventBusInstance? _owner;
|
||||||
|
private readonly long _subscriptionId;
|
||||||
|
|
||||||
|
public EventSubscription(ShrinkEventBusInstance owner, long subscriptionId)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_subscriptionId = subscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.UnregisterSubscription(_subscriptionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int _nextBusSlot;
|
||||||
|
private readonly Dictionary<Type, IShrinkEventChannel> _channels = new();
|
||||||
|
private readonly object _channelGate = new();
|
||||||
|
private readonly IShrinkBusScheduler _scheduler;
|
||||||
|
private readonly Action<IShrinkEvent, Type, ShrinkBusKey>? _postObserver;
|
||||||
|
private readonly int _busSlot;
|
||||||
|
private readonly bool _isInline;
|
||||||
|
private long _nextSubscriptionId;
|
||||||
|
private int _subscriberCount;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public ShrinkEventBusInstance(ShrinkEventBusBuilder builder)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
|
||||||
|
Key = builder.Key;
|
||||||
|
Options = builder.Options.CloneValidated();
|
||||||
|
_busSlot = Interlocked.Increment(ref _nextBusSlot);
|
||||||
|
_scheduler = ShrinkBusSchedulerFactory.Create(Options, Key.Id);
|
||||||
|
_postObserver = builder.PostObserver;
|
||||||
|
_isInline = Options.Scheduler == ShrinkBusSchedulerKind.Inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkBusKey Key { get; }
|
||||||
|
public ShrinkBusOptions Options { get; }
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public virtual ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (eventData is null)
|
||||||
|
throw new ArgumentNullException(nameof(eventData));
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
|
||||||
|
|
||||||
|
if (_isInline || _scheduler.IsOnSchedulerThread)
|
||||||
|
return Dispatch(in eventData);
|
||||||
|
|
||||||
|
var queuedEvent = eventData;
|
||||||
|
if (_scheduler.TryPost(() => Dispatch(in queuedEvent)))
|
||||||
|
return new ShrinkPostResult(true, false, false);
|
||||||
|
return TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (eventData is null)
|
||||||
|
throw new ArgumentNullException(nameof(eventData));
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
|
||||||
|
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
|
||||||
|
|
||||||
|
if (_isInline || _scheduler.IsOnSchedulerThread)
|
||||||
|
return DispatchAsync(eventData, cancellationToken);
|
||||||
|
|
||||||
|
return PostScheduledAsync(eventData, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask<ShrinkPostResult> PostScheduledAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var result = default(ShrinkPostResult);
|
||||||
|
var dispatchStarted = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _scheduler.PostAsync(async () =>
|
||||||
|
{
|
||||||
|
dispatchStarted = true;
|
||||||
|
result = await DispatchAsync(eventData, cancellationToken);
|
||||||
|
}, cancellationToken);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (ShrinkQueueFullException)
|
||||||
|
{
|
||||||
|
return TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), true);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var canceled = new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled);
|
||||||
|
return dispatchStarted ? canceled : TraceWithoutDispatch(in eventData, canceled, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Attach(object target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
if (target is not IShrinkGeneratedSubscriber generated)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
|
||||||
|
return generated.AttachGenerated(new SingleBusResolver(this), Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IDisposable SubscribeGenerated<TEvent>(Action<TEvent> handler,
|
||||||
|
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
|
||||||
|
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
|
||||||
|
Interlocked.Increment(ref _subscriberCount);
|
||||||
|
return new EventSubscription(this, subscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IDisposable SubscribeGenerated<TEvent>(ShrinkAsyncEventHandler<TEvent> handler,
|
||||||
|
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
|
||||||
|
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
|
||||||
|
Interlocked.Increment(ref _subscriberCount);
|
||||||
|
return new EventSubscription(this, subscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int SubscriberCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Math.Max(0, Volatile.Read(ref _subscriberCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async UniTask ShutdownAsync()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
await _scheduler.ShutdownAsync(Options.DrainOnShutdown, Options.ShutdownTimeout);
|
||||||
|
ClearChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_scheduler.Dispose();
|
||||||
|
ClearChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected ShrinkPostResult Dispatch<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
|
||||||
|
return DispatchObserved(in eventData);
|
||||||
|
|
||||||
|
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
var result = channel == null
|
||||||
|
? new ShrinkPostResult(true, false, false)
|
||||||
|
: channel.Post(in eventData);
|
||||||
|
if (_postObserver != null && EventBus.HasPostedObservers)
|
||||||
|
_postObserver(eventData, typeof(TEvent), Key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async UniTask<ShrinkPostResult> DispatchAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
|
||||||
|
return await DispatchAsyncObserved(eventData, cancellationToken);
|
||||||
|
|
||||||
|
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
|
||||||
|
var result = channel == null
|
||||||
|
? ShrinkPostResult.Completed(false)
|
||||||
|
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
|
||||||
|
if (_postObserver != null && EventBus.HasPostedObservers)
|
||||||
|
_postObserver(eventData, typeof(TEvent), Key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShrinkPostResult DispatchObserved<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var startedAt = Stopwatch.GetTimestamp();
|
||||||
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
ShrinkPostResult result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = channel == null
|
||||||
|
? new ShrinkPostResult(true, false, false)
|
||||||
|
: channel.Post(in eventData);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var failedResult = new ShrinkPostResult(true, channel != null,
|
||||||
|
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
|
||||||
|
ShrinkPostFailure.HandlerException);
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, false));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
|
||||||
|
if (EventBus.HasPostedObservers)
|
||||||
|
_postObserver!(eventData, typeof(TEvent), Key);
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, threadId, elapsedTicks, result, false));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected ShrinkPostResult TraceWithoutDispatch<TEvent>(in TEvent eventData,
|
||||||
|
ShrinkPostResult result, bool isAsync) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
|
||||||
|
{
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, Thread.CurrentThread.ManagedThreadId, 0L, result, isAsync));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask<ShrinkPostResult> DispatchAsyncObserved<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var startedAt = Stopwatch.GetTimestamp();
|
||||||
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
ShrinkPostResult result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = channel == null
|
||||||
|
? ShrinkPostResult.Completed(false)
|
||||||
|
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var canceledResult = new ShrinkPostResult(true, channel != null, false,
|
||||||
|
ShrinkPostFailure.Canceled);
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, threadId, Stopwatch.GetTimestamp() - startedAt, canceledResult, true));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var failedResult = new ShrinkPostResult(true, channel != null,
|
||||||
|
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
|
||||||
|
ShrinkPostFailure.HandlerException);
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, true));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
|
||||||
|
if (EventBus.HasPostedObservers)
|
||||||
|
_postObserver!(eventData, typeof(TEvent), Key);
|
||||||
|
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
|
||||||
|
Options, threadId, elapsedTicks, result, true));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShrinkEventChannel<TEvent> GetOrCreateChannel<TEvent>() where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
var existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
if (existing != null)
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
lock (_channelGate)
|
||||||
|
{
|
||||||
|
existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
|
||||||
|
if (existing != null)
|
||||||
|
return existing;
|
||||||
|
var channel = new ShrinkEventChannel<TEvent>();
|
||||||
|
_channels.Add(typeof(TEvent), channel);
|
||||||
|
ShrinkEventChannelSlots<TEvent>.Set(_busSlot, channel);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterSubscription(long subscriptionId)
|
||||||
|
{
|
||||||
|
lock (_channelGate)
|
||||||
|
{
|
||||||
|
foreach (var channel in _channels.Values)
|
||||||
|
{
|
||||||
|
if (channel.RemoveSubscription(subscriptionId))
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _subscriberCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearChannels()
|
||||||
|
{
|
||||||
|
lock (_channelGate)
|
||||||
|
{
|
||||||
|
foreach (var channel in _channels.Values)
|
||||||
|
{
|
||||||
|
channel.DetachSlot(_busSlot);
|
||||||
|
channel.Clear();
|
||||||
|
}
|
||||||
|
_channels.Clear();
|
||||||
|
Volatile.Write(ref _subscriberCount, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsDisposed
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => Volatile.Read(ref _disposed) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ShrinkInlineEventBusInstance : ShrinkEventBusInstance
|
||||||
|
{
|
||||||
|
public ShrinkInlineEventBusInstance(ShrinkEventBusBuilder builder) : base(builder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override ShrinkPostResult Post<TEvent>(in TEvent eventData)
|
||||||
|
{
|
||||||
|
if (eventData is null)
|
||||||
|
throw new ArgumentNullException(nameof(eventData));
|
||||||
|
if (IsDisposed)
|
||||||
|
return TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
|
||||||
|
return Dispatch(in eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (eventData is null)
|
||||||
|
throw new ArgumentNullException(nameof(eventData));
|
||||||
|
if (IsDisposed)
|
||||||
|
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
|
||||||
|
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
|
||||||
|
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
|
||||||
|
return DispatchAsync(eventData, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 92c1c7cd96e37c8458600728c7c0c57f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
/// <summary>Marker shared by managed, Unity and ECS event payloads.</summary>
|
||||||
|
public interface IShrinkEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkCancelableEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
bool IsCanceled { get; }
|
||||||
|
void SetCanceled(bool value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkResultEvent<TResult> : IShrinkEvent
|
||||||
|
{
|
||||||
|
TResult Result { get; }
|
||||||
|
void SetResult(TResult result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShrinkEventPriority
|
||||||
|
{
|
||||||
|
Highest = 0,
|
||||||
|
High = 1,
|
||||||
|
Normal = 2,
|
||||||
|
Low = 3,
|
||||||
|
Lowest = 4,
|
||||||
|
Monitor = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShrinkDispatchMode
|
||||||
|
{
|
||||||
|
Ordered = 0,
|
||||||
|
Parallel = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShrinkBusSchedulerKind
|
||||||
|
{
|
||||||
|
Inline = 0,
|
||||||
|
MainThread = 1,
|
||||||
|
DedicatedThread = 2,
|
||||||
|
TaskPool = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShrinkQueueOverflowPolicy
|
||||||
|
{
|
||||||
|
Reject = 0,
|
||||||
|
DropNewest = 1,
|
||||||
|
DropOldest = 2,
|
||||||
|
Wait = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShrinkPostFailure
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
BusStopped = 1,
|
||||||
|
QueueFull = 2,
|
||||||
|
Canceled = 3,
|
||||||
|
HandlerException = 4,
|
||||||
|
InvalidEvent = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ShrinkPostResult
|
||||||
|
{
|
||||||
|
private const int AcceptedMask = 1 << 0;
|
||||||
|
private const int HandledMask = 1 << 1;
|
||||||
|
private const int CanceledMask = 1 << 2;
|
||||||
|
private const int FailureShift = 8;
|
||||||
|
private readonly int _value;
|
||||||
|
|
||||||
|
public ShrinkPostResult(bool accepted, bool handled, bool canceled,
|
||||||
|
ShrinkPostFailure failure = ShrinkPostFailure.None)
|
||||||
|
{
|
||||||
|
_value = (accepted ? AcceptedMask : 0) |
|
||||||
|
(handled ? HandledMask : 0) |
|
||||||
|
(canceled ? CanceledMask : 0) |
|
||||||
|
((int)failure << FailureShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Accepted => (_value & AcceptedMask) != 0;
|
||||||
|
public bool Handled => (_value & HandledMask) != 0;
|
||||||
|
public bool Canceled => (_value & CanceledMask) != 0;
|
||||||
|
public ShrinkPostFailure Failure => (ShrinkPostFailure)((uint)_value >> FailureShift);
|
||||||
|
public bool Succeeded => Accepted && Failure == ShrinkPostFailure.None;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ShrinkPostResult Rejected(ShrinkPostFailure failure) =>
|
||||||
|
new(false, false, false, failure);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ShrinkPostResult Completed(bool handled, bool canceled = false) =>
|
||||||
|
new(true, handled, canceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ShrinkBusKey : IEquatable<ShrinkBusKey>
|
||||||
|
{
|
||||||
|
public ShrinkBusKey(string scope, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(scope))
|
||||||
|
throw new ArgumentException("Bus scope must not be empty.", nameof(scope));
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
throw new ArgumentException("Bus name must not be empty.", nameof(name));
|
||||||
|
|
||||||
|
Scope = scope.Trim().ToLowerInvariant();
|
||||||
|
Name = name.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Scope { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string Id => Scope == "game" && Name == "default" ? "game" : $"{Scope}:{Name}";
|
||||||
|
|
||||||
|
public static ShrinkBusKey Game => new("game", "default");
|
||||||
|
public static ShrinkBusKey Server => new("server", "default");
|
||||||
|
public static ShrinkBusKey Scene(string sceneId) => new("scene", sceneId);
|
||||||
|
public static ShrinkBusKey Mod(string modId) => new("mod", modId);
|
||||||
|
public static ShrinkBusKey World(string worldId) => new("world", worldId);
|
||||||
|
|
||||||
|
public static ShrinkBusKey Parse(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "game", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return Game;
|
||||||
|
|
||||||
|
var normalized = value.Trim();
|
||||||
|
var separator = normalized.IndexOf(':');
|
||||||
|
return separator <= 0 || separator == normalized.Length - 1
|
||||||
|
? new ShrinkBusKey("custom", normalized)
|
||||||
|
: new ShrinkBusKey(normalized.Substring(0, separator), normalized.Substring(separator + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(ShrinkBusKey other) =>
|
||||||
|
string.Equals(Scope, other.Scope, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(Name, other.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ShrinkBusKey other && Equals(other);
|
||||||
|
public override int GetHashCode() => HashCode.Combine(Scope.ToLowerInvariant(), Name);
|
||||||
|
public override string ToString() => Id;
|
||||||
|
public static bool operator ==(ShrinkBusKey left, ShrinkBusKey right) => left.Equals(right);
|
||||||
|
public static bool operator !=(ShrinkBusKey left, ShrinkBusKey right) => !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkBusOptions
|
||||||
|
{
|
||||||
|
public ShrinkBusSchedulerKind Scheduler { get; set; } = ShrinkBusSchedulerKind.Inline;
|
||||||
|
public ShrinkDispatchMode DispatchMode { get; set; } = ShrinkDispatchMode.Ordered;
|
||||||
|
public int QueueCapacity { get; set; } = 4096;
|
||||||
|
public int MaxConcurrency { get; set; } = Math.Max(1, Environment.ProcessorCount);
|
||||||
|
public ShrinkQueueOverflowPolicy OverflowPolicy { get; set; } = ShrinkQueueOverflowPolicy.Reject;
|
||||||
|
public bool DrainOnShutdown { get; set; } = true;
|
||||||
|
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
public static ShrinkBusOptions Inline(ShrinkDispatchMode mode = ShrinkDispatchMode.Ordered) =>
|
||||||
|
new() { Scheduler = ShrinkBusSchedulerKind.Inline, DispatchMode = mode };
|
||||||
|
|
||||||
|
public static ShrinkBusOptions MainThread(int queueCapacity = 4096) =>
|
||||||
|
new() { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = queueCapacity };
|
||||||
|
|
||||||
|
public static ShrinkBusOptions DedicatedThread(int queueCapacity = 4096) =>
|
||||||
|
new() { Scheduler = ShrinkBusSchedulerKind.DedicatedThread, QueueCapacity = queueCapacity };
|
||||||
|
|
||||||
|
public static ShrinkBusOptions TaskPool(int maxConcurrency = 0, int queueCapacity = 4096) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Scheduler = ShrinkBusSchedulerKind.TaskPool,
|
||||||
|
DispatchMode = ShrinkDispatchMode.Parallel,
|
||||||
|
QueueCapacity = queueCapacity,
|
||||||
|
MaxConcurrency = maxConcurrency > 0 ? maxConcurrency : Math.Max(1, Environment.ProcessorCount)
|
||||||
|
};
|
||||||
|
|
||||||
|
internal ShrinkBusOptions CloneValidated()
|
||||||
|
{
|
||||||
|
if (QueueCapacity <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(QueueCapacity));
|
||||||
|
if (MaxConcurrency <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(MaxConcurrency));
|
||||||
|
if (ShutdownTimeout < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(ShutdownTimeout));
|
||||||
|
if (OverflowPolicy == ShrinkQueueOverflowPolicy.Wait && Scheduler == ShrinkBusSchedulerKind.Inline)
|
||||||
|
throw new InvalidOperationException("Inline buses do not have a queue and cannot use Wait overflow policy.");
|
||||||
|
|
||||||
|
return new ShrinkBusOptions
|
||||||
|
{
|
||||||
|
Scheduler = Scheduler,
|
||||||
|
DispatchMode = DispatchMode,
|
||||||
|
QueueCapacity = QueueCapacity,
|
||||||
|
MaxConcurrency = MaxConcurrency,
|
||||||
|
OverflowPolicy = OverflowPolicy,
|
||||||
|
DrainOnShutdown = DrainOnShutdown,
|
||||||
|
ShutdownTimeout = ShutdownTimeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ShrinkSubscribeDescriptor
|
||||||
|
{
|
||||||
|
public ShrinkSubscribeDescriptor(string? bus, ShrinkEventPriority priority,
|
||||||
|
int numericPriority, bool receiveCanceled)
|
||||||
|
{
|
||||||
|
Bus = bus ?? string.Empty;
|
||||||
|
Priority = priority;
|
||||||
|
NumericPriority = numericPriority;
|
||||||
|
ReceiveCanceled = receiveCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Bus { get; }
|
||||||
|
public ShrinkEventPriority Priority { get; }
|
||||||
|
public int NumericPriority { get; }
|
||||||
|
public bool ReceiveCanceled { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 04b409c00e3144fd8d9f3efb7c2901b7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optional Unity lifecycle host for generated event bindings. It does not require
|
||||||
|
/// the target component to inherit from an SDK base class.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class ShrinkMonoEventScope : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private string bus = "game";
|
||||||
|
[SerializeField] private bool includeChildren;
|
||||||
|
private IDisposable? _binding;
|
||||||
|
private ShrinkEventBinding? _bindingGroup;
|
||||||
|
|
||||||
|
public string BusId
|
||||||
|
{
|
||||||
|
get => bus;
|
||||||
|
set => bus = value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
RefreshBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
ReleaseBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshBindings()
|
||||||
|
{
|
||||||
|
ReleaseBindings();
|
||||||
|
var key = ShrinkBusKey.Parse(bus);
|
||||||
|
if (!EventBus.TryGetBus(key, out var targetBus))
|
||||||
|
targetBus = EventBus.GetOrCreateBus(key, ShrinkBusOptions.MainThread());
|
||||||
|
|
||||||
|
var components = includeChildren
|
||||||
|
? GetComponentsInChildren<MonoBehaviour>(true)
|
||||||
|
: GetComponents<MonoBehaviour>();
|
||||||
|
for (var i = 0; i < components.Length; i++)
|
||||||
|
TryAttach(components[i], targetBus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseBindings()
|
||||||
|
{
|
||||||
|
_binding?.Dispose();
|
||||||
|
_binding = null;
|
||||||
|
_bindingGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryAttach(MonoBehaviour target, IShrinkEventBus targetBus)
|
||||||
|
{
|
||||||
|
if (target == null || ReferenceEquals(target, this))
|
||||||
|
return;
|
||||||
|
if (target is not IShrinkGeneratedSubscriber)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var binding = targetBus.Attach(target);
|
||||||
|
if (_bindingGroup == null)
|
||||||
|
_bindingGroup = new ShrinkEventBinding();
|
||||||
|
_bindingGroup.Add(binding);
|
||||||
|
_binding = _bindingGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5fe48da79b2f4a10a338e497f4ce5ae1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a41c52d2d40c42479d1f1dfd2d35ae3c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "ShrinkEventBus.PlayMode.Tests",
|
||||||
|
"rootNamespace": "ShrinkEventBus.PlayMode.Tests",
|
||||||
|
"references": [
|
||||||
|
"ShrinkEventBus.Runtime",
|
||||||
|
"UniTask"
|
||||||
|
],
|
||||||
|
"optionalUnityReferences": ["TestAssemblies"],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"autoReferenced": false,
|
||||||
|
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b03fa714413c4af5ad4140e7a409ef2f
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.PlayMode.Tests
|
||||||
|
{
|
||||||
|
internal readonly struct MonoLifecycleEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public MonoLifecycleEvent(int value) => Value = value;
|
||||||
|
public int Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
internal sealed class MonoLifecycleTarget : MonoBehaviour
|
||||||
|
{
|
||||||
|
public int Sum { get; private set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private void OnEvent(MonoLifecycleEvent value) => Sum += value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkMonoEventScopePlayModeTests
|
||||||
|
{
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator OnEnableAttachesAndOnDisableReleases()
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject("ShrinkMonoEventScope-PlayMode");
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
var target = gameObject.AddComponent<MonoLifecycleTarget>();
|
||||||
|
gameObject.AddComponent<ShrinkMonoEventScope>();
|
||||||
|
|
||||||
|
gameObject.SetActive(true);
|
||||||
|
yield return null;
|
||||||
|
yield return EventBus.PostAsync(new MonoLifecycleEvent(3)).ToCoroutine();
|
||||||
|
Assert.AreEqual(3, target.Sum);
|
||||||
|
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
yield return null;
|
||||||
|
yield return EventBus.PostAsync(new MonoLifecycleEvent(5)).ToCoroutine();
|
||||||
|
Assert.AreEqual(3, target.Sum);
|
||||||
|
Object.Destroy(gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f42b5f6f2249483b9978f0fa729d479b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6f28409a6ba08444cbdbfc5eb37b1809
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
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:
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.Tests
|
||||||
|
{
|
||||||
|
internal readonly struct GeneratedPingEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public GeneratedPingEvent(int value) => Value = value;
|
||||||
|
public int Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
internal sealed class CodeGeneratedTarget
|
||||||
|
{
|
||||||
|
public int Sum { get; private set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
internal static class StaticCodeGeneratedTarget
|
||||||
|
{
|
||||||
|
public static int Sum { get; set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "mod:late-static")]
|
||||||
|
internal static class DelayedStaticCodeGeneratedTarget
|
||||||
|
{
|
||||||
|
public static int Sum { get; set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
internal sealed class MonoCodeGeneratedTarget : MonoBehaviour
|
||||||
|
{
|
||||||
|
public int Sum { get; private set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkEventBusV2Tests
|
||||||
|
{
|
||||||
|
private readonly struct PingEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public PingEvent(int value) => Value = value;
|
||||||
|
public int Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct DiagnosticEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CancelEvent : IShrinkCancelableEvent
|
||||||
|
{
|
||||||
|
public bool IsCanceled { get; private set; }
|
||||||
|
public void SetCanceled(bool value) => IsCanceled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ResultEvent : IShrinkResultEvent<int>
|
||||||
|
{
|
||||||
|
public int Result { get; private set; }
|
||||||
|
public void SetResult(int result) => Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GeneratedTarget : IShrinkGeneratedSubscriber
|
||||||
|
{
|
||||||
|
public int Sum;
|
||||||
|
|
||||||
|
public IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null)
|
||||||
|
{
|
||||||
|
var binding = new ShrinkEventBinding();
|
||||||
|
binding.Add(ShrinkGeneratedBinding.Subscribe<PingEvent>(resolver, defaultBus, string.Empty,
|
||||||
|
this, OnPing, ShrinkEventPriority.Normal, 0, false));
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPing(PingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void MultipleBusesAreIsolated()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var mod = host.CreateBus(ShrinkBusKey.Mod("sample"), ShrinkBusOptions.Inline());
|
||||||
|
var gameHits = 0;
|
||||||
|
var modHits = 0;
|
||||||
|
using var gameBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, evt => gameHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
using var modBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Mod("sample"),
|
||||||
|
string.Empty, this, evt => modHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
|
||||||
|
game.Post(new PingEvent(2));
|
||||||
|
mod.Post(new PingEvent(3));
|
||||||
|
|
||||||
|
Assert.AreEqual(2, gameHits);
|
||||||
|
Assert.AreEqual(3, modHits);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GeneratedTargetAttachDisposesAllHandlers()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var target = new GeneratedTarget();
|
||||||
|
var binding = host.Attach(target, ShrinkBusKey.Game);
|
||||||
|
|
||||||
|
game.Post(new PingEvent(4));
|
||||||
|
binding.Dispose();
|
||||||
|
game.Post(new PingEvent(7));
|
||||||
|
|
||||||
|
Assert.AreEqual(4, target.Sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AttributeOnlyTargetUsesIlGeneratedBinding()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var target = new CodeGeneratedTarget();
|
||||||
|
|
||||||
|
using (host.Attach(target, ShrinkBusKey.Game))
|
||||||
|
game.Post(new GeneratedPingEvent(9));
|
||||||
|
game.Post(new GeneratedPingEvent(4));
|
||||||
|
|
||||||
|
Assert.AreEqual(9, target.Sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StaticAttributeSubscriberUsesModuleInitializerBinding()
|
||||||
|
{
|
||||||
|
StaticCodeGeneratedTarget.Sum = 0;
|
||||||
|
|
||||||
|
EventBus.Post(new GeneratedPingEvent(11));
|
||||||
|
|
||||||
|
Assert.AreEqual(11, StaticCodeGeneratedTarget.Sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StaticSubscriberAttachesWhenNamedBusIsCreatedLater()
|
||||||
|
{
|
||||||
|
var key = ShrinkBusKey.Mod("late-static");
|
||||||
|
EventBus.RemoveBus(key);
|
||||||
|
DelayedStaticCodeGeneratedTarget.Sum = 0;
|
||||||
|
var bus = EventBus.CreateBus(key, ShrinkBusOptions.Inline());
|
||||||
|
|
||||||
|
bus.Post(new GeneratedPingEvent(13));
|
||||||
|
|
||||||
|
Assert.AreEqual(13, DelayedStaticCodeGeneratedTarget.Sum);
|
||||||
|
Assert.IsTrue(EventBus.RemoveBus(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator MonoScopeRefreshAndReleaseControlGeneratedBindings()
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject("ShrinkEventBusV2-MonoScope");
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
var target = gameObject.AddComponent<MonoCodeGeneratedTarget>();
|
||||||
|
var scope = gameObject.AddComponent<ShrinkMonoEventScope>();
|
||||||
|
Assert.IsInstanceOf<IShrinkGeneratedSubscriber>(target);
|
||||||
|
|
||||||
|
scope.RefreshBindings();
|
||||||
|
yield return null;
|
||||||
|
yield return EventBus.PostAsync(new GeneratedPingEvent(5)).ToCoroutine();
|
||||||
|
Assert.AreEqual(5, target.Sum);
|
||||||
|
|
||||||
|
scope.ReleaseBindings();
|
||||||
|
yield return EventBus.PostAsync(new GeneratedPingEvent(7)).ToCoroutine();
|
||||||
|
Assert.AreEqual(5, target.Sum);
|
||||||
|
UnityEngine.Object.DestroyImmediate(gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CanceledEventsSkipNormalHandlersButReachMonitors()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var normal = 0;
|
||||||
|
var monitor = 0;
|
||||||
|
using var first = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, evt => evt.SetCanceled(true), ShrinkEventPriority.Highest, 0, false);
|
||||||
|
using var second = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => normal++, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
using var third = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => monitor++, ShrinkEventPriority.Monitor, 0, true);
|
||||||
|
|
||||||
|
var result = game.Post(new CancelEvent());
|
||||||
|
|
||||||
|
Assert.IsTrue(result.Canceled);
|
||||||
|
Assert.AreEqual(0, normal);
|
||||||
|
Assert.AreEqual(1, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PriorityAndNumericPriorityAreStable()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var order = new List<string>();
|
||||||
|
using var normal = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => order.Add("normal"), ShrinkEventPriority.Normal, 0, false);
|
||||||
|
using var high = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => order.Add("high"), ShrinkEventPriority.High, 0, false);
|
||||||
|
using var numeric = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => order.Add("numeric"), ShrinkEventPriority.Normal, 75, false);
|
||||||
|
|
||||||
|
game.Post(new PingEvent(1));
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new[] { "numeric", "high", "normal" }, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResultEventUsesTypedResultContract()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<ResultEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, value => value.SetResult(42),
|
||||||
|
ShrinkEventPriority.Normal, 0, false);
|
||||||
|
var value = new ResultEvent();
|
||||||
|
|
||||||
|
var result = game.Post(value);
|
||||||
|
|
||||||
|
Assert.IsTrue(result.Handled);
|
||||||
|
Assert.AreEqual(42, value.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HandlerExceptionPropagatesFromPost()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, _ => throw new InvalidOperationException("expected"),
|
||||||
|
ShrinkEventPriority.Normal, 0, false);
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => game.Post(new PingEvent(1)));
|
||||||
|
|
||||||
|
Assert.AreEqual("expected", exception!.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator PostAsyncCancellationReturnsCanceledFailure()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var bus = host.CreateBus(new ShrinkBusKey("worker", "cancellation"),
|
||||||
|
ShrinkBusOptions.DedicatedThread());
|
||||||
|
using var source = new CancellationTokenSource();
|
||||||
|
source.Cancel();
|
||||||
|
ShrinkPostResult result = default;
|
||||||
|
|
||||||
|
yield return bus.PostAsync(new PingEvent(1), source.Token)
|
||||||
|
.ContinueWith(value => result = value)
|
||||||
|
.ToCoroutine();
|
||||||
|
|
||||||
|
Assert.AreEqual(ShrinkPostFailure.Canceled, result.Failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator DedicatedThreadShutdownDrainsAcceptedWork()
|
||||||
|
{
|
||||||
|
var host = new ShrinkEventBusHost();
|
||||||
|
var key = new ShrinkBusKey("worker", "drain");
|
||||||
|
var bus = host.CreateBus(key, ShrinkBusOptions.DedicatedThread(queueCapacity: 4));
|
||||||
|
var hits = 0;
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key,
|
||||||
|
string.Empty, this, _ => Interlocked.Increment(ref hits),
|
||||||
|
ShrinkEventPriority.Normal, 0, false);
|
||||||
|
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
|
||||||
|
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
|
||||||
|
|
||||||
|
yield return host.ShutdownAsync().ToCoroutine();
|
||||||
|
|
||||||
|
Assert.AreEqual(2, Volatile.Read(ref hits));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DedicatedThreadRejectsWhenBoundedQueueIsFull()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var key = new ShrinkBusKey("worker", "bounded");
|
||||||
|
var options = ShrinkBusOptions.DedicatedThread(queueCapacity: 1);
|
||||||
|
options.OverflowPolicy = ShrinkQueueOverflowPolicy.Reject;
|
||||||
|
var bus = host.CreateBus(key, options);
|
||||||
|
using var entered = new ManualResetEventSlim(false);
|
||||||
|
using var release = new ManualResetEventSlim(false);
|
||||||
|
var hits = 0;
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key, string.Empty,
|
||||||
|
this, _ =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref hits);
|
||||||
|
entered.Set();
|
||||||
|
release.Wait(TimeSpan.FromSeconds(2));
|
||||||
|
}, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
|
||||||
|
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
|
||||||
|
Assert.IsTrue(entered.Wait(TimeSpan.FromSeconds(2)));
|
||||||
|
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
|
||||||
|
var rejected = bus.Post(new PingEvent(3));
|
||||||
|
release.Set();
|
||||||
|
|
||||||
|
Assert.IsFalse(rejected.Accepted);
|
||||||
|
Assert.AreEqual(ShrinkPostFailure.QueueFull, rejected.Failure);
|
||||||
|
Assert.IsTrue(SpinWait.SpinUntil(() => Volatile.Read(ref hits) == 2, TimeSpan.FromSeconds(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TaskPoolHonorsMaximumConcurrency()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var key = new ShrinkBusKey("worker", "parallel");
|
||||||
|
var bus = host.CreateBus(key, ShrinkBusOptions.TaskPool(maxConcurrency: 2, queueCapacity: 8));
|
||||||
|
var active = 0;
|
||||||
|
var maxActive = 0;
|
||||||
|
using var binding = ShrinkGeneratedBinding.SubscribeAsync<PingEvent>(host, key, string.Empty,
|
||||||
|
this, async (_, _) =>
|
||||||
|
{
|
||||||
|
var current = Interlocked.Increment(ref active);
|
||||||
|
UpdateMaximum(ref maxActive, current);
|
||||||
|
Thread.Sleep(30);
|
||||||
|
Interlocked.Decrement(ref active);
|
||||||
|
await UniTask.CompletedTask;
|
||||||
|
}, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
var posts = new List<UniTask<ShrinkPostResult>>();
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
posts.Add(bus.PostAsync(new PingEvent(i)));
|
||||||
|
|
||||||
|
yield return UniTask.WhenAll(posts).ToCoroutine();
|
||||||
|
|
||||||
|
Assert.AreEqual(2, maxActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WarmStructPostDoesNotAllocate()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var sum = 0;
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||||
|
string.Empty, this, evt => sum += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||||
|
for (var i = 0; i < 100; i++)
|
||||||
|
game.Post(new PingEvent(i));
|
||||||
|
|
||||||
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (var i = 0; i < 10_000; i++)
|
||||||
|
game.Post(new PingEvent(i));
|
||||||
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, allocated);
|
||||||
|
Assert.Greater(sum, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GlobalDetailedObserverReportsDispatchMetadata()
|
||||||
|
{
|
||||||
|
var captured = false;
|
||||||
|
var trace = default(ShrinkEventTrace);
|
||||||
|
void Capture(ShrinkEventTrace value)
|
||||||
|
{
|
||||||
|
trace = value;
|
||||||
|
captured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventBus.DetailedPosted += Capture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EventBus.Post(new DiagnosticEvent());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EventBus.DetailedPosted -= Capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(captured);
|
||||||
|
Assert.AreEqual(typeof(DiagnosticEvent), trace.EventType);
|
||||||
|
Assert.AreEqual(ShrinkBusKey.Game, trace.BusKey);
|
||||||
|
Assert.AreEqual(ShrinkBusSchedulerKind.MainThread, trace.Scheduler);
|
||||||
|
Assert.IsTrue(trace.Result.Accepted);
|
||||||
|
Assert.GreaterOrEqual(trace.ElapsedTimestampTicks, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GlobalStructPostDoesNotAllocateWithoutObservers()
|
||||||
|
{
|
||||||
|
var value = new DiagnosticEvent();
|
||||||
|
for (var i = 0; i < 100; i++)
|
||||||
|
EventBus.Post(in value);
|
||||||
|
|
||||||
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (var i = 0; i < 10_000; i++)
|
||||||
|
EventBus.Post(in value);
|
||||||
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator DedicatedThreadPostAsyncRunsOnDedicatedThread()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var bus = host.CreateBus(new ShrinkBusKey("worker", "tests"),
|
||||||
|
ShrinkBusOptions.DedicatedThread());
|
||||||
|
var callerThread = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
var handlerThread = callerThread;
|
||||||
|
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host,
|
||||||
|
new ShrinkBusKey("worker", "tests"), string.Empty, this,
|
||||||
|
_ => handlerThread = Thread.CurrentThread.ManagedThreadId,
|
||||||
|
ShrinkEventPriority.Normal, 0, false);
|
||||||
|
|
||||||
|
yield return bus.PostAsync(new PingEvent(1)).ToCoroutine();
|
||||||
|
|
||||||
|
Assert.AreNotEqual(callerThread, handlerThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateMaximum(ref int location, int candidate)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var current = Volatile.Read(ref location);
|
||||||
|
if (candidate <= current || Interlocked.CompareExchange(ref location, candidate, current) == current)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f11f5ea66801460ca4ec08d35763d627
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# ShrinkEventBus for .NET
|
||||||
|
|
||||||
|
- `ShrinkEventBus.Core`:`netstandard2.1` ValueTask 运行时,不引用 UnityEngine 或 UniTask。
|
||||||
|
- `ShrinkEventBus.Generator`:Roslyn incremental generator,为 `partial` 普通 C# subscriber 生成强类型绑定。
|
||||||
|
- `ShrinkEventBus.Smoke`:`net8.0` 烟测,覆盖多 Bus 隔离、Inline 和 DedicatedThread。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build Tools~/DotNet/ShrinkEventBus.Core/ShrinkEventBus.Core.csproj -c Release
|
||||||
|
dotnet build Tools~/DotNet/ShrinkEventBus.Generator/ShrinkEventBus.Generator.csproj -c Release
|
||||||
|
dotnet run --project Tools~/DotNet/ShrinkEventBus.Smoke/ShrinkEventBus.Smoke.csproj -c Release
|
||||||
|
```
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
internal interface IDotNetScheduler : IDisposable
|
||||||
|
{
|
||||||
|
bool IsOnSchedulerThread { get; }
|
||||||
|
bool TryPost(Action action);
|
||||||
|
ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class QueueFullException : InvalidOperationException { }
|
||||||
|
|
||||||
|
internal static class DotNetSchedulerFactory
|
||||||
|
{
|
||||||
|
public static IDotNetScheduler Create(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
return options.Scheduler switch
|
||||||
|
{
|
||||||
|
ShrinkBusSchedulerKind.MainThread => new SynchronizationContextScheduler(options),
|
||||||
|
ShrinkBusSchedulerKind.DedicatedThread => new DedicatedScheduler(name, options),
|
||||||
|
ShrinkBusSchedulerKind.TaskPool => new TaskPoolScheduler(options),
|
||||||
|
_ => new InlineScheduler()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InlineScheduler : IDotNetScheduler
|
||||||
|
{
|
||||||
|
public bool IsOnSchedulerThread => true;
|
||||||
|
public bool TryPost(Action action) { action(); return true; }
|
||||||
|
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SynchronizationContextScheduler : IDotNetScheduler
|
||||||
|
{
|
||||||
|
private readonly SynchronizationContext _context;
|
||||||
|
private readonly int _threadId;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public SynchronizationContextScheduler(ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_context = SynchronizationContext.Current ?? throw new InvalidOperationException(
|
||||||
|
"MainThread scheduler requires a current SynchronizationContext in a pure .NET host.");
|
||||||
|
_threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == _threadId;
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
|
return false;
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
action();
|
||||||
|
else
|
||||||
|
_context.Post(_ => action(), null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
return action();
|
||||||
|
return new ValueTask(PostCoreAsync(action, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task PostCoreAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
_context.Post(async _ =>
|
||||||
|
{
|
||||||
|
try { cancellationToken.ThrowIfCancellationRequested(); await action(); completion.SetResult(true); }
|
||||||
|
catch (OperationCanceledException) { completion.SetCanceled(); }
|
||||||
|
catch (Exception ex) { completion.SetException(ex); }
|
||||||
|
}, null);
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Interlocked.Exchange(ref _disposed, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TaskPoolScheduler : IDotNetScheduler
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _concurrency;
|
||||||
|
private readonly int _capacity;
|
||||||
|
private int _pending;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public TaskPoolScheduler(ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
var concurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
|
||||||
|
_concurrency = new SemaphoreSlim(concurrency, concurrency);
|
||||||
|
_capacity = options.QueueCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => false;
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _pending);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _concurrency.WaitAsync().ConfigureAwait(false);
|
||||||
|
try { action(); }
|
||||||
|
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _pending);
|
||||||
|
throw new QueueFullException();
|
||||||
|
}
|
||||||
|
await _concurrency.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
try { await action().ConfigureAwait(false); }
|
||||||
|
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _disposed, 1);
|
||||||
|
_concurrency.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DedicatedScheduler : IDotNetScheduler
|
||||||
|
{
|
||||||
|
private sealed class Work
|
||||||
|
{
|
||||||
|
public Func<ValueTask> Action = null!;
|
||||||
|
public TaskCompletionSource<bool>? Completion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly BlockingCollection<Work> _queue;
|
||||||
|
private readonly Thread _thread;
|
||||||
|
private int _threadId;
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public DedicatedScheduler(string name, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
_queue = new BlockingCollection<Work>(options.QueueCapacity);
|
||||||
|
_thread = new Thread(Run) { IsBackground = true, Name = $"ShrinkBus:{name}" };
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
|
||||||
|
|
||||||
|
public bool TryPost(Action action)
|
||||||
|
{
|
||||||
|
if (IsOnSchedulerThread) { action(); return true; }
|
||||||
|
return Volatile.Read(ref _disposed) == 0 && _queue.TryAdd(new Work
|
||||||
|
{
|
||||||
|
Action = () => { action(); return default; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (IsOnSchedulerThread)
|
||||||
|
return action();
|
||||||
|
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
if (Volatile.Read(ref _disposed) != 0 || !_queue.TryAdd(new Work
|
||||||
|
{ Action = action, Completion = completion }))
|
||||||
|
throw new QueueFullException();
|
||||||
|
return new ValueTask(completion.Task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
return;
|
||||||
|
_queue.CompleteAdding();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
|
||||||
|
foreach (var work in _queue.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
try { work.Action().AsTask().GetAwaiter().GetResult(); work.Completion?.SetResult(true); }
|
||||||
|
catch (OperationCanceledException) { work.Completion?.SetCanceled(); }
|
||||||
|
catch (Exception ex) { work.Completion?.SetException(ex); }
|
||||||
|
}
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus
|
||||||
|
{
|
||||||
|
public delegate ValueTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
|
||||||
|
|
||||||
|
public interface IShrinkEventBus : IDisposable
|
||||||
|
{
|
||||||
|
ShrinkBusKey Key { get; }
|
||||||
|
ShrinkBusOptions Options { get; }
|
||||||
|
ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent;
|
||||||
|
ValueTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent;
|
||||||
|
IDisposable Attach(object target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkBusResolver
|
||||||
|
{
|
||||||
|
IShrinkEventBus GetBus(ShrinkBusKey key);
|
||||||
|
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IShrinkGeneratedSubscriber
|
||||||
|
{
|
||||||
|
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkEventBinding : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<IDisposable> _items = new List<IDisposable>();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Add(IDisposable item)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
throw new ArgumentNullException(nameof(item));
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
item.Dispose();
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
|
||||||
|
}
|
||||||
|
_items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
for (var i = _items.Count - 1; i >= 0; i--)
|
||||||
|
_items[i].Dispose();
|
||||||
|
_items.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ShrinkStaticBindingRegistry
|
||||||
|
{
|
||||||
|
internal sealed class Entry
|
||||||
|
{
|
||||||
|
public long Id;
|
||||||
|
public ShrinkBusKey Key;
|
||||||
|
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object Gate = new object();
|
||||||
|
private static readonly List<Entry> Entries = new List<Entry>();
|
||||||
|
private static readonly List<WeakReference<ShrinkEventBusHost>> Hosts =
|
||||||
|
new List<WeakReference<ShrinkEventBusHost>>();
|
||||||
|
private static long _nextId;
|
||||||
|
|
||||||
|
public static void Register(ShrinkBusKey key,
|
||||||
|
Func<IShrinkBusResolver, IDisposable> factory)
|
||||||
|
{
|
||||||
|
if (factory == null)
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
|
ShrinkEventBusHost[] hosts;
|
||||||
|
Entry entry;
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
entry = new Entry
|
||||||
|
{
|
||||||
|
Id = Interlocked.Increment(ref _nextId),
|
||||||
|
Key = key,
|
||||||
|
Factory = factory
|
||||||
|
};
|
||||||
|
Entries.Add(entry);
|
||||||
|
hosts = LiveHostsLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < hosts.Length; i++)
|
||||||
|
hosts[i].TryAttachStatic(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void TrackHost(ShrinkEventBusHost host)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
LiveHostsLocked();
|
||||||
|
Hosts.Add(new WeakReference<ShrinkEventBusHost>(host));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Entry[] GetEntries(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
return Entries.FindAll(item => item.Key == key).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ShrinkEventBusHost[] LiveHostsLocked()
|
||||||
|
{
|
||||||
|
var result = new List<ShrinkEventBusHost>(Hosts.Count);
|
||||||
|
for (var i = Hosts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Hosts[i].TryGetTarget(out var host))
|
||||||
|
result.Add(host);
|
||||||
|
else
|
||||||
|
Hosts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses =
|
||||||
|
new ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus>();
|
||||||
|
private readonly object _staticGate = new object();
|
||||||
|
private readonly Dictionary<long, IDisposable> _staticBindings =
|
||||||
|
new Dictionary<long, IDisposable>();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public ShrinkEventBusHost()
|
||||||
|
{
|
||||||
|
ShrinkStaticBindingRegistry.TrackHost(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
|
||||||
|
var bus = new DotNetShrinkEventBus(key, options);
|
||||||
|
if (!_buses.TryAdd(key, bus))
|
||||||
|
{
|
||||||
|
bus.Dispose();
|
||||||
|
throw new InvalidOperationException($"Bus '{key}' is already registered.");
|
||||||
|
}
|
||||||
|
var entries = ShrinkStaticBindingRegistry.GetEntries(key);
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
TryAttachStatic(entries[i]);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IShrinkEventBus GetBus(ShrinkBusKey key)
|
||||||
|
{
|
||||||
|
if (_buses.TryGetValue(key, out var bus))
|
||||||
|
return bus;
|
||||||
|
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
|
||||||
|
_buses.TryGetValue(key, out bus!);
|
||||||
|
|
||||||
|
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
|
||||||
|
{
|
||||||
|
if (target is not IShrinkGeneratedSubscriber generated)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Type {target?.GetType().FullName ?? "<null>"} has no generated event binding.");
|
||||||
|
return generated.AttachGenerated(this, defaultBus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
lock (_staticGate)
|
||||||
|
{
|
||||||
|
foreach (var binding in _staticBindings.Values)
|
||||||
|
binding.Dispose();
|
||||||
|
_staticBindings.Clear();
|
||||||
|
}
|
||||||
|
foreach (var bus in _buses.Values)
|
||||||
|
bus.Dispose();
|
||||||
|
_buses.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TryAttachStatic(ShrinkStaticBindingRegistry.Entry entry)
|
||||||
|
{
|
||||||
|
lock (_staticGate)
|
||||||
|
{
|
||||||
|
if (_disposed || _staticBindings.ContainsKey(entry.Id) || !_buses.ContainsKey(entry.Key))
|
||||||
|
return;
|
||||||
|
_staticBindings.Add(entry.Id, entry.Factory(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ShrinkGeneratedBinding
|
||||||
|
{
|
||||||
|
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
|
||||||
|
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
return Resolve(resolver, defaultBus, configuredBus).Subscribe(
|
||||||
|
owner, handler, priority, numericPriority, receiveCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
return Resolve(resolver, defaultBus, configuredBus).SubscribeAsync(
|
||||||
|
owner, handler, priority, numericPriority, receiveCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DotNetShrinkEventBus Resolve(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||||
|
string? configuredBus)
|
||||||
|
{
|
||||||
|
var key = string.IsNullOrWhiteSpace(configuredBus)
|
||||||
|
? defaultBus ?? ShrinkBusKey.Game
|
||||||
|
: ShrinkBusKey.Parse(configuredBus);
|
||||||
|
return resolver.GetBus(key) as DotNetShrinkEventBus
|
||||||
|
?? throw new InvalidOperationException("Generated bindings require the built-in bus implementation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DotNetShrinkEventBus : IShrinkEventBus
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<Type, IEventChannel> _channels =
|
||||||
|
new ConcurrentDictionary<Type, IEventChannel>();
|
||||||
|
private readonly IDotNetScheduler _scheduler;
|
||||||
|
private long _nextSubscriptionId;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public DotNetShrinkEventBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
|
||||||
|
_scheduler = DotNetSchedulerFactory.Create(key.Id, Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkBusKey Key { get; }
|
||||||
|
public ShrinkBusOptions Options { get; }
|
||||||
|
|
||||||
|
public ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped);
|
||||||
|
var copied = eventData;
|
||||||
|
if (_scheduler.IsOnSchedulerThread)
|
||||||
|
return Channel<TEvent>().Post(copied);
|
||||||
|
|
||||||
|
return _scheduler.TryPost(() => Channel<TEvent>().Post(copied))
|
||||||
|
? new ShrinkPostResult(true, false, false)
|
||||||
|
: ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||||
|
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped);
|
||||||
|
ShrinkPostResult result = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _scheduler.PostAsync(async () =>
|
||||||
|
{
|
||||||
|
result = await Channel<TEvent>().PostAsync(eventData, Options.DispatchMode, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (QueueFullException)
|
||||||
|
{
|
||||||
|
return ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Attach(object target)
|
||||||
|
{
|
||||||
|
if (target is not IShrinkGeneratedSubscriber generated)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Type {target?.GetType().FullName ?? "<null>"} has no generated event binding.");
|
||||||
|
return generated.AttachGenerated(new SingleBusResolver(this), Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Subscribe<TEvent>(object? owner, Action<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
return Channel<TEvent>().Add(Interlocked.Increment(ref _nextSubscriptionId), owner,
|
||||||
|
handler, null, priority, numericPriority, receiveCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable SubscribeAsync<TEvent>(object? owner, ShrinkAsyncEventHandler<TEvent> handler,
|
||||||
|
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
return Channel<TEvent>().Add(Interlocked.Increment(ref _nextSubscriptionId), owner,
|
||||||
|
null, handler, priority, numericPriority, receiveCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
_scheduler.Dispose();
|
||||||
|
foreach (var channel in _channels.Values)
|
||||||
|
channel.Clear();
|
||||||
|
_channels.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventChannel<TEvent> Channel<TEvent>() where TEvent : IShrinkEvent =>
|
||||||
|
(EventChannel<TEvent>)_channels.GetOrAdd(typeof(TEvent), _ => new EventChannel<TEvent>());
|
||||||
|
|
||||||
|
private sealed class SingleBusResolver : IShrinkBusResolver
|
||||||
|
{
|
||||||
|
private readonly IShrinkEventBus _bus;
|
||||||
|
public SingleBusResolver(IShrinkEventBus bus) => _bus = bus;
|
||||||
|
public IShrinkEventBus GetBus(ShrinkBusKey key) => key == _bus.Key
|
||||||
|
? _bus
|
||||||
|
: throw new KeyNotFoundException($"Bus '{key}' is not available.");
|
||||||
|
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus)
|
||||||
|
{
|
||||||
|
bus = _bus;
|
||||||
|
return key == _bus.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IEventChannel
|
||||||
|
{
|
||||||
|
void Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EventChannel<TEvent> : IEventChannel where TEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
private sealed class Entry
|
||||||
|
{
|
||||||
|
public long Id = 0;
|
||||||
|
public object? Owner;
|
||||||
|
public Action<TEvent>? Sync;
|
||||||
|
public ShrinkAsyncEventHandler<TEvent>? Async;
|
||||||
|
public ShrinkEventPriority Priority;
|
||||||
|
public int NumericPriority;
|
||||||
|
public bool ReceiveCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _gate = new object();
|
||||||
|
private readonly List<Entry> _entries = new List<Entry>();
|
||||||
|
private Entry[] _snapshot = Array.Empty<Entry>();
|
||||||
|
|
||||||
|
public IDisposable Add(long id, object? owner, Action<TEvent>? sync,
|
||||||
|
ShrinkAsyncEventHandler<TEvent>? asyncHandler, ShrinkEventPriority priority,
|
||||||
|
int numericPriority, bool receiveCanceled)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Add(new Entry
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Owner = owner,
|
||||||
|
Sync = sync,
|
||||||
|
Async = asyncHandler,
|
||||||
|
Priority = priority,
|
||||||
|
NumericPriority = numericPriority,
|
||||||
|
ReceiveCanceled = receiveCanceled
|
||||||
|
});
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
return new Subscription(this, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkPostResult Post(TEvent eventData)
|
||||||
|
{
|
||||||
|
var handled = false;
|
||||||
|
var snapshot = _snapshot;
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
if (entry.Sync != null)
|
||||||
|
entry.Sync(eventData);
|
||||||
|
else if (entry.Async != null)
|
||||||
|
_ = entry.Async(eventData, CancellationToken.None).AsTask();
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode mode,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var handled = false;
|
||||||
|
var snapshot = _snapshot;
|
||||||
|
if (mode == ShrinkDispatchMode.Parallel)
|
||||||
|
{
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
if (entry.Sync != null)
|
||||||
|
entry.Sync(eventData);
|
||||||
|
else if (entry.Async != null)
|
||||||
|
tasks.Add(entry.Async(eventData, cancellationToken).AsTask());
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var entry = snapshot[i];
|
||||||
|
if (IsCanceled(eventData) && !entry.ReceiveCanceled)
|
||||||
|
continue;
|
||||||
|
if (entry.Sync != null)
|
||||||
|
entry.Sync(eventData);
|
||||||
|
else if (entry.Async != null)
|
||||||
|
await entry.Async(eventData, cancellationToken).ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Clear();
|
||||||
|
_snapshot = Array.Empty<Entry>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Remove(long id)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.RemoveAll(item => item.Id == id);
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rebuild()
|
||||||
|
{
|
||||||
|
_entries.Sort((left, right) =>
|
||||||
|
{
|
||||||
|
var leftValue = left.NumericPriority == 0 ? (int)left.Priority * 1000 : -left.NumericPriority;
|
||||||
|
var rightValue = right.NumericPriority == 0 ? (int)right.Priority * 1000 : -right.NumericPriority;
|
||||||
|
var result = leftValue.CompareTo(rightValue);
|
||||||
|
return result != 0 ? result : left.Id.CompareTo(right.Id);
|
||||||
|
});
|
||||||
|
_snapshot = _entries.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCanceled(TEvent value) =>
|
||||||
|
value is IShrinkCancelableEvent cancelable && cancelable.IsCanceled;
|
||||||
|
|
||||||
|
private sealed class Subscription : IDisposable
|
||||||
|
{
|
||||||
|
private EventChannel<TEvent>? _owner;
|
||||||
|
private readonly long _id;
|
||||||
|
public Subscription(EventChannel<TEvent> owner, long id)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_id = id;
|
||||||
|
}
|
||||||
|
public void Dispose() => Interlocked.Exchange(ref _owner, null)?.Remove(_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
|
||||||
|
namespace ShrinkEventBus.Generator
|
||||||
|
{
|
||||||
|
[Generator]
|
||||||
|
public sealed class ShrinkEventBusGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
private const string SubscriberAttribute = "ShrinkEventBus.ShrinkEventSubscriberAttribute";
|
||||||
|
private const string SubscribeAttribute = "ShrinkEventBus.ShrinkSubscribeAttribute";
|
||||||
|
|
||||||
|
private static readonly DiagnosticDescriptor PartialRequired = new DiagnosticDescriptor(
|
||||||
|
"SHRINKEVENT001", "Subscriber must be partial",
|
||||||
|
"Subscriber type '{0}' must be partial so ShrinkEventBus can generate a reflection-free binding",
|
||||||
|
"ShrinkEventBus", DiagnosticSeverity.Error, true);
|
||||||
|
|
||||||
|
private static readonly DiagnosticDescriptor UnsupportedHandler = new DiagnosticDescriptor(
|
||||||
|
"SHRINKEVENT002", "Unsupported handler signature",
|
||||||
|
"Handler '{0}' must be void(TEvent), UniTask(TEvent), UniTask(TEvent, CancellationToken), or ValueTask(TEvent, CancellationToken)",
|
||||||
|
"ShrinkEventBus", DiagnosticSeverity.Error, true);
|
||||||
|
|
||||||
|
private static readonly DiagnosticDescriptor TopLevelRequired = new DiagnosticDescriptor(
|
||||||
|
"SHRINKEVENT003", "Top-level subscriber required",
|
||||||
|
"Subscriber type '{0}' must be top-level in the current generator version",
|
||||||
|
"ShrinkEventBus", DiagnosticSeverity.Error, true);
|
||||||
|
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
context.RegisterSourceOutput(context.CompilationProvider,
|
||||||
|
static (sourceContext, compilation) =>
|
||||||
|
{
|
||||||
|
if (compilation.GetTypeByMetadataName(
|
||||||
|
"System.Runtime.CompilerServices.ModuleInitializerAttribute") == null)
|
||||||
|
{
|
||||||
|
sourceContext.AddSource("ShrinkEventBus.ModuleInitializerAttribute.g.cs",
|
||||||
|
SourceText.From(
|
||||||
|
"namespace System.Runtime.CompilerServices { [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false)] internal sealed class ModuleInitializerAttribute : global::System.Attribute { } }",
|
||||||
|
Encoding.UTF8));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var candidates = context.SyntaxProvider.ForAttributeWithMetadataName(
|
||||||
|
SubscriberAttribute,
|
||||||
|
static (node, _) => node is ClassDeclarationSyntax,
|
||||||
|
static (syntaxContext, _) => (INamedTypeSymbol)syntaxContext.TargetSymbol);
|
||||||
|
|
||||||
|
context.RegisterSourceOutput(candidates.Collect(), Generate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Generate(SourceProductionContext context,
|
||||||
|
ImmutableArray<INamedTypeSymbol> candidates)
|
||||||
|
{
|
||||||
|
foreach (var type in candidates)
|
||||||
|
GenerateType(context, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GenerateType(SourceProductionContext context, INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
if (type.ContainingType != null)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(TopLevelRequired,
|
||||||
|
type.Locations.FirstOrDefault(), type.ToDisplayString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPartial = type.DeclaringSyntaxReferences
|
||||||
|
.Select(reference => reference.GetSyntax())
|
||||||
|
.OfType<ClassDeclarationSyntax>()
|
||||||
|
.Any(declaration => declaration.Modifiers.Any(SyntaxKind.PartialKeyword));
|
||||||
|
if (!isPartial)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(PartialRequired,
|
||||||
|
type.Locations.FirstOrDefault(), type.ToDisplayString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handlers = new List<HandlerModel>();
|
||||||
|
foreach (var method in type.GetMembers().OfType<IMethodSymbol>())
|
||||||
|
{
|
||||||
|
var attribute = method.GetAttributes().FirstOrDefault(item =>
|
||||||
|
item.AttributeClass?.ToDisplayString() == SubscribeAttribute);
|
||||||
|
if (attribute == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!TryCreateHandler(method, attribute, type.IsStatic, out var handler))
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(UnsupportedHandler,
|
||||||
|
method.Locations.FirstOrDefault(), method.ToDisplayString()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handlers.Add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriber = type.GetAttributes().First(item =>
|
||||||
|
item.AttributeClass?.ToDisplayString() == SubscriberAttribute);
|
||||||
|
var defaultBus = ReadString(subscriber, "DefaultBus");
|
||||||
|
var source = type.IsStatic
|
||||||
|
? BuildStaticSource(type, handlers, defaultBus)
|
||||||
|
: BuildSource(type, handlers, defaultBus);
|
||||||
|
var hint = type.ToDisplayString().Replace('.', '_').Replace('+', '_') + ".ShrinkEvents.g.cs";
|
||||||
|
context.AddSource(hint, SourceText.From(source, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateHandler(IMethodSymbol method, AttributeData attribute,
|
||||||
|
bool staticSubscriber,
|
||||||
|
out HandlerModel model)
|
||||||
|
{
|
||||||
|
model = default;
|
||||||
|
if (method.IsStatic != staticSubscriber || method.Parameters.Length == 0 || method.Parameters.Length > 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var eventType = method.Parameters[0].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
var returnType = method.ReturnType.ToDisplayString();
|
||||||
|
var isSync = method.ReturnsVoid && method.Parameters.Length == 1;
|
||||||
|
var isValueTask = returnType == "System.Threading.Tasks.ValueTask" &&
|
||||||
|
method.Parameters.Length == 2 &&
|
||||||
|
method.Parameters[1].Type.ToDisplayString() == "System.Threading.CancellationToken";
|
||||||
|
var isUniTask = returnType == "Cysharp.Threading.Tasks.UniTask";
|
||||||
|
var isUniTaskLegacy = isUniTask && method.Parameters.Length == 1;
|
||||||
|
var isUniTaskCancelable = isUniTask && method.Parameters.Length == 2 &&
|
||||||
|
method.Parameters[1].Type.ToDisplayString() == "System.Threading.CancellationToken";
|
||||||
|
if (!isSync && !isValueTask && !isUniTaskLegacy && !isUniTaskCancelable)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var bindingMethod = isSync
|
||||||
|
? "Subscribe"
|
||||||
|
: isUniTaskLegacy ? "SubscribeAsyncLegacy" : "SubscribeAsync";
|
||||||
|
|
||||||
|
model = new HandlerModel(
|
||||||
|
method.Name,
|
||||||
|
eventType,
|
||||||
|
bindingMethod,
|
||||||
|
ReadString(attribute, "Bus"),
|
||||||
|
ReadInt(attribute, "Priority", 2),
|
||||||
|
ReadInt(attribute, "NumericPriority", 0),
|
||||||
|
ReadBool(attribute, "ReceiveCanceled", false));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSource(INamedTypeSymbol type, IReadOnlyList<HandlerModel> handlers,
|
||||||
|
string defaultBus)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||||||
|
{
|
||||||
|
builder.Append("namespace ").Append(type.ContainingNamespace.ToDisplayString()).AppendLine();
|
||||||
|
builder.AppendLine("{");
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessibility = type.DeclaredAccessibility switch
|
||||||
|
{
|
||||||
|
Accessibility.Public => "public ",
|
||||||
|
Accessibility.Internal => "internal ",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
builder.Append(" ").Append(accessibility).Append("partial class ")
|
||||||
|
.Append(type.Name).AppendLine(" : global::ShrinkEventBus.IShrinkGeneratedSubscriber");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" global::System.IDisposable global::ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated(");
|
||||||
|
builder.AppendLine(" global::ShrinkEventBus.IShrinkBusResolver resolver,");
|
||||||
|
builder.AppendLine(" global::ShrinkEventBus.ShrinkBusKey? defaultBus)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" var binding = new global::ShrinkEventBus.ShrinkEventBinding();");
|
||||||
|
|
||||||
|
foreach (var handler in handlers)
|
||||||
|
{
|
||||||
|
var bus = string.IsNullOrWhiteSpace(handler.Bus) ? defaultBus : handler.Bus;
|
||||||
|
builder.Append(" binding.Add(global::ShrinkEventBus.ShrinkGeneratedBinding.")
|
||||||
|
.Append(handler.BindingMethod).Append('<').Append(handler.EventType).AppendLine(">(");
|
||||||
|
builder.Append(" resolver, defaultBus, ")
|
||||||
|
.Append(ToLiteral(bus)).AppendLine(", this,");
|
||||||
|
builder.Append(" this.").Append(handler.MethodName)
|
||||||
|
.Append(", (global::ShrinkEventBus.ShrinkEventPriority)")
|
||||||
|
.Append(handler.Priority).Append(", ").Append(handler.NumericPriority).Append(", ")
|
||||||
|
.Append(handler.ReceiveCanceled ? "true" : "false").AppendLine("));");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" return binding;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildStaticSource(INamedTypeSymbol type,
|
||||||
|
IReadOnlyList<HandlerModel> handlers, string defaultBus)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||||||
|
{
|
||||||
|
builder.Append("namespace ").Append(type.ContainingNamespace.ToDisplayString()).AppendLine();
|
||||||
|
builder.AppendLine("{");
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessibility = type.DeclaredAccessibility switch
|
||||||
|
{
|
||||||
|
Accessibility.Public => "public ",
|
||||||
|
Accessibility.Internal => "internal ",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
builder.Append(" ").Append(accessibility).Append("static partial class ")
|
||||||
|
.Append(type.Name).AppendLine();
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
|
||||||
|
builder.AppendLine(" internal static void ShrinkEventBus_RegisterStaticBindings()");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
for (var i = 0; i < handlers.Count; i++)
|
||||||
|
{
|
||||||
|
var bus = string.IsNullOrWhiteSpace(handlers[i].Bus) ? defaultBus : handlers[i].Bus;
|
||||||
|
builder.Append(" global::ShrinkEventBus.ShrinkStaticBindingRegistry.Register(")
|
||||||
|
.Append("global::ShrinkEventBus.ShrinkBusKey.Parse(")
|
||||||
|
.Append(ToLiteral(bus)).Append("), ShrinkEventBus_Bind_").Append(i).AppendLine(");");
|
||||||
|
}
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
|
||||||
|
for (var i = 0; i < handlers.Count; i++)
|
||||||
|
{
|
||||||
|
var handler = handlers[i];
|
||||||
|
var bus = string.IsNullOrWhiteSpace(handler.Bus) ? defaultBus : handler.Bus;
|
||||||
|
builder.Append(" private static global::System.IDisposable ShrinkEventBus_Bind_")
|
||||||
|
.Append(i).AppendLine("(global::ShrinkEventBus.IShrinkBusResolver resolver)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.Append(" return global::ShrinkEventBus.ShrinkGeneratedBinding.")
|
||||||
|
.Append(handler.BindingMethod).Append('<').Append(handler.EventType).AppendLine(">(");
|
||||||
|
builder.Append(" resolver, null, ").Append(ToLiteral(bus))
|
||||||
|
.AppendLine(", null,");
|
||||||
|
builder.Append(" ").Append(handler.MethodName)
|
||||||
|
.Append(", (global::ShrinkEventBus.ShrinkEventPriority)")
|
||||||
|
.Append(handler.Priority).Append(", ").Append(handler.NumericPriority).Append(", ")
|
||||||
|
.Append(handler.ReceiveCanceled ? "true" : "false").AppendLine(");");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadString(AttributeData attribute, string name)
|
||||||
|
{
|
||||||
|
foreach (var pair in attribute.NamedArguments)
|
||||||
|
{
|
||||||
|
if (pair.Key == name)
|
||||||
|
return pair.Value.Value as string ?? string.Empty;
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt(AttributeData attribute, string name, int defaultValue)
|
||||||
|
{
|
||||||
|
foreach (var pair in attribute.NamedArguments)
|
||||||
|
{
|
||||||
|
if (pair.Key == name && pair.Value.Value != null)
|
||||||
|
return Convert.ToInt32(pair.Value.Value);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReadBool(AttributeData attribute, string name, bool defaultValue)
|
||||||
|
{
|
||||||
|
foreach (var pair in attribute.NamedArguments)
|
||||||
|
{
|
||||||
|
if (pair.Key == name && pair.Value.Value is bool value)
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToLiteral(string value) => SymbolDisplay.FormatLiteral(value ?? string.Empty, true);
|
||||||
|
|
||||||
|
private readonly struct HandlerModel
|
||||||
|
{
|
||||||
|
public HandlerModel(string methodName, string eventType, string bindingMethod, string bus,
|
||||||
|
int priority, int numericPriority, bool receiveCanceled)
|
||||||
|
{
|
||||||
|
MethodName = methodName;
|
||||||
|
EventType = eventType;
|
||||||
|
BindingMethod = bindingMethod;
|
||||||
|
Bus = bus;
|
||||||
|
Priority = priority;
|
||||||
|
NumericPriority = numericPriority;
|
||||||
|
ReceiveCanceled = receiveCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string MethodName { get; }
|
||||||
|
public string EventType { get; }
|
||||||
|
public string BindingMethod { get; }
|
||||||
|
public string Bus { get; }
|
||||||
|
public int Priority { get; }
|
||||||
|
public int NumericPriority { get; }
|
||||||
|
public bool ReceiveCanceled { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ShrinkEventBus;
|
||||||
|
|
||||||
|
internal readonly struct PingEvent : IShrinkEvent
|
||||||
|
{
|
||||||
|
public PingEvent(int value) => Value = value;
|
||||||
|
public int Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
internal sealed partial class SmokeSubscriber
|
||||||
|
{
|
||||||
|
public int Sum { get; private set; }
|
||||||
|
public int ThreadId { get; private set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private void OnPing(PingEvent evt)
|
||||||
|
{
|
||||||
|
Sum += evt.Value;
|
||||||
|
ThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||||
|
internal static partial class StaticSmokeSubscriber
|
||||||
|
{
|
||||||
|
public static int Sum { get; set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private static void OnPing(PingEvent evt) => Sum += evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static async Task<int> Main()
|
||||||
|
{
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var workerKey = new ShrinkBusKey("worker", "smoke");
|
||||||
|
var worker = host.CreateBus(workerKey, ShrinkBusOptions.DedicatedThread());
|
||||||
|
var gameSubscriber = new SmokeSubscriber();
|
||||||
|
var workerSubscriber = new SmokeSubscriber();
|
||||||
|
var callerThread = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
using var gameBinding = host.Attach(gameSubscriber, ShrinkBusKey.Game);
|
||||||
|
using var workerBinding = host.Attach(workerSubscriber, workerKey);
|
||||||
|
game.Post(new PingEvent(2));
|
||||||
|
await worker.PostAsync(new PingEvent(3));
|
||||||
|
|
||||||
|
if (gameSubscriber.Sum != 2 || workerSubscriber.Sum != 3 || StaticSmokeSubscriber.Sum != 2 ||
|
||||||
|
workerSubscriber.ThreadId == callerThread)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("FAIL");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("PASS");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c0d34cdfe6432fc45b8e3aa75065b201
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable:
|
||||||
|
- first:
|
||||||
|
213: -6576374159750988736
|
||||||
|
second: img1_0
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 1
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 2
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Android
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: WebGL
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites:
|
||||||
|
- serializedVersion: 2
|
||||||
|
name: img1_0
|
||||||
|
rect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 659
|
||||||
|
height: 658
|
||||||
|
alignment: 0
|
||||||
|
pivot: {x: 0, y: 0}
|
||||||
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
customData:
|
||||||
|
outline: []
|
||||||
|
physicsShape: []
|
||||||
|
tessellationDetail: -1
|
||||||
|
bones: []
|
||||||
|
spriteID: 04cfbf97b860cb4a0800000000000000
|
||||||
|
internalID: -6576374159750988736
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID:
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable:
|
||||||
|
img1_0: -6576374159750988736
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 265b481fbf28c6949a2e93bcfcdbe946
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable:
|
||||||
|
- first:
|
||||||
|
213: -1076633883802439962
|
||||||
|
second: img2_0
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 1
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 2
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Android
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: WebGL
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites:
|
||||||
|
- serializedVersion: 2
|
||||||
|
name: img2_0
|
||||||
|
rect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 659
|
||||||
|
height: 658
|
||||||
|
alignment: 0
|
||||||
|
pivot: {x: 0, y: 0}
|
||||||
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
customData:
|
||||||
|
outline: []
|
||||||
|
physicsShape: []
|
||||||
|
tessellationDetail: -1
|
||||||
|
bones: []
|
||||||
|
spriteID: 6ee6ecadf270f01f0800000000000000
|
||||||
|
internalID: -1076633883802439962
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID:
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable:
|
||||||
|
img2_0: -1076633883802439962
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "com.cneicy.shrink-eventbus",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"displayName": "ShrinkEventBus",
|
||||||
|
"description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。",
|
||||||
|
"unity": "2022.3",
|
||||||
|
"documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus",
|
||||||
|
"changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/CHANGELOG.md",
|
||||||
|
"licensesUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/LICENSE",
|
||||||
|
"dependencies": {
|
||||||
|
"com.cysharp.unitask": "2.5.10",
|
||||||
|
"com.unity.nuget.mono-cecil": "1.11.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"eventbus",
|
||||||
|
"event",
|
||||||
|
"messaging",
|
||||||
|
"unitask"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "cneicy",
|
||||||
|
"url": "https://git.crash.work/ShrinkSDK"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 09a3fe335f19475db0839afd05b0d8cb
|
||||||
|
timeCreated: 1772900717
|
||||||
Reference in New Issue
Block a user