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