feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
# 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/
|
||||
*.unityproj
|
||||
*.sln
|
||||
*.csproj
|
||||
*.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*
|
||||
Assets/StreamingAssets/Whisper/ggml-base.bin
|
||||
.planning/
|
||||
AGENTS.md
|
||||
Assets/Screenshots/
|
||||
output/
|
||||
tmp/
|
||||
Packages/
|
||||
Assets/unity-mcp/
|
||||
Assets/unity-mcp.meta
|
||||
.vscode/
|
||||
.claude/
|
||||
.codex/
|
||||
.funplay/
|
||||
CLAUDE.md
|
||||
KNOWN_PITFALLS.md
|
||||
Assets/~UnityDirMonSyncFile~84ae11425aac2204fbff8739574b1de6~
|
||||
.idea/
|
||||
GeneratedServers/
|
||||
GeneratedModSdk/
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a618cfe20080eb41a405afb915c6598
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aacbfcd500ef7304b9d5c78a9467b191
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkApp.Core` 的包内变更。
|
||||
|
||||
## [0.1.1] - 2026-08-16
|
||||
|
||||
### Added
|
||||
|
||||
- `ShrinkAppSettings.hostingMode`(`ShrinkAppHostingMode`):`ClassicHost` 保持原有自动启动;`ContextLoader` 让位给 ShrinkContext 加载器宿主(见 `ShrinkContext.AppAdapter`),支持运行中按模块 disable/enable。
|
||||
- `ShrinkApp.InitializeForExternalHost(services, settings)`:外部宿主把服务容器接回 `ShrinkApp.Services` 静态入口。
|
||||
- `ShrinkAppInstallers.GetDiscoveredInstallerTypes()`:编译期安装器发现的公共入口,供外部宿主枚举。
|
||||
- `ShrinkAppContext.CreateStandalone(services, settings)`:非 MonoBehaviour 宿主构建上下文。
|
||||
|
||||
### Changed
|
||||
|
||||
- `Bootstrap` 在 `ContextLoader` 模式下直接返回,经典路径零行为变化。
|
||||
|
||||
## [0.1.0] - 2026-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- 新增统一宿主层 `ShrinkApp.Core`。
|
||||
- 新增 `ShrinkApp`、`ShrinkAppHost`、`ShrinkAppSettings`、`ShrinkAppServices`、`ShrinkAppContext`。
|
||||
- 新增模块安装器契约:`IShrinkAppModuleInstaller` 与 `[ShrinkAppModuleInstaller]`。
|
||||
- 新增运行时生命周期事件:`ShrinkAppStartedEvent`、`ShrinkAppStartFailedEvent`。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3557769562f2f7042a462bcf44b9fedf
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 346774b237ebc2b49bc74a69b150780e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ShrinkApp.Core.Editor",
|
||||
"rootNamespace": "ShrinkApp.Editor",
|
||||
"references": [
|
||||
"ShrinkApp.Core.Runtime"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d16f21ff20129e14283877a81cca3054
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp.Editor
|
||||
{
|
||||
public static class ShrinkAppEditorMenu
|
||||
{
|
||||
[MenuItem("ShrinkApp/创建 Settings")]
|
||||
public static void CreateSettingsAsset()
|
||||
{
|
||||
const string resourcesDir = "Assets/Resources";
|
||||
if (!AssetDatabase.IsValidFolder(resourcesDir))
|
||||
AssetDatabase.CreateFolder("Assets", "Resources");
|
||||
|
||||
const string assetPath = "Assets/Resources/ShrinkAppSettings.asset";
|
||||
var existing = AssetDatabase.LoadAssetAtPath<ShrinkAppSettings>(assetPath);
|
||||
if (existing != null)
|
||||
{
|
||||
Selection.activeObject = existing;
|
||||
EditorGUIUtility.PingObject(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧版本曾生成过 m_Script=fileID:0 的无效占位资产。删除该占位后
|
||||
// 才能在同一路径创建真正绑定 ShrinkAppSettings 类型的资产。
|
||||
if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null)
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
|
||||
var asset = ScriptableObject.CreateInstance<ShrinkAppSettings>();
|
||||
AssetDatabase.CreateAsset(asset, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
Selection.activeObject = asset;
|
||||
EditorGUIUtility.PingObject(asset);
|
||||
Debug.Log("[ShrinkApp] 已创建 ShrinkAppSettings.asset");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30f867e31a8e02e41b25f1ef5b4dddec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
# ShrinkApp.Core
|
||||
|
||||
`ShrinkApp.Core` 是 `Shrink` 系列模块的统一宿主层。它不直接替代各个基础包的独立使用方式,而是在项目需要“导入一组包后直接起盘”时,提供统一的启动链、模块安装器、服务容器和基础生命周期事件。
|
||||
|
||||
## 当前定位
|
||||
|
||||
- 统一宿主:在 `BeforeSceneLoad` 阶段创建 `ShrinkAppHost`
|
||||
- 模块安装器:通过编译期注入的安装器注册表获取 `IShrinkAppModuleInstaller`
|
||||
- 服务容器:各模块通过 `RegisterServices(...)` 暴露门面服务
|
||||
- 生命周期事件:通过 `ShrinkEventBus` 发布 `ShrinkAppStartedEvent` / `ShrinkAppStartFailedEvent`
|
||||
|
||||
## 核心类型
|
||||
|
||||
```csharp
|
||||
ShrinkApp
|
||||
ShrinkAppHost
|
||||
ShrinkAppSettings
|
||||
ShrinkAppServices
|
||||
ShrinkAppContext
|
||||
IShrinkAppModuleInstaller
|
||||
[ShrinkAppModuleInstaller]
|
||||
```
|
||||
|
||||
## 启动流程
|
||||
|
||||
1. `ShrinkApp` 在 `RuntimeInitializeOnLoadMethod(BeforeSceneLoad)` 创建宿主。
|
||||
2. 宿主从编译期注册表 `ShrinkAppGeneratedRegistry` 读取全部安装器类型。
|
||||
3. 安装器按 `DependsOn + Order` 排序。
|
||||
4. 先执行 `RegisterServices(...)`,再顺序执行 `InitializeAsync(...)`。
|
||||
5. 启动完成后在首场景 `Start()` 阶段发布 `ShrinkAppStartedEvent`。
|
||||
6. 任一安装器重复、依赖缺失、循环依赖、初始化异常都会 fail-fast。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 271d3627f55ae2b498a8b430db8d2b41
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c30f0c5343e70914d970bffb896bd45a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ShrinkApp.Core.Runtime",
|
||||
"rootNamespace": "ShrinkApp",
|
||||
"references": [
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b7047dbc0ca2e438a92daa2baa200c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
public static class ShrinkApp
|
||||
{
|
||||
public static ShrinkAppHost? Host { get; private set; }
|
||||
public static ShrinkAppContext? Context { get; private set; }
|
||||
public static ShrinkAppSettings Settings => ShrinkAppSettings.Instance;
|
||||
public static ShrinkAppServices Services => Context?.Services ?? throw new System.InvalidOperationException("ShrinkApp is not initialized.");
|
||||
public static bool IsRunning => (Host != null && Host.IsRunning) || _externalHostRunning;
|
||||
|
||||
private static bool _externalHostRunning;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
Host = null;
|
||||
Context = null;
|
||||
_externalHostRunning = false;
|
||||
ShrinkAppSettings.ResetCachedInstance();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
if (Host != null)
|
||||
return;
|
||||
|
||||
var settings = ShrinkAppSettings.Instance;
|
||||
if (settings.hostingMode == ShrinkAppHostingMode.ContextLoader)
|
||||
{
|
||||
// 阶段 2 过渡:经典宿主让位,由 ShrinkContext 加载器宿主(ShrinkContext.AppAdapter)接管启动
|
||||
return;
|
||||
}
|
||||
|
||||
var hostGameObject = new GameObject("ShrinkAppHost");
|
||||
var host = hostGameObject.AddComponent<ShrinkAppHost>();
|
||||
if (settings.dontDestroyOnLoad)
|
||||
UnityEngine.Object.DontDestroyOnLoad(hostGameObject);
|
||||
|
||||
var services = new ShrinkAppServices();
|
||||
var context = new ShrinkAppContext(host, settings, services);
|
||||
host.Configure(context);
|
||||
|
||||
Host = host;
|
||||
Context = context;
|
||||
host.StartAppAsync().Forget(ex =>
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Debug.LogError("[ShrinkApp] 启动失败: " + ex.Message);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段 2 过渡:由外部宿主(如 ShrinkContext 加载器宿主)提供已初始化的服务容器。
|
||||
/// Host 保持 null——外部宿主不经过 ShrinkAppHost 生命周期,IsRunning 以外部宿主为准。
|
||||
/// </summary>
|
||||
public static void InitializeForExternalHost(ShrinkAppServices services, ShrinkAppSettings? settings = null)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
|
||||
Context = new ShrinkAppContext(null!, settings ?? Settings, services);
|
||||
_externalHostRunning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets a non-<see cref="ShrinkAppHost"/> orchestrator expose its lifecycle through
|
||||
/// the legacy static facade while the migration is in progress.
|
||||
/// </summary>
|
||||
public static void SetExternalHostRunning(bool running)
|
||||
{
|
||||
_externalHostRunning = running;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10523dad09cece4438f29620b3253fdb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class ShrinkAppInstallerRegistryAttribute : Attribute
|
||||
{
|
||||
public ShrinkAppInstallerRegistryAttribute(params Type[] installerTypes)
|
||||
{
|
||||
InstallerTypes = installerTypes ?? Array.Empty<Type>();
|
||||
}
|
||||
|
||||
public Type[] InstallerTypes { get; }
|
||||
}
|
||||
|
||||
internal static class ShrinkAppGeneratedRegistry
|
||||
{
|
||||
public static IReadOnlyList<Type> GetInstallerTypes()
|
||||
{
|
||||
var types = new List<Type>();
|
||||
var seen = new HashSet<Type>();
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
object[] attributes;
|
||||
try
|
||||
{
|
||||
attributes = assembly.GetCustomAttributes(typeof(ShrinkAppInstallerRegistryAttribute), false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attribute in attributes.OfType<ShrinkAppInstallerRegistryAttribute>())
|
||||
{
|
||||
foreach (var installerType in attribute.InstallerTypes ?? Array.Empty<Type>())
|
||||
{
|
||||
if (installerType == null || !seen.Add(installerType))
|
||||
continue;
|
||||
|
||||
types.Add(installerType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1818116c8e85f5e489c0c4955f506ff2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
public sealed class ShrinkAppHost : MonoBehaviour
|
||||
{
|
||||
private readonly List<string> _startedModules = new();
|
||||
private ShrinkAppContext? _context;
|
||||
private bool _unityStarted;
|
||||
private bool _publishedStartedEvent;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool HasFailed { get; private set; }
|
||||
public string LastError { get; private set; } = string.Empty;
|
||||
public IReadOnlyList<string> StartedModules => _startedModules;
|
||||
|
||||
internal void Configure(ShrinkAppContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
internal async UniTask StartAppAsync()
|
||||
{
|
||||
if (IsRunning || HasFailed)
|
||||
return;
|
||||
if (_context == null)
|
||||
throw new InvalidOperationException("ShrinkAppHost is not configured.");
|
||||
|
||||
var installers = CreateInstallerInstances();
|
||||
var orderedInstallers = SortInstallers(installers, _context.Settings);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var installer in orderedInstallers)
|
||||
installer.RegisterServices(_context);
|
||||
|
||||
foreach (var installer in orderedInstallers)
|
||||
{
|
||||
await installer.InitializeAsync(_context);
|
||||
_startedModules.Add(installer.ModuleId);
|
||||
}
|
||||
|
||||
IsRunning = true;
|
||||
PublishStartedEventIfReady();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HasFailed = true;
|
||||
LastError = ex.Message;
|
||||
EventBus.TriggerEvent(new ShrinkAppStartFailedEvent
|
||||
{
|
||||
ErrorMessage = ex.Message,
|
||||
FailedModuleId = _startedModules.LastOrDefault() ?? string.Empty
|
||||
});
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_unityStarted = true;
|
||||
PublishStartedEventIfReady();
|
||||
}
|
||||
|
||||
private void PublishStartedEventIfReady()
|
||||
{
|
||||
if (_publishedStartedEvent || !_unityStarted || !IsRunning)
|
||||
return;
|
||||
|
||||
_publishedStartedEvent = true;
|
||||
EventBus.TriggerEvent(new ShrinkAppStartedEvent
|
||||
{
|
||||
ModuleIds = _startedModules.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IShrinkAppModuleInstaller> CreateInstallerInstances()
|
||||
{
|
||||
var installers = new List<IShrinkAppModuleInstaller>();
|
||||
foreach (var installerType in ShrinkAppGeneratedRegistry.GetInstallerTypes())
|
||||
{
|
||||
if (installerType == null)
|
||||
continue;
|
||||
if (!typeof(IShrinkAppModuleInstaller).IsAssignableFrom(installerType))
|
||||
throw new InvalidOperationException($"Installer type does not implement IShrinkAppModuleInstaller: {installerType.FullName}");
|
||||
if (installerType.IsAbstract)
|
||||
throw new InvalidOperationException($"Installer type cannot be abstract: {installerType.FullName}");
|
||||
|
||||
if (Activator.CreateInstance(installerType) is not IShrinkAppModuleInstaller installer)
|
||||
throw new InvalidOperationException($"Failed to create installer: {installerType.FullName}");
|
||||
|
||||
installers.Add(installer);
|
||||
}
|
||||
|
||||
return installers;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IShrinkAppModuleInstaller> SortInstallers(
|
||||
IReadOnlyList<IShrinkAppModuleInstaller> installers,
|
||||
ShrinkAppSettings settings)
|
||||
{
|
||||
var disabled = new HashSet<string>(
|
||||
settings.disabledModuleIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()) ??
|
||||
Enumerable.Empty<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var enabledInstallers = installers
|
||||
.Where(installer => !disabled.Contains(installer.ModuleId))
|
||||
.ToArray();
|
||||
|
||||
var byId = new Dictionary<string, IShrinkAppModuleInstaller>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var installer in enabledInstallers)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(installer.ModuleId))
|
||||
throw new InvalidOperationException($"Installer {installer.GetType().FullName} has an empty ModuleId.");
|
||||
if (!byId.TryAdd(installer.ModuleId.Trim(), installer))
|
||||
throw new InvalidOperationException($"Duplicate installer ModuleId: {installer.ModuleId}");
|
||||
}
|
||||
|
||||
var visitState = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var ordered = new List<IShrinkAppModuleInstaller>();
|
||||
foreach (var installer in enabledInstallers.OrderBy(item => item.Order).ThenBy(item => item.ModuleId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
Visit(installer, byId, visitState, ordered);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static void Visit(
|
||||
IShrinkAppModuleInstaller installer,
|
||||
IReadOnlyDictionary<string, IShrinkAppModuleInstaller> byId,
|
||||
IDictionary<string, int> visitState,
|
||||
IList<IShrinkAppModuleInstaller> ordered)
|
||||
{
|
||||
var state = visitState.TryGetValue(installer.ModuleId, out var existingState) ? existingState : 0;
|
||||
if (state == 2)
|
||||
return;
|
||||
if (state == 1)
|
||||
throw new InvalidOperationException($"Circular installer dependency detected at module: {installer.ModuleId}");
|
||||
|
||||
visitState[installer.ModuleId] = 1;
|
||||
foreach (var dependencyId in installer.DependsOn ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dependencyId))
|
||||
continue;
|
||||
|
||||
if (!byId.TryGetValue(dependencyId.Trim(), out var dependency))
|
||||
throw new InvalidOperationException(
|
||||
$"Installer dependency missing. Module={installer.ModuleId}, DependsOn={dependencyId}");
|
||||
|
||||
Visit(dependency, byId, visitState, ordered);
|
||||
}
|
||||
|
||||
visitState[installer.ModuleId] = 2;
|
||||
if (!ordered.Contains(installer))
|
||||
ordered.Add(installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df43c81d962edd64cbd214b2697b4179
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
/// <summary>
|
||||
/// 编译期发现的应用模块安装器类型(公共入口)。
|
||||
/// 经典 ShrinkAppHost 与外部宿主(ShrinkContext 加载器宿主)共用同一份发现结果。
|
||||
/// </summary>
|
||||
public static class ShrinkAppInstallers
|
||||
{
|
||||
public static IReadOnlyList<Type> GetDiscoveredInstallerTypes() =>
|
||||
ShrinkAppGeneratedRegistry.GetInstallerTypes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c22f2e857d66bae458ff3dd6d9519d0d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,181 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class ShrinkAppModuleInstallerAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
public interface IShrinkAppModuleInstaller
|
||||
{
|
||||
string ModuleId { get; }
|
||||
int Order { get; }
|
||||
IReadOnlyList<string> DependsOn { get; }
|
||||
void RegisterServices(ShrinkAppContext context);
|
||||
UniTask InitializeAsync(ShrinkAppContext context);
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppContext
|
||||
{
|
||||
internal ShrinkAppContext(ShrinkAppHost host, ShrinkAppSettings settings, ShrinkAppServices services)
|
||||
{
|
||||
Host = host;
|
||||
Settings = settings;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为非 MonoBehaviour 宿主(如 ShrinkContext 纤程适配器)构建上下文。
|
||||
/// Host 为 null:适用于不经过 ShrinkAppHost 启动流的独立编排场景。
|
||||
/// </summary>
|
||||
public static ShrinkAppContext CreateStandalone(ShrinkAppServices services, ShrinkAppSettings? settings = null)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
return new ShrinkAppContext(null!, settings ?? ShrinkAppSettings.Instance, services);
|
||||
}
|
||||
|
||||
public ShrinkAppHost Host { get; }
|
||||
public ShrinkAppSettings Settings { get; }
|
||||
public ShrinkAppServices Services { get; }
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppServices
|
||||
{
|
||||
private readonly Dictionary<Type, object> _services = new();
|
||||
|
||||
public void Register<TService>(TService service) where TService : class
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
_services[typeof(TService)] = service;
|
||||
}
|
||||
|
||||
public bool TryGet<TService>(out TService? service) where TService : class
|
||||
{
|
||||
if (_services.TryGetValue(typeof(TService), out var boxed) && boxed is TService typed)
|
||||
{
|
||||
service = typed;
|
||||
return true;
|
||||
}
|
||||
|
||||
service = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a service only when the current registration is the supplied instance.
|
||||
/// This lets context-driven components expose the legacy service facade as a
|
||||
/// reversible effect without removing a newer replacement during teardown.
|
||||
/// </summary>
|
||||
public bool TryUnregister<TService>(TService service) where TService : class
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
if (!_services.TryGetValue(typeof(TService), out var current) ||
|
||||
!ReferenceEquals(current, service))
|
||||
return false;
|
||||
|
||||
return _services.Remove(typeof(TService));
|
||||
}
|
||||
|
||||
public TService GetRequired<TService>() where TService : class
|
||||
{
|
||||
if (!TryGet<TService>(out var service) || service == null)
|
||||
throw new InvalidOperationException($"Required service is not registered: {typeof(TService).FullName}");
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetRegisteredServiceTypeNames()
|
||||
{
|
||||
return _services.Keys
|
||||
.Select(type => type.FullName ?? type.Name)
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ShrinkApp 的宿主模式(阶段 2 过渡:两种模式并存,按项目选择)。</summary>
|
||||
public enum ShrinkAppHostingMode
|
||||
{
|
||||
/// <summary>经典模式:ShrinkAppHost 在 BeforeSceneLoad 自动创建并按拓扑序启动安装器。</summary>
|
||||
ClassicHost = 0,
|
||||
|
||||
/// <summary>加载器模式:经典宿主让位,由 ShrinkContext 加载器宿主(ShrinkContext.AppAdapter)接管启动,
|
||||
/// 支持运行中按模块 disable/enable 与依赖响应式等待。</summary>
|
||||
ContextLoader = 1,
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "ShrinkAppSettings", menuName = "ShrinkApp/Settings")]
|
||||
public class ShrinkAppSettings : ScriptableObject
|
||||
{
|
||||
private static ShrinkAppSettings? _instance;
|
||||
|
||||
public static ShrinkAppSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance != null)
|
||||
return _instance;
|
||||
|
||||
_instance = Resources.Load<ShrinkAppSettings>("ShrinkAppSettings");
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!_instance)
|
||||
{
|
||||
var guids = UnityEditor.AssetDatabase.FindAssets("t:ShrinkAppSettings");
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
_instance = UnityEditor.AssetDatabase.LoadAssetAtPath<ShrinkAppSettings>(path);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!_instance)
|
||||
{
|
||||
_instance = CreateInstance<ShrinkAppSettings>();
|
||||
Debug.LogWarning(
|
||||
"[ShrinkApp] 未找到 ShrinkAppSettings,当前使用默认配置。建议通过 Assets -> Create -> ShrinkApp -> Settings 创建配置。");
|
||||
}
|
||||
|
||||
return _instance!;
|
||||
}
|
||||
internal set => _instance = value;
|
||||
}
|
||||
|
||||
internal static void ResetCachedInstance()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
public bool dontDestroyOnLoad = true;
|
||||
public bool verboseLogging;
|
||||
public string[] disabledModuleIds = Array.Empty<string>();
|
||||
|
||||
[Tooltip("宿主模式:ClassicHost 为经典自动启动;ContextLoader 由 ShrinkContext 加载器宿主接管(阶段 2 过渡)")]
|
||||
public ShrinkAppHostingMode hostingMode = ShrinkAppHostingMode.ClassicHost;
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppStartedEvent : EventBase
|
||||
{
|
||||
public IReadOnlyList<string> ModuleIds { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppStartFailedEvent : EventBase
|
||||
{
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string FailedModuleId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 384dd878beec6b848b64aa843b464e95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-app-core",
|
||||
"version": "0.1.1",
|
||||
"displayName": "ShrinkApp Core",
|
||||
"description": "Shrink 系列统一宿主层,提供模块安装器、服务容器与统一启动流程。",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-eventbus": "1.1.5",
|
||||
"com.cysharp.unitask": "2.5.10"
|
||||
},
|
||||
"keywords": [
|
||||
"app",
|
||||
"bootstrap",
|
||||
"framework",
|
||||
"host",
|
||||
"starter"
|
||||
],
|
||||
"author": {
|
||||
"name": "cneicy",
|
||||
"url": "https://github.com/cneicy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30fa106725f9afc46af98aff2ee92198
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f69712f8fd5471e46b41e82204b5e845
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkApp.Starter.Basic` 的包内变更。
|
||||
|
||||
## [0.1.0] - 2026-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- 新增最小起盘 Starter 包。
|
||||
- 新增场景生成器 `ShrinkAppBasicStarterGenerator`。
|
||||
- 新增示例控制器 `ShrinkAppBasicSampleController`。
|
||||
- 新增 `ShrinkAppBasicDemoRuntime`,补齐默认命令源、状态命令与最小 loopback 网络示例。
|
||||
- 新增 `ShrinkAppBasicStarterSettings`,提供最小 Starter 级别的默认配置资产。
|
||||
- 示例场景继续扩展为小型调试台,补齐命令输入、输出日志、模块/服务/存档/命令/网络状态面板。
|
||||
- 状态面板进一步细化为已启动模块、已注册服务、存档槽位、命令列表、网络诊断五块只读视图。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3e78d4720b16b84fbf0d766b0a0122b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6be676789111fd4a9afd67b011a12c9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "ShrinkApp.Starter.Basic.Editor",
|
||||
"rootNamespace": "ShrinkApp.Starter.Basic.Editor",
|
||||
"references": [
|
||||
"ShrinkApp.Starter.Basic.Runtime",
|
||||
"ShrinkApp.Core.Runtime",
|
||||
"ShrinkApp.Core.Editor",
|
||||
"ShrinkDataSaver.Runtime",
|
||||
"ShrinkDataSaver.Integration.App",
|
||||
"UnityEngine.UI"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa988409913004043b6cbe3d19de32bd
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,215 @@
|
||||
#if UNITY_EDITOR
|
||||
using ShrinkApp.Editor;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ShrinkApp.Starter.Basic.Editor
|
||||
{
|
||||
public static class ShrinkAppBasicStarterGenerator
|
||||
{
|
||||
private const string MenuPath = "ShrinkApp/Starter/生成 Basic Entry 场景";
|
||||
|
||||
[MenuItem(MenuPath)]
|
||||
public static void Generate()
|
||||
{
|
||||
EnsureAssets();
|
||||
BuildScene();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log("[ShrinkApp.Starter.Basic] 已生成 Basic Entry 场景与配置。");
|
||||
}
|
||||
|
||||
private static void EnsureAssets()
|
||||
{
|
||||
ShrinkAppEditorMenu.CreateSettingsAsset();
|
||||
|
||||
const string resourcesDir = "Assets/Resources";
|
||||
if (!AssetDatabase.IsValidFolder(resourcesDir))
|
||||
AssetDatabase.CreateFolder("Assets", "Resources");
|
||||
|
||||
const string dataSaverSettingsPath = "Assets/Resources/ShrinkDataSaverSettings.asset";
|
||||
if (AssetDatabase.LoadAssetAtPath<ScriptableObject>(dataSaverSettingsPath) == null)
|
||||
{
|
||||
var asset = ScriptableObject.CreateInstance("ShrinkDataSaverSettings");
|
||||
if (asset == null)
|
||||
throw new System.InvalidOperationException(
|
||||
"Cannot create ShrinkDataSaverSettings. Ensure ShrinkDataSaver.Runtime is imported and compiled.");
|
||||
|
||||
AssetDatabase.CreateAsset(asset, dataSaverSettingsPath);
|
||||
}
|
||||
|
||||
const string starterSettingsPath = "Assets/Resources/ShrinkAppBasicStarterSettings.asset";
|
||||
if (AssetDatabase.LoadAssetAtPath<ShrinkAppBasicStarterSettings>(starterSettingsPath) == null)
|
||||
{
|
||||
var starterAsset = ScriptableObject.CreateInstance<ShrinkAppBasicStarterSettings>();
|
||||
starterAsset.autoBindLoopbackTransport = true;
|
||||
starterAsset.defaultCommandText = "starter status";
|
||||
starterAsset.defaultLoopbackSessionId = 1;
|
||||
starterAsset.logCommandOutputToConsole = true;
|
||||
starterAsset.maxCommandOutputLines = 12;
|
||||
AssetDatabase.CreateAsset(starterAsset, starterSettingsPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildScene()
|
||||
{
|
||||
const string scenesDir = "Assets/Scenes";
|
||||
if (!AssetDatabase.IsValidFolder(scenesDir))
|
||||
AssetDatabase.CreateFolder("Assets", "Scenes");
|
||||
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
|
||||
new GameObject("Main Camera", typeof(Camera));
|
||||
new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem),
|
||||
typeof(UnityEngine.EventSystems.StandaloneInputModule));
|
||||
|
||||
var canvasGo = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = canvasGo.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var scaler = canvasGo.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
|
||||
var panelGo = new GameObject("Panel", typeof(RectTransform), typeof(Image));
|
||||
panelGo.transform.SetParent(canvasGo.transform, false);
|
||||
var panelRect = panelGo.GetComponent<RectTransform>();
|
||||
panelRect.sizeDelta = new Vector2(980f, 720f);
|
||||
panelRect.anchoredPosition = Vector2.zero;
|
||||
panelGo.GetComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f, 0.92f);
|
||||
|
||||
var statusText = CreateText("StatusText", panelGo.transform, new Vector2(0f, 240f), new Vector2(840f, 120f),
|
||||
"ShrinkApp Basic starter ready.");
|
||||
statusText.alignment = TextAnchor.MiddleCenter;
|
||||
var detailText = CreateText("DetailText", panelGo.transform, new Vector2(0f, 135f), new Vector2(840f, 140f),
|
||||
"app=False, save=False, cmd=False, net=False");
|
||||
detailText.alignment = TextAnchor.UpperLeft;
|
||||
detailText.fontSize = 20;
|
||||
var commandInputField = CreateInputField("CommandInput", panelGo.transform, new Vector2(0f, 20f),
|
||||
new Vector2(520f, 54f), "starter status");
|
||||
var outputText = CreateText("OutputText", panelGo.transform, new Vector2(0f, -250f), new Vector2(840f, 180f),
|
||||
string.Empty);
|
||||
outputText.alignment = TextAnchor.UpperLeft;
|
||||
outputText.fontSize = 20;
|
||||
outputText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
outputText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
var saveMetaText = CreateText("SaveMetaText", panelGo.transform, new Vector2(-280f, -430f), new Vector2(240f, 150f),
|
||||
"No save slots found.");
|
||||
saveMetaText.alignment = TextAnchor.UpperLeft;
|
||||
saveMetaText.fontSize = 18;
|
||||
var moduleText = CreateText("ModuleText", panelGo.transform, new Vector2(0f, -430f), new Vector2(240f, 150f),
|
||||
"No started modules.");
|
||||
moduleText.alignment = TextAnchor.UpperLeft;
|
||||
moduleText.fontSize = 18;
|
||||
var serviceText = CreateText("ServiceText", panelGo.transform, new Vector2(280f, -430f), new Vector2(240f, 150f),
|
||||
"No registered services.");
|
||||
serviceText.alignment = TextAnchor.UpperLeft;
|
||||
serviceText.fontSize = 18;
|
||||
var commandListText = CreateText("CommandListText", panelGo.transform, new Vector2(-280f, -600f), new Vector2(240f, 150f),
|
||||
"Command service not ready.");
|
||||
commandListText.alignment = TextAnchor.UpperLeft;
|
||||
commandListText.fontSize = 18;
|
||||
var networkText = CreateText("NetworkText", panelGo.transform, new Vector2(280f, -600f), new Vector2(240f, 150f),
|
||||
"Network service not ready.");
|
||||
networkText.alignment = TextAnchor.UpperLeft;
|
||||
networkText.fontSize = 18;
|
||||
|
||||
var saveButton = CreateButton("SaveButton", panelGo.transform, new Vector2(-250f, -70f), "Save Slot 0");
|
||||
var loadButton = CreateButton("LoadButton", panelGo.transform, new Vector2(0f, -70f), "Load Slot 0");
|
||||
var commandButton = CreateButton("CommandButton", panelGo.transform, new Vector2(250f, -70f), "Run Command");
|
||||
var loopbackButton = CreateButton("LoopbackButton", panelGo.transform, new Vector2(-250f, -165f), "Bind Loopback");
|
||||
var disconnectButton = CreateButton("DisconnectLoopbackButton", panelGo.transform, new Vector2(0f, -165f), "Disconnect Loopback");
|
||||
var refreshButton = CreateButton("RefreshButton", panelGo.transform, new Vector2(250f, -165f), "Refresh Status");
|
||||
var clearOutputButton = CreateButton("ClearOutputButton", panelGo.transform, new Vector2(0f, -345f), "Clear Output");
|
||||
|
||||
var controller = panelGo.AddComponent<ShrinkAppBasicSampleController>();
|
||||
var serializedObject = new SerializedObject(controller);
|
||||
serializedObject.FindProperty("statusText").objectReferenceValue = statusText;
|
||||
serializedObject.FindProperty("detailText").objectReferenceValue = detailText;
|
||||
serializedObject.FindProperty("outputText").objectReferenceValue = outputText;
|
||||
serializedObject.FindProperty("moduleText").objectReferenceValue = moduleText;
|
||||
serializedObject.FindProperty("serviceText").objectReferenceValue = serviceText;
|
||||
serializedObject.FindProperty("saveMetaText").objectReferenceValue = saveMetaText;
|
||||
serializedObject.FindProperty("commandListText").objectReferenceValue = commandListText;
|
||||
serializedObject.FindProperty("networkText").objectReferenceValue = networkText;
|
||||
serializedObject.FindProperty("saveButton").objectReferenceValue = saveButton;
|
||||
serializedObject.FindProperty("loadButton").objectReferenceValue = loadButton;
|
||||
serializedObject.FindProperty("commandButton").objectReferenceValue = commandButton;
|
||||
serializedObject.FindProperty("loopbackButton").objectReferenceValue = loopbackButton;
|
||||
serializedObject.FindProperty("refreshButton").objectReferenceValue = refreshButton;
|
||||
serializedObject.FindProperty("clearOutputButton").objectReferenceValue = clearOutputButton;
|
||||
serializedObject.FindProperty("disconnectLoopbackButton").objectReferenceValue = disconnectButton;
|
||||
serializedObject.FindProperty("commandInputField").objectReferenceValue = commandInputField;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
EditorSceneManager.SaveScene(scene, "Assets/Scenes/ShrinkAppEntry.unity");
|
||||
}
|
||||
|
||||
private static Text CreateText(string name, Transform parent, Vector2 anchoredPosition, Vector2 size, string text)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Text));
|
||||
go.transform.SetParent(parent, false);
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = size;
|
||||
rect.anchoredPosition = anchoredPosition;
|
||||
var label = go.GetComponent<Text>();
|
||||
label.text = text;
|
||||
label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
label.fontSize = 28;
|
||||
label.color = Color.white;
|
||||
return label;
|
||||
}
|
||||
|
||||
private static Button CreateButton(string name, Transform parent, Vector2 anchoredPosition, string labelText)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
go.transform.SetParent(parent, false);
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(220f, 72f);
|
||||
rect.anchoredPosition = anchoredPosition;
|
||||
go.GetComponent<Image>().color = new Color(0.24f, 0.45f, 0.75f, 1f);
|
||||
|
||||
var label = CreateText("Label", go.transform, Vector2.zero, new Vector2(220f, 72f), labelText);
|
||||
label.alignment = TextAnchor.MiddleCenter;
|
||||
label.resizeTextForBestFit = true;
|
||||
label.resizeTextMinSize = 18;
|
||||
label.resizeTextMaxSize = 28;
|
||||
|
||||
return go.GetComponent<Button>();
|
||||
}
|
||||
|
||||
private static InputField CreateInputField(string name, Transform parent, Vector2 anchoredPosition, Vector2 size, string defaultText)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(InputField));
|
||||
go.transform.SetParent(parent, false);
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = size;
|
||||
rect.anchoredPosition = anchoredPosition;
|
||||
go.GetComponent<Image>().color = new Color(0.08f, 0.1f, 0.14f, 1f);
|
||||
|
||||
var text = CreateText("Text", go.transform, Vector2.zero, size, defaultText);
|
||||
text.alignment = TextAnchor.MiddleLeft;
|
||||
text.fontSize = 24;
|
||||
text.color = Color.white;
|
||||
var textRect = text.GetComponent<RectTransform>();
|
||||
textRect.offsetMin = new Vector2(16f, 8f);
|
||||
textRect.offsetMax = new Vector2(-16f, -8f);
|
||||
|
||||
var placeholder = CreateText("Placeholder", go.transform, Vector2.zero, size, "Enter command...");
|
||||
placeholder.alignment = TextAnchor.MiddleLeft;
|
||||
placeholder.fontSize = 24;
|
||||
placeholder.color = new Color(1f, 1f, 1f, 0.35f);
|
||||
var placeholderRect = placeholder.GetComponent<RectTransform>();
|
||||
placeholderRect.offsetMin = new Vector2(16f, 8f);
|
||||
placeholderRect.offsetMax = new Vector2(-16f, -8f);
|
||||
|
||||
var inputField = go.GetComponent<InputField>();
|
||||
inputField.textComponent = text;
|
||||
inputField.placeholder = placeholder;
|
||||
inputField.text = defaultText;
|
||||
return inputField;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02316ba70f6e08249b09f5b7ff63eb93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
# ShrinkApp.Starter.Basic
|
||||
|
||||
`ShrinkApp.Starter.Basic` 是 `ShrinkApp` 的最小起盘 Starter。目标不是提供完整游戏模板,而是让项目在导入最小基础包后,能快速得到一个可运行的入口场景、基础配置和最小示例流程。
|
||||
|
||||
在 `ShrinkAppSettings.hostingMode = ContextLoader` 时,Starter 组合根会把 Command、DataSaver、Network 三个已发现模块替换为原生 `IShrinkComponent`,并把兼容用的服务门面作为可逆效应挂回 `ShrinkApp.Services`。因此 Starter 调试台仍可使用原来的服务 API,同时模块 disable/enable 由 `ShrinkContextLoader` 增量协调。
|
||||
|
||||
## 当前能力
|
||||
|
||||
- 生成 `ShrinkAppSettings.asset`
|
||||
- 生成或补齐 `ShrinkDataSaverSettings.asset`
|
||||
- 生成 `ShrinkAppBasicStarterSettings.asset`
|
||||
- 生成 `Assets/Scenes/ShrinkAppEntry.unity`
|
||||
- 生成最小示例 UI 与 `ShrinkAppBasicSampleController`
|
||||
- 演示 `ShrinkApp + ShrinkDataSaver + Command + Network` 的最小闭环
|
||||
|
||||
当前示例场景内置的最小入口已经扩成一个小型调试台,包括:
|
||||
|
||||
- `Save Slot 0`
|
||||
- `Load Slot 0`
|
||||
- `Run starter status`
|
||||
- `Bind Loopback`
|
||||
- `Disconnect Loopback`
|
||||
- `Refresh Status`
|
||||
- `Clear Output`
|
||||
- 命令输入框
|
||||
- 输出日志区
|
||||
- 模块/服务/存档/命令/网络五块只读状态面板
|
||||
|
||||
当前五块只读状态面板分别展示:
|
||||
|
||||
- 已启动模块列表
|
||||
- 已注册服务类型列表
|
||||
- 存档槽位元数据概览
|
||||
- 当前已注册命令列表
|
||||
- 网络诊断摘要与 session 概览
|
||||
|
||||
同时会生成一份 Starter 自己的配置资产,用于控制:
|
||||
|
||||
- 是否在启动时自动绑定 loopback transport
|
||||
- 默认命令文本
|
||||
- 默认 loopback session id
|
||||
- 是否把命令输出打印到控制台
|
||||
- 启动时是否自动执行默认命令
|
||||
- 命令输出区最多保留多少行
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a8145b76d3e3b445b3768d897fe21c0
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfffe5a1b320812469b0c0f7a81e790b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "ShrinkApp.Starter.Basic.Runtime",
|
||||
"rootNamespace": "ShrinkApp.Starter.Basic",
|
||||
"references": [
|
||||
"ShrinkApp.Core.Runtime",
|
||||
"ShrinkContext.AppAdapter.Runtime",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkCommand.Runtime",
|
||||
"ShrinkCommand.Integration.App",
|
||||
"ShrinkDataSaver.Integration.App",
|
||||
"ShrinkDataSaver.Runtime",
|
||||
"ShrinkNetwork.Runtime",
|
||||
"ShrinkNetwork.Integration.App",
|
||||
"ShrinkCommand.Integration.EventBus",
|
||||
"ShrinkDataSaver.Integration.EventBus",
|
||||
"ShrinkNetwork.Integration.EventBus",
|
||||
"ShrinkCommand.Integration.Network",
|
||||
"UnityEngine.UI",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10b7cf71574bd904594339a0824e028b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
#nullable enable
|
||||
|
||||
using ShrinkCommand.Integration.App;
|
||||
using ShrinkCommand.Integration;
|
||||
using ShrinkContext.AppAdapter;
|
||||
using ShrinkDataSaver.Integration.App;
|
||||
using ShrinkDataSaver.Integration;
|
||||
using ShrinkNetwork.Integration.App;
|
||||
using ShrinkNetwork.Integration;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp.Starter.Basic
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic Starter 的组合根:ContextLoader 模式下默认使用原生 Cordis 组件,
|
||||
/// 不再把三个功能模块交给 IShrinkAppModuleInstaller 包装器执行。
|
||||
/// </summary>
|
||||
public static class ShrinkAppBasicContextComposition
|
||||
{
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void RegisterDefaultComposition()
|
||||
{
|
||||
ShrinkAppLoaderBootstrapper.DefaultComposition = Configure;
|
||||
}
|
||||
|
||||
public static void Configure(ShrinkAppLoaderHost host)
|
||||
{
|
||||
if (host == null)
|
||||
throw new System.ArgumentNullException(nameof(host));
|
||||
|
||||
host.OverrideModuleComponent("shrink.network", static () => new ShrinkNetworkAppComponent());
|
||||
host.OverrideModuleComponent("shrink.command", static () => new ShrinkCommandAppComponent());
|
||||
host.OverrideModuleComponent("shrink.datasaver", static () => new ShrinkDataSaverAppComponent());
|
||||
host.AddModuleComponent("shrink.integration.command-network",
|
||||
static () => new ShrinkCommandNetworkIntegrationComponent());
|
||||
host.AddModuleComponent("shrink.integration.command-eventbus",
|
||||
static () => new ShrinkCommandEventBusComponent());
|
||||
host.AddModuleComponent("shrink.integration.datasaver-eventbus",
|
||||
static () => new ShrinkDataSaverEventBusComponent());
|
||||
host.AddModuleComponent("shrink.integration.network-eventbus",
|
||||
static () => new ShrinkNetworkEventBusComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b07e8ba4e0ea394d929c688f6f4fcc4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,214 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkCommand;
|
||||
using ShrinkCommand.Integration.App;
|
||||
using ShrinkContext.AppAdapter;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkNetwork.Integration.App;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp.Starter.Basic
|
||||
{
|
||||
public sealed class ShrinkAppBasicCommandSource : IShrinkCommandSource
|
||||
{
|
||||
private readonly Action<string>? _sink;
|
||||
private readonly bool _logToConsole;
|
||||
|
||||
public ShrinkAppBasicCommandSource(Action<string>? sink = null, bool logToConsole = true)
|
||||
{
|
||||
_sink = sink;
|
||||
_logToConsole = logToConsole;
|
||||
}
|
||||
|
||||
public string SourceId => "starter.basic.console";
|
||||
public string DisplayName => "StarterBasicConsole";
|
||||
public bool IsConsole => true;
|
||||
|
||||
public bool HasPermission(string permission) => true;
|
||||
|
||||
public UniTask WriteLineAsync(string message, System.Threading.CancellationToken cancellationToken = default)
|
||||
{
|
||||
_sink?.Invoke(message);
|
||||
if (_logToConsole)
|
||||
Debug.Log("[ShrinkApp.Basic.Command] " + message);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class ShrinkAppBasicDemoCommands
|
||||
{
|
||||
[ShrinkCommand("starter status", Description = "显示 Starter.Basic 当前状态")]
|
||||
private static string StarterStatus()
|
||||
{
|
||||
var appRunning = ShrinkApp.IsRunning;
|
||||
var loadedSlot = ShrinkDataSaver.ShrinkSave.LoadedSlot;
|
||||
return $"appRunning={appRunning}, loadedSlot={loadedSlot}";
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShrinkAppBasicDemoRuntime
|
||||
{
|
||||
private const string SettingsResourceName = "ShrinkAppBasicStarterSettings";
|
||||
private static ShrinkLoopbackTransport? _loopbackClientTransport;
|
||||
private static ShrinkLoopbackTransport? _loopbackServerTransport;
|
||||
private static long _activeLoopbackSessionId = -1;
|
||||
|
||||
public static ShrinkAppBasicStarterSettings GetOrCreateSettings()
|
||||
{
|
||||
var asset = Resources.Load<ShrinkAppBasicStarterSettings>(SettingsResourceName);
|
||||
if (asset != null)
|
||||
return asset;
|
||||
|
||||
var fallback = ScriptableObject.CreateInstance<ShrinkAppBasicStarterSettings>();
|
||||
fallback.defaultCommandText = "starter status";
|
||||
fallback.defaultLoopbackSessionId = 1;
|
||||
fallback.logCommandOutputToConsole = true;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static string BuildStatusSummary()
|
||||
{
|
||||
var commandReady = ShrinkApp.Context?.Services.TryGet<ShrinkCommandAppService>(out _) == true;
|
||||
var networkReady = ShrinkApp.Context?.Services.TryGet<ShrinkNetworkAppService>(out _) == true;
|
||||
var dataSaverReady = ShrinkApp.Context?.Services.TryGet<ShrinkDataSaver.Integration.App.ShrinkDataSaverService>(out _) == true;
|
||||
var appRunning = ShrinkApp.IsRunning || ShrinkAppLoaderBootstrapper.Instance?.Host.IsRunning == true;
|
||||
return $"app={appRunning}, save={dataSaverReady}, cmd={commandReady}, net={networkReady}, loopbackSession={_activeLoopbackSessionId}";
|
||||
}
|
||||
|
||||
public static string BuildDetailedStatus()
|
||||
{
|
||||
var settings = GetOrCreateSettings();
|
||||
var recentSlot = ShrinkDataSaver.ShrinkSave.GetRecentSlotIndex();
|
||||
var commandService = ShrinkApp.Context?.Services.TryGet<ShrinkCommandAppService>(out var command) == true ? command : null;
|
||||
var networkService = ShrinkApp.Context?.Services.TryGet<ShrinkNetworkAppService>(out var network) == true ? network : null;
|
||||
var diagnostics = networkService?.GetDiagnosticsSnapshot();
|
||||
var moduleCount = ShrinkAppLoaderBootstrapper.Instance?.Host.ActiveModuleIds.Count
|
||||
?? ShrinkApp.Host?.StartedModules.Count
|
||||
?? 0;
|
||||
var serviceCount = ShrinkApp.Context?.Services.GetRegisteredServiceTypeNames().Count ?? 0;
|
||||
return
|
||||
$"defaultCommand={settings.defaultCommandText}\n" +
|
||||
$"loopbackAutoBind={settings.autoBindLoopbackTransport}, sessionId={settings.defaultLoopbackSessionId}\n" +
|
||||
$"recentSlot={recentSlot}, loadedSlot={ShrinkDataSaver.ShrinkSave.LoadedSlot}\n" +
|
||||
$"modules={moduleCount}, services={serviceCount}, commandCount={commandService?.Commands.Count ?? 0}, currentSessions={diagnostics?.CurrentSessions ?? 0}\n" +
|
||||
$"{BuildStatusSummary()}";
|
||||
}
|
||||
|
||||
public static string BuildModuleSummary()
|
||||
{
|
||||
var loaderHost = ShrinkAppLoaderBootstrapper.Instance?.Host;
|
||||
if (loaderHost != null && loaderHost.ActiveModuleIds.Count > 0)
|
||||
return string.Join("\n", loaderHost.ActiveModuleIds);
|
||||
|
||||
if (ShrinkApp.Host?.StartedModules == null || ShrinkApp.Host.StartedModules.Count == 0)
|
||||
return "No started modules.";
|
||||
|
||||
return string.Join("\n", ShrinkApp.Host.StartedModules);
|
||||
}
|
||||
|
||||
public static string BuildServiceSummary()
|
||||
{
|
||||
if (ShrinkApp.Context == null)
|
||||
return "No registered services.";
|
||||
|
||||
return string.Join("\n", ShrinkApp.Context.Services.GetRegisteredServiceTypeNames());
|
||||
}
|
||||
|
||||
public static string BuildSaveMetaSummary(ShrinkDataSaver.SaveMeta[]? metas)
|
||||
{
|
||||
if (metas == null || metas.Length == 0)
|
||||
return "No save slots found.";
|
||||
|
||||
return string.Join("\n", metas
|
||||
.OrderBy(meta => meta.SlotIndex)
|
||||
.Select(meta =>
|
||||
$"slot={meta.SlotIndex}, name={meta.SlotName}, playtime={meta.PlaytimeSeconds:F0}s, modified={meta.LastModifiedTime:g}"));
|
||||
}
|
||||
|
||||
public static string BuildCommandListSummary()
|
||||
{
|
||||
if (ShrinkApp.Context?.Services.TryGet<ShrinkCommandAppService>(out var commandService) != true || commandService == null)
|
||||
return "Command service not ready.";
|
||||
|
||||
return string.Join("\n", commandService.Commands
|
||||
.OrderBy(command => command.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.Take(10)
|
||||
.Select(command => command.Path));
|
||||
}
|
||||
|
||||
public static string BuildNetworkSummary()
|
||||
{
|
||||
if (ShrinkApp.Context?.Services.TryGet<ShrinkNetworkAppService>(out var networkService) != true || networkService == null)
|
||||
return "Network service not ready.";
|
||||
|
||||
var diagnostics = networkService.GetDiagnosticsSnapshot();
|
||||
var sessions = networkService.GetSessionSummaries();
|
||||
return
|
||||
$"sessions={diagnostics.CurrentSessions}, connected={diagnostics.SessionsConnected}, disconnected={diagnostics.SessionsDisconnected}\n" +
|
||||
$"packetsSent={diagnostics.PacketsSent}, packetsReceived={diagnostics.PacketsReceived}\n" +
|
||||
$"rpcStarted={diagnostics.RpcStarted}, rpcCompleted={diagnostics.RpcCompleted}\n" +
|
||||
string.Join("\n", sessions);
|
||||
}
|
||||
|
||||
public static async UniTask<string> ExecuteCommandAsync(string? commandText, Action<string>? outputSink = null)
|
||||
{
|
||||
var command = ShrinkApp.Services.GetRequired<ShrinkCommandAppService>();
|
||||
var settings = GetOrCreateSettings();
|
||||
var source = new ShrinkAppBasicCommandSource(outputSink, settings.logCommandOutputToConsole);
|
||||
var effectiveCommand = string.IsNullOrWhiteSpace(commandText)
|
||||
? settings.defaultCommandText
|
||||
: commandText.Trim();
|
||||
var result = await command.ExecuteAsync(source, effectiveCommand);
|
||||
if (!string.IsNullOrWhiteSpace(result.Message))
|
||||
outputSink?.Invoke(result.Message);
|
||||
return result.Message;
|
||||
}
|
||||
|
||||
public static string EnsureLoopbackReady()
|
||||
{
|
||||
var network = ShrinkApp.Services.GetRequired<ShrinkNetworkAppService>().Service;
|
||||
var settings = GetOrCreateSettings();
|
||||
if (_loopbackClientTransport == null || _loopbackServerTransport == null)
|
||||
{
|
||||
_loopbackClientTransport = new ShrinkLoopbackTransport();
|
||||
_loopbackServerTransport = new ShrinkLoopbackTransport();
|
||||
_loopbackClientTransport.LinkPeer(_loopbackServerTransport);
|
||||
}
|
||||
|
||||
network.BindTransport(_loopbackClientTransport);
|
||||
if (_activeLoopbackSessionId != settings.defaultLoopbackSessionId)
|
||||
{
|
||||
if (_activeLoopbackSessionId >= 0)
|
||||
_loopbackClientTransport.CloseSession(_activeLoopbackSessionId);
|
||||
|
||||
_activeLoopbackSessionId = settings.defaultLoopbackSessionId;
|
||||
_loopbackClientTransport.OpenSession(_activeLoopbackSessionId);
|
||||
}
|
||||
|
||||
return $"loopbackBound={network.Sessions.Count}, session={_activeLoopbackSessionId}";
|
||||
}
|
||||
|
||||
public static string AutoBindLoopbackIfEnabled()
|
||||
{
|
||||
var settings = GetOrCreateSettings();
|
||||
if (!settings.autoBindLoopbackTransport)
|
||||
return string.Empty;
|
||||
|
||||
return EnsureLoopbackReady();
|
||||
}
|
||||
|
||||
public static string DisconnectLoopback()
|
||||
{
|
||||
if (_loopbackClientTransport == null || _activeLoopbackSessionId < 0)
|
||||
return "loopbackNotBound";
|
||||
|
||||
_loopbackClientTransport.CloseSession(_activeLoopbackSessionId);
|
||||
_activeLoopbackSessionId = -1;
|
||||
return "loopbackDisconnected";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21ca7aa1ab7f9c64e9b4d050458df3d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,173 @@
|
||||
#nullable enable
|
||||
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkDataSaver;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ShrinkApp.Starter.Basic
|
||||
{
|
||||
public sealed class ShrinkAppBasicSampleController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Text? statusText;
|
||||
[SerializeField] private Text? detailText;
|
||||
[SerializeField] private Text? outputText;
|
||||
[SerializeField] private Text? moduleText;
|
||||
[SerializeField] private Text? serviceText;
|
||||
[SerializeField] private Text? saveMetaText;
|
||||
[SerializeField] private Text? commandListText;
|
||||
[SerializeField] private Text? networkText;
|
||||
[SerializeField] private Button? saveButton;
|
||||
[SerializeField] private Button? loadButton;
|
||||
[SerializeField] private Button? commandButton;
|
||||
[SerializeField] private Button? loopbackButton;
|
||||
[SerializeField] private Button? refreshButton;
|
||||
[SerializeField] private Button? clearOutputButton;
|
||||
[SerializeField] private Button? disconnectLoopbackButton;
|
||||
[SerializeField] private InputField? commandInputField;
|
||||
|
||||
private int _counter;
|
||||
private readonly List<string> _outputLines = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (saveButton != null)
|
||||
saveButton.onClick.AddListener(() => SaveAsync().Forget());
|
||||
if (loadButton != null)
|
||||
loadButton.onClick.AddListener(() => LoadAsync().Forget());
|
||||
if (commandButton != null)
|
||||
commandButton.onClick.AddListener(() => RunCommandAsync().Forget());
|
||||
if (loopbackButton != null)
|
||||
loopbackButton.onClick.AddListener(BindLoopback);
|
||||
if (refreshButton != null)
|
||||
refreshButton.onClick.AddListener(RefreshPanels);
|
||||
if (clearOutputButton != null)
|
||||
clearOutputButton.onClick.AddListener(ClearOutput);
|
||||
if (disconnectLoopbackButton != null)
|
||||
disconnectLoopbackButton.onClick.AddListener(DisconnectLoopback);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
var settings = ShrinkAppBasicDemoRuntime.GetOrCreateSettings();
|
||||
if (commandInputField != null)
|
||||
commandInputField.text = settings.defaultCommandText;
|
||||
|
||||
var autoLoopbackMessage = ShrinkAppBasicDemoRuntime.AutoBindLoopbackIfEnabled();
|
||||
RefreshStatus("ShrinkApp Basic starter ready.");
|
||||
if (!string.IsNullOrWhiteSpace(autoLoopbackMessage))
|
||||
AppendOutput(autoLoopbackMessage);
|
||||
RefreshPanels();
|
||||
|
||||
if (settings.autoRunDefaultCommandOnStart)
|
||||
RunCommandAsync().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid SaveAsync()
|
||||
{
|
||||
_counter++;
|
||||
ShrinkSettings.Set("ShrinkApp.Basic.Counter", _counter);
|
||||
await ShrinkSave.SaveSlotAsync(0);
|
||||
RefreshStatus($"Saved counter={_counter}, recentSlot={ShrinkSave.GetRecentSlotIndex()}");
|
||||
AppendOutput("save: slot 0");
|
||||
RefreshPanels();
|
||||
}
|
||||
|
||||
private async UniTaskVoid LoadAsync()
|
||||
{
|
||||
if (await ShrinkSave.SlotExistsAsync(0))
|
||||
await ShrinkSave.LoadSlotAsync(0);
|
||||
|
||||
_counter = ShrinkSettings.Get("ShrinkApp.Basic.Counter", 0);
|
||||
RefreshStatus($"Loaded counter={_counter}, loadedSlot={ShrinkSave.LoadedSlot}");
|
||||
AppendOutput("load: slot 0");
|
||||
RefreshPanels();
|
||||
}
|
||||
|
||||
private async UniTaskVoid RunCommandAsync()
|
||||
{
|
||||
var message = await ShrinkAppBasicDemoRuntime.ExecuteCommandAsync(commandInputField?.text, AppendOutput);
|
||||
RefreshStatus(message);
|
||||
RefreshPanels();
|
||||
}
|
||||
|
||||
private void BindLoopback()
|
||||
{
|
||||
var message = ShrinkAppBasicDemoRuntime.EnsureLoopbackReady();
|
||||
AppendOutput(message);
|
||||
RefreshStatus(message);
|
||||
RefreshPanels();
|
||||
}
|
||||
|
||||
private void DisconnectLoopback()
|
||||
{
|
||||
var message = ShrinkAppBasicDemoRuntime.DisconnectLoopback();
|
||||
AppendOutput(message);
|
||||
RefreshStatus(message);
|
||||
RefreshPanels();
|
||||
}
|
||||
|
||||
private void RefreshPanels()
|
||||
{
|
||||
RefreshDetail(ShrinkAppBasicDemoRuntime.BuildDetailedStatus());
|
||||
RefreshInspectorText().Forget();
|
||||
}
|
||||
|
||||
private void RefreshStatus(string text)
|
||||
{
|
||||
if (statusText != null)
|
||||
statusText.text = text;
|
||||
|
||||
Debug.Log("[ShrinkApp.Starter.Basic] " + text);
|
||||
}
|
||||
|
||||
private void RefreshDetail(string text)
|
||||
{
|
||||
if (detailText != null)
|
||||
detailText.text = text;
|
||||
}
|
||||
|
||||
private void AppendOutput(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
var settings = ShrinkAppBasicDemoRuntime.GetOrCreateSettings();
|
||||
_outputLines.Add(text.Trim());
|
||||
while (_outputLines.Count > settings.maxCommandOutputLines)
|
||||
_outputLines.RemoveAt(0);
|
||||
|
||||
if (outputText != null)
|
||||
outputText.text = string.Join("\n", _outputLines);
|
||||
}
|
||||
|
||||
private void ClearOutput()
|
||||
{
|
||||
_outputLines.Clear();
|
||||
if (outputText != null)
|
||||
outputText.text = string.Empty;
|
||||
}
|
||||
|
||||
private async UniTaskVoid RefreshInspectorText()
|
||||
{
|
||||
if (saveMetaText != null)
|
||||
{
|
||||
var metas = await ShrinkSave.GetAllMetaAsync();
|
||||
saveMetaText.text = ShrinkAppBasicDemoRuntime.BuildSaveMetaSummary(metas);
|
||||
}
|
||||
|
||||
if (moduleText != null)
|
||||
moduleText.text = ShrinkAppBasicDemoRuntime.BuildModuleSummary();
|
||||
|
||||
if (serviceText != null)
|
||||
serviceText.text = ShrinkAppBasicDemoRuntime.BuildServiceSummary();
|
||||
|
||||
if (commandListText != null)
|
||||
commandListText.text = ShrinkAppBasicDemoRuntime.BuildCommandListSummary();
|
||||
|
||||
if (networkText != null)
|
||||
networkText.text = ShrinkAppBasicDemoRuntime.BuildNetworkSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bc64b7d684d71f469b2335a8c4269ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp.Starter.Basic
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ShrinkAppBasicStarterSettings", menuName = "ShrinkApp/Starter/Basic Settings")]
|
||||
public class ShrinkAppBasicStarterSettings : ScriptableObject
|
||||
{
|
||||
public bool autoBindLoopbackTransport;
|
||||
public long defaultLoopbackSessionId = 1;
|
||||
public string defaultCommandText = "starter status";
|
||||
public bool logCommandOutputToConsole = true;
|
||||
public bool autoRunDefaultCommandOnStart;
|
||||
public int maxCommandOutputLines = 12;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a4a0f6f7946adc45a3437ca04698c65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-app-starter-basic",
|
||||
"version": "0.1.0",
|
||||
"displayName": "ShrinkApp Starter Basic",
|
||||
"description": "ShrinkApp 最小起盘 Starter,提供向导、入口场景和 DataSaver 示例。",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-app-core": "0.1.0",
|
||||
"com.cneicy.shrink-context-core": "0.1.0",
|
||||
"com.cneicy.shrink-context-app-adapter": "0.1.0",
|
||||
"com.cneicy.shrink-command": "0.2.0",
|
||||
"com.cneicy.shrink-command-integration-app": "0.1.0",
|
||||
"com.cneicy.shrink-datasaver": "2.2.0",
|
||||
"com.cneicy.shrink-datasaver-integration-app": "0.1.0",
|
||||
"com.cneicy.shrink-network": "0.2.0",
|
||||
"com.cneicy.shrink-network-integration-app": "0.1.0",
|
||||
"com.cneicy.shrink-command-integration-eventbus": "0.1.1",
|
||||
"com.cneicy.shrink-datasaver-integration-eventbus": "2.1.0",
|
||||
"com.cneicy.shrink-network-integration-eventbus": "0.1.1",
|
||||
"com.cneicy.shrink-command-integration-network": "0.1.0",
|
||||
"com.unity.ugui": "1.0.0",
|
||||
"com.cysharp.unitask": "2.5.10"
|
||||
},
|
||||
"keywords": [
|
||||
"starter",
|
||||
"app",
|
||||
"bootstrap",
|
||||
"sample"
|
||||
],
|
||||
"author": {
|
||||
"name": "cneicy",
|
||||
"url": "https://github.com/cneicy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 715b4b70d23970947be94997f77fc319
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9af734fd233467f43b2411018a21c4fa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c49d7f26769ac5d44bb096fedfc358f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkCommand.Integration.EventBus` 在当前工作区中的包内变更。
|
||||
|
||||
## [0.1.1] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- 提供 `UseEventBusBridge(...)` 扩展,可把命令服务接到 EventBus。
|
||||
- 提供 `RequestCommandAsync(...)` 扩展,支持通过事件请求式执行命令。
|
||||
- 建立命令桥接事件:`ShrinkCommandExecuteRequestEvent`、`ShrinkCommandExecutingEvent`、`ShrinkCommandExecutedEvent`、`ShrinkCommandFailedEvent`。
|
||||
|
||||
### Changed
|
||||
|
||||
- 桥接公共类型补齐 `#nullable enable`,统一可空语义。
|
||||
- 请求结果字段统一使用 `ExecutionResult`,避免与 `EventBase.Result` 命名冲突。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca7f23ed84bf7aa409d36b858591a823
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
# ShrinkCommand.Integration.EventBus
|
||||
|
||||
`ShrinkCommand` 与 `ShrinkEventBus` 的桥接层。
|
||||
|
||||
## 能力
|
||||
|
||||
- 通过事件请求执行命令
|
||||
- 把命令执行过程发布为 EventBus 生命周期事件
|
||||
- 保持 `ShrinkCommand` 核心不依赖 `ShrinkEventBus`
|
||||
|
||||
## 事件
|
||||
|
||||
- `ShrinkCommandExecuteRequestEvent`
|
||||
- `ShrinkCommandExecutingEvent`
|
||||
- `ShrinkCommandExecutedEvent`
|
||||
- `ShrinkCommandFailedEvent`
|
||||
|
||||
## 接入
|
||||
|
||||
`ShrinkApp.Starter.Basic` 的 ContextLoader 组合根会注册 `ShrinkCommandEventBusComponent`:它注入
|
||||
`shrink.service.command`,激活时接桥,依赖撤回时自动拆桥。独立使用本包时再显式注册:
|
||||
|
||||
```csharp
|
||||
var service = new ShrinkCommandService();
|
||||
service.AutoRegisterAll();
|
||||
service.UseEventBusBridge(new ShrinkCommandEventBusBridgeOptions
|
||||
{
|
||||
ServiceName = "default",
|
||||
DefaultSource = source
|
||||
});
|
||||
```
|
||||
|
||||
请求执行:
|
||||
|
||||
```csharp
|
||||
var result = await ShrinkCommandEventBusBridge.RequestCommandAsync("help", source);
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5b03f228e6ccef4e8225c62b96bd4a8
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ShrinkCommand.Integration.EventBus",
|
||||
"rootNamespace": "ShrinkCommand.Integration",
|
||||
"references": [
|
||||
"ShrinkCommand.Runtime",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63b0366d1e731824bb0900fbb820d543
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public static class ShrinkCommandEventBusBridge
|
||||
{
|
||||
private sealed class ServiceRegistration
|
||||
{
|
||||
public string ServiceName = ShrinkCommandConstants.DefaultServiceName;
|
||||
public ShrinkCommandService Service = null!;
|
||||
public IShrinkCommandSource? DefaultSource;
|
||||
public Action<ShrinkCommandExecutingInfo>? ExecutingHandler;
|
||||
public Action<ShrinkCommandExecutedInfo>? ExecutedHandler;
|
||||
}
|
||||
|
||||
private static readonly object SyncRoot = new();
|
||||
private static readonly Dictionary<string, ServiceRegistration> RegisteredByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<ShrinkCommandService, ServiceRegistration> RegisteredByService = new();
|
||||
private static bool _initialized;
|
||||
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var registration in RegisteredByService.Values)
|
||||
{
|
||||
if (registration.ExecutingHandler != null)
|
||||
registration.Service.OnCommandExecuting -= registration.ExecutingHandler;
|
||||
if (registration.ExecutedHandler != null)
|
||||
registration.Service.OnCommandExecuted -= registration.ExecutedHandler;
|
||||
}
|
||||
|
||||
RegisteredByService.Clear();
|
||||
RegisteredByName.Clear();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterService(
|
||||
ShrinkCommandService service,
|
||||
ShrinkCommandEventBusBridgeOptions? options = null)
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
EnsureInitialized();
|
||||
options ??= new ShrinkCommandEventBusBridgeOptions();
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (RegisteredByService.TryGetValue(service, out var existing))
|
||||
{
|
||||
existing.DefaultSource = options.DefaultSource;
|
||||
if (!string.Equals(existing.ServiceName, options.ServiceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RegisteredByName.Remove(existing.ServiceName);
|
||||
existing.ServiceName = NormalizeServiceName(options.ServiceName);
|
||||
RegisteredByName[existing.ServiceName] = existing;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var registration = new ServiceRegistration
|
||||
{
|
||||
ServiceName = NormalizeServiceName(options.ServiceName),
|
||||
Service = service,
|
||||
DefaultSource = options.DefaultSource
|
||||
};
|
||||
registration.ExecutingHandler = info => PublishExecuting(registration, info);
|
||||
registration.ExecutedHandler = info => PublishExecuted(registration, info);
|
||||
service.OnCommandExecuting += registration.ExecutingHandler;
|
||||
service.OnCommandExecuted += registration.ExecutedHandler;
|
||||
RegisteredByService[service] = registration;
|
||||
RegisteredByName[registration.ServiceName] = registration;
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnregisterService(ShrinkCommandService service)
|
||||
{
|
||||
if (service == null)
|
||||
return;
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!RegisteredByService.TryGetValue(service, out var registration))
|
||||
return;
|
||||
|
||||
if (registration.ExecutingHandler != null)
|
||||
service.OnCommandExecuting -= registration.ExecutingHandler;
|
||||
if (registration.ExecutedHandler != null)
|
||||
service.OnCommandExecuted -= registration.ExecutedHandler;
|
||||
|
||||
RegisteredByService.Remove(service);
|
||||
RegisteredByName.Remove(registration.ServiceName);
|
||||
}
|
||||
}
|
||||
|
||||
public static async UniTask<ShrinkCommandExecutionResult> RequestCommandAsync(
|
||||
string rawInput,
|
||||
IShrinkCommandSource? source = null,
|
||||
string? serviceName = null)
|
||||
{
|
||||
var requestEvent = new ShrinkCommandExecuteRequestEvent
|
||||
{
|
||||
ServiceName = NormalizeServiceName(serviceName),
|
||||
RawInput = rawInput ?? string.Empty,
|
||||
Source = source
|
||||
};
|
||||
|
||||
await EventBus.TriggerEventAsync(requestEvent);
|
||||
return requestEvent.ExecutionResult ?? ShrinkCommandExecutionResult.Failure("没有命令桥处理该请求。");
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
EventBus.RegisterEvent<ShrinkCommandExecuteRequestEvent>(HandleExecuteRequestAsync, EventPriority.LOWEST);
|
||||
}
|
||||
|
||||
private static async UniTask HandleExecuteRequestAsync(ShrinkCommandExecuteRequestEvent eventArgs)
|
||||
{
|
||||
if (eventArgs == null || eventArgs.IsHandled || eventArgs.IsCanceled)
|
||||
return;
|
||||
|
||||
ServiceRegistration? registration;
|
||||
lock (SyncRoot)
|
||||
{
|
||||
RegisteredByName.TryGetValue(NormalizeServiceName(eventArgs.ServiceName), out registration);
|
||||
}
|
||||
|
||||
if (registration == null)
|
||||
{
|
||||
eventArgs.IsHandled = true;
|
||||
eventArgs.ExecutionResult = ShrinkCommandExecutionResult.Failure(
|
||||
$"未找到命令服务: {NormalizeServiceName(eventArgs.ServiceName)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = eventArgs.Source ?? registration.DefaultSource;
|
||||
if (source == null)
|
||||
{
|
||||
eventArgs.IsHandled = true;
|
||||
eventArgs.ExecutionResult = ShrinkCommandExecutionResult.Failure("命令请求缺少来源对象。");
|
||||
return;
|
||||
}
|
||||
|
||||
eventArgs.ExecutionResult = await registration.Service.ExecuteAsync(source, eventArgs.RawInput);
|
||||
eventArgs.IsHandled = true;
|
||||
}
|
||||
|
||||
private static void PublishExecuting(ServiceRegistration registration, ShrinkCommandExecutingInfo info)
|
||||
{
|
||||
EventBus.TriggerEvent(new ShrinkCommandExecutingEvent
|
||||
{
|
||||
ServiceName = registration.ServiceName,
|
||||
RawInput = info.RawInput,
|
||||
CommandPath = info.Command.Path,
|
||||
SourceId = info.Source.SourceId,
|
||||
SourceDisplayName = info.Source.DisplayName,
|
||||
Arguments = new Dictionary<string, string>(info.Arguments, StringComparer.OrdinalIgnoreCase)
|
||||
});
|
||||
}
|
||||
|
||||
private static void PublishExecuted(ServiceRegistration registration, ShrinkCommandExecutedInfo info)
|
||||
{
|
||||
var commandPath = info.Command?.Path ?? string.Empty;
|
||||
var arguments = new Dictionary<string, string>(info.Arguments, StringComparer.OrdinalIgnoreCase);
|
||||
EventBus.TriggerEvent(new ShrinkCommandExecutedEvent
|
||||
{
|
||||
ServiceName = registration.ServiceName,
|
||||
RawInput = info.RawInput,
|
||||
CommandPath = commandPath,
|
||||
SourceId = info.Source.SourceId,
|
||||
SourceDisplayName = info.Source.DisplayName,
|
||||
Arguments = arguments,
|
||||
IsSuccess = info.Result.IsSuccess,
|
||||
Message = info.Result.Message
|
||||
});
|
||||
|
||||
if (info.Result.IsSuccess)
|
||||
return;
|
||||
|
||||
EventBus.TriggerEvent(new ShrinkCommandFailedEvent
|
||||
{
|
||||
ServiceName = registration.ServiceName,
|
||||
RawInput = info.RawInput,
|
||||
CommandPath = commandPath,
|
||||
SourceId = info.Source.SourceId,
|
||||
SourceDisplayName = info.Source.DisplayName,
|
||||
Arguments = arguments,
|
||||
ErrorMessage = info.Result.Message
|
||||
});
|
||||
}
|
||||
|
||||
private static string NormalizeServiceName(string? serviceName)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(serviceName) ? ShrinkCommandConstants.DefaultServiceName : serviceName.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de2d897c6d6fa1f419101a2c04ad57e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
[Cancelable]
|
||||
public sealed class ShrinkCommandExecuteRequestEvent : EventBase
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string RawInput { get; set; } = string.Empty;
|
||||
public IShrinkCommandSource? Source { get; set; }
|
||||
public bool IsHandled { get; set; }
|
||||
public ShrinkCommandExecutionResult? ExecutionResult { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShrinkCommandExecutingEvent : EventBase
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string RawInput { get; set; } = string.Empty;
|
||||
public string CommandPath { get; set; } = string.Empty;
|
||||
public string SourceId { get; set; } = string.Empty;
|
||||
public string SourceDisplayName { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Arguments { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ShrinkCommandExecutedEvent : EventBase
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string RawInput { get; set; } = string.Empty;
|
||||
public string CommandPath { get; set; } = string.Empty;
|
||||
public string SourceId { get; set; } = string.Empty;
|
||||
public string SourceDisplayName { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Arguments { get; set; } = new();
|
||||
public bool IsSuccess { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShrinkCommandFailedEvent : EventBase
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string RawInput { get; set; } = string.Empty;
|
||||
public string CommandPath { get; set; } = string.Empty;
|
||||
public string SourceId { get; set; } = string.Empty;
|
||||
public string SourceDisplayName { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Arguments { get; set; } = new();
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f8b986a535559d41a2ab05a20708b28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public static class ShrinkCommandEventBusBridgeExtensions
|
||||
{
|
||||
public static ShrinkCommandService UseEventBusBridge(
|
||||
this ShrinkCommandService service,
|
||||
ShrinkCommandEventBusBridgeOptions? options = null)
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
ShrinkCommandEventBusBridge.RegisterService(service, options);
|
||||
return service;
|
||||
}
|
||||
|
||||
public static UniTask<ShrinkCommandExecutionResult> RequestCommandAsync(
|
||||
this ShrinkCommandService service,
|
||||
string rawInput,
|
||||
IShrinkCommandSource? source = null,
|
||||
string? serviceName = null)
|
||||
{
|
||||
return ShrinkCommandEventBusBridge.RequestCommandAsync(rawInput, source, serviceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0ed31dba396b6d4ab075e75a16b00d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public sealed class ShrinkCommandEventBusBridgeOptions
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public IShrinkCommandSource? DefaultSource { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6de8286e0b74a54398432828ae503fb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
/// <summary>通过命令服务键管理 Command ↔ EventBus 桥的可逆生命周期。</summary>
|
||||
public sealed class ShrinkCommandEventBusComponent : IShrinkComponent
|
||||
{
|
||||
public const string CommandServiceKey = "shrink.service.command";
|
||||
public const string ProvideKey = "shrink.integration.command-eventbus";
|
||||
|
||||
private static readonly string[] InjectKeys = { CommandServiceKey };
|
||||
private static readonly string[] ProvideKeys = { ProvideKey };
|
||||
|
||||
public string Name => "shrink.integration.command-eventbus";
|
||||
public IReadOnlyList<string> Inject => InjectKeys;
|
||||
public IReadOnlyList<string> Provide => ProvideKeys;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
var service = ctx.Get<ShrinkCommandService>(CommandServiceKey);
|
||||
var options = config as ShrinkCommandEventBusBridgeOptions;
|
||||
ShrinkCommandEventBusBridge.RegisterService(service, options);
|
||||
ctx.EffectInverse(() =>
|
||||
{
|
||||
ShrinkCommandEventBusBridge.UnregisterService(service);
|
||||
return UniTask.CompletedTask;
|
||||
});
|
||||
ctx.Set(ProvideKey, Name);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 078a765d611231c478ac77312b44c444
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-command-integration-eventbus",
|
||||
"version": "0.1.1",
|
||||
"displayName": "ShrinkCommand - EventBus Integration",
|
||||
"description": "ShrinkCommand 与 ShrinkEventBus 的桥接层,支持事件请求执行命令,以及命令执行生命周期事件发布。",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-command": "0.1.0",
|
||||
"com.cneicy.shrink-context-core": "0.1.0",
|
||||
"com.cneicy.shrink-eventbus": "1.1.5"
|
||||
},
|
||||
"keywords": ["command", "eventbus", "integration", "bridge"],
|
||||
"author": {
|
||||
"name": "cneicy",
|
||||
"url": "https://github.com/cneicy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63114b97f96b400469a77b9701d8ffbc
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8866149adf87b2a44b9a382d544ced41
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkCommand.Integration.Network` 在当前工作区中的包内变更。
|
||||
|
||||
## [0.1.0] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- 增加 `command/execute` RPC 桥接。
|
||||
- 远程会话可自动映射为 `IShrinkCommandSource`,统一走命令运行时。
|
||||
- 命令执行结果可直接通过 `ShrinkNetwork` 返回给远端调用方。
|
||||
|
||||
### Changed
|
||||
|
||||
- 桥接层公共类型补齐 `#nullable enable`,对齐当前运行时与宿主侧的可空语义。
|
||||
- 保持桥接层职责边界,命令核心仍不直接依赖网络实现。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc0d137c46b092f49a54bc3afcd7c161
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
# ShrinkCommand.Integration.Network
|
||||
|
||||
`ShrinkCommand` 与 `ShrinkNetwork` 的桥接层。
|
||||
|
||||
## 能力
|
||||
|
||||
- 通过网络 RPC 执行命令
|
||||
- 远程命令来源自动映射为 `IShrinkCommandSource`
|
||||
- 继续复用命令系统自己的权限与来源限制
|
||||
|
||||
## 接入
|
||||
|
||||
```csharp
|
||||
commandService = new ShrinkCommandService();
|
||||
commandService.AutoRegisterAll();
|
||||
|
||||
networkService.UseCommandBridge(commandService);
|
||||
```
|
||||
|
||||
客户端调用:
|
||||
|
||||
```csharp
|
||||
var response = await session.ExecuteCommandAsync("help");
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c85d4974b8207cf45848e1379ad7baa3
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ShrinkCommand.Integration.Network",
|
||||
"rootNamespace": "ShrinkCommand.Integration",
|
||||
"references": [
|
||||
"ShrinkCommand.Runtime",
|
||||
"ShrinkNetwork.Runtime",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9c305f415a0515409b2795a809e7240
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkCommand;
|
||||
using ShrinkContext;
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// 命令-网络集成组件(论文 6.5 集成组件模式):
|
||||
/// 核心(command / network)互不依赖,本组件注入双方服务键,全部就绪才激活,
|
||||
/// 任一提供者退役即自动停用——依赖接线由响应式余效应结构性完成,替代手工桥接编排。
|
||||
///
|
||||
/// 已知边界:ShrinkNetworkService 当前无 handler 注销 API,停用不撤销已注册的
|
||||
/// command/execute 处理器(传输解绑后不可达;网络服务实例重建时自然消失)。
|
||||
/// </summary>
|
||||
public sealed class ShrinkCommandNetworkIntegrationComponent : IShrinkComponent
|
||||
{
|
||||
/// <summary>与 ShrinkNetworkAppComponent.ServiceKey 保持一致(避免跨包硬引用)。</summary>
|
||||
public const string NetworkServiceKey = "shrink.service.network";
|
||||
|
||||
public const string CommandServiceKey = "shrink.service.command";
|
||||
public const string ProvideKey = "shrink.integration.command-network";
|
||||
|
||||
private static readonly string[] InjectKeys = { CommandServiceKey, NetworkServiceKey };
|
||||
private static readonly string[] ProvideKeys = { ProvideKey };
|
||||
|
||||
public string Name => "shrink.integration.command-network";
|
||||
|
||||
public IReadOnlyList<string> Inject => InjectKeys;
|
||||
|
||||
public IReadOnlyList<string> Provide => ProvideKeys;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
var commandService = ctx.Get<ShrinkCommandService>(CommandServiceKey);
|
||||
var networkService = ctx.Get<ShrinkNetworkService>(NetworkServiceKey);
|
||||
var options = config as ShrinkNetworkCommandBridgeOptions;
|
||||
|
||||
ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options);
|
||||
ctx.Set(ProvideKey, ShrinkNetworkCommandBridge.ExecuteRoute);
|
||||
|
||||
// 桥注册按网络服务实例幂等去重;可逆面为集成键的发布/撤回
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5710d88a762cde489282366255f755c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public static class ShrinkNetworkCommandBridge
|
||||
{
|
||||
public const string ExecuteRoute = "command/execute";
|
||||
|
||||
private static readonly object SyncRoot = new();
|
||||
private static readonly HashSet<ShrinkNetworkService> RegisteredServices = new();
|
||||
|
||||
public static void RegisterService(
|
||||
ShrinkNetworkService networkService,
|
||||
ShrinkCommandService commandService,
|
||||
ShrinkNetworkCommandBridgeOptions? options = null)
|
||||
{
|
||||
if (networkService == null)
|
||||
throw new ArgumentNullException(nameof(networkService));
|
||||
if (commandService == null)
|
||||
throw new ArgumentNullException(nameof(commandService));
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (RegisteredServices.Contains(networkService))
|
||||
return;
|
||||
RegisteredServices.Add(networkService);
|
||||
}
|
||||
|
||||
options ??= new ShrinkNetworkCommandBridgeOptions();
|
||||
|
||||
EnsureMessagesRegistered(networkService);
|
||||
var requirement = new ShrinkNetworkPermissionRequirement(options.Authority, options.Permission);
|
||||
networkService.RegisterRequestHandler<ShrinkNetworkCommandRequest, ShrinkNetworkCommandResponse>(
|
||||
(context, request) => HandleExecuteAsync(commandService, options, context, request),
|
||||
requirement);
|
||||
}
|
||||
|
||||
public static void EnsureMessagesRegistered(ShrinkNetworkService service)
|
||||
{
|
||||
if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandRequest), out _))
|
||||
{
|
||||
var requestAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute(
|
||||
typeof(ShrinkNetworkCommandRequest),
|
||||
typeof(ShrinkNetworkMessageAttribute),
|
||||
false)!;
|
||||
service.RegisterMessage(typeof(ShrinkNetworkCommandRequest), requestAttribute.Opcode, requestAttribute.Route);
|
||||
}
|
||||
|
||||
if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandResponse), out _))
|
||||
{
|
||||
var responseAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute(
|
||||
typeof(ShrinkNetworkCommandResponse),
|
||||
typeof(ShrinkNetworkMessageAttribute),
|
||||
false)!;
|
||||
service.RegisterMessage(typeof(ShrinkNetworkCommandResponse), responseAttribute.Opcode, responseAttribute.Route);
|
||||
}
|
||||
}
|
||||
|
||||
private static async UniTask<ShrinkNetworkCommandResponse> HandleExecuteAsync(
|
||||
ShrinkCommandService commandService,
|
||||
ShrinkNetworkCommandBridgeOptions options,
|
||||
ShrinkNetworkContext context,
|
||||
ShrinkNetworkCommandRequest request)
|
||||
{
|
||||
var source = options.SourceFactory?.Invoke(context, request) ?? new ShrinkNetworkCommandSource(context, request);
|
||||
var result = await commandService.ExecuteAsync(source, request.CommandLine);
|
||||
var output = source is ShrinkNetworkCommandSource networkSource
|
||||
? networkSource.BuildOutputMessage(result.Message)
|
||||
: result.Message;
|
||||
|
||||
return new ShrinkNetworkCommandResponse
|
||||
{
|
||||
ServiceName = string.IsNullOrWhiteSpace(request.ServiceName) ? options.ServiceName : request.ServiceName.Trim(),
|
||||
CommandLine = request.CommandLine ?? string.Empty,
|
||||
OutputMessage = output,
|
||||
ErrorCode = result.IsSuccess ? 0 : ShrinkRpcErrorCode.HandlerException,
|
||||
ErrorMessage = result.IsSuccess ? string.Empty : output
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f22899cbeb3779d4eb1338217fa4d38f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public static class ShrinkNetworkCommandBridgeExtensions
|
||||
{
|
||||
public static ShrinkNetworkService UseCommandBridge(
|
||||
this ShrinkNetworkService networkService,
|
||||
ShrinkCommandService commandService,
|
||||
ShrinkNetworkCommandBridgeOptions? options = null)
|
||||
{
|
||||
if (networkService == null)
|
||||
throw new ArgumentNullException(nameof(networkService));
|
||||
if (commandService == null)
|
||||
throw new ArgumentNullException(nameof(commandService));
|
||||
|
||||
ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options);
|
||||
return networkService;
|
||||
}
|
||||
|
||||
public static async UniTask<ShrinkNetworkCommandResponse> ExecuteCommandAsync(
|
||||
this ShrinkNetworkSession session,
|
||||
string commandLine,
|
||||
string? serviceName = null,
|
||||
string? clientLabel = null,
|
||||
ShrinkRpcCallOptions? options = null)
|
||||
{
|
||||
if (session == null)
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
|
||||
ShrinkNetworkCommandBridge.EnsureMessagesRegistered(session.Service);
|
||||
return await session.RpcAsync<ShrinkNetworkCommandRequest, ShrinkNetworkCommandResponse>(
|
||||
new ShrinkNetworkCommandRequest
|
||||
{
|
||||
ServiceName = string.IsNullOrWhiteSpace(serviceName)
|
||||
? ShrinkCommandConstants.DefaultServiceName
|
||||
: serviceName.Trim(),
|
||||
CommandLine = commandLine ?? string.Empty,
|
||||
ClientLabel = clientLabel ?? string.Empty
|
||||
},
|
||||
options ?? new ShrinkRpcCallOptions
|
||||
{
|
||||
TimeoutMs = 5000,
|
||||
DebugLabel = "RemoteCommand"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11bf32dbf7be0e14cbddf1f03ded5cbb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public sealed class ShrinkNetworkCommandBridgeOptions
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public ShrinkNetworkAuthority Authority { get; set; } = ShrinkNetworkAuthority.ClientOnly;
|
||||
public string Permission { get; set; } = string.Empty;
|
||||
public Func<ShrinkNetworkContext, ShrinkNetworkCommandRequest, IShrinkCommandSource>? SourceFactory { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 174745a3cab12534c8d4b09788063fe9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
[ShrinkNetworkMessage(1501, ShrinkNetworkCommandBridge.ExecuteRoute)]
|
||||
public sealed class ShrinkNetworkCommandRequest : IShrinkNetworkRequest
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string CommandLine { get; set; } = string.Empty;
|
||||
public string ClientLabel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[ShrinkNetworkMessage(1502, "command/execute_response")]
|
||||
public sealed class ShrinkNetworkCommandResponse : ShrinkRpcResponseBase
|
||||
{
|
||||
public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName;
|
||||
public string CommandLine { get; set; } = string.Empty;
|
||||
public string OutputMessage { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b44977ad838cce438d99ea565848485
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkNetwork;
|
||||
|
||||
namespace ShrinkCommand.Integration
|
||||
{
|
||||
public sealed class ShrinkNetworkCommandSource : IShrinkCommandSource
|
||||
{
|
||||
private readonly List<string> _lines = new();
|
||||
private readonly ShrinkNetworkContext _context;
|
||||
private readonly ShrinkNetworkCommandRequest _request;
|
||||
|
||||
public ShrinkNetworkCommandSource(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request)
|
||||
{
|
||||
_context = context;
|
||||
_request = request;
|
||||
SourceId = $"network:{context.Session.SessionId}";
|
||||
DisplayName = ResolveDisplayName(context, request);
|
||||
}
|
||||
|
||||
public string SourceId { get; }
|
||||
public string DisplayName { get; }
|
||||
public bool IsConsole => false;
|
||||
public ShrinkNetworkSession Session => _context.Session;
|
||||
|
||||
public bool HasPermission(string permission)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(permission) || Session.HasPermission(permission);
|
||||
}
|
||||
|
||||
public UniTask WriteLineAsync(string message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
_lines.Add(message);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public string BuildOutputMessage(string resultMessage)
|
||||
{
|
||||
if (_lines.Count == 0)
|
||||
return resultMessage ?? string.Empty;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var index = 0; index < _lines.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.AppendLine();
|
||||
builder.Append(_lines[index]);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resultMessage))
|
||||
{
|
||||
if (builder.Length > 0 && !string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal))
|
||||
builder.AppendLine();
|
||||
if (!string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal))
|
||||
builder.Append(resultMessage);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string ResolveDisplayName(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request)
|
||||
{
|
||||
if (context.Session.Items.TryGetValue("auth.name", out var authName) && authName is string authNameText &&
|
||||
!string.IsNullOrWhiteSpace(authNameText))
|
||||
{
|
||||
return authNameText.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ClientLabel))
|
||||
return request.ClientLabel.Trim();
|
||||
|
||||
return context.Session.RemoteAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf31a6f53f1bb0c4ca0af60b92dce81f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9777f2e6be37f64e8ec2df9b9afef87
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,99 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkCommand.Integration;
|
||||
using ShrinkCommand.Integration.App;
|
||||
using ShrinkContext;
|
||||
using ShrinkDataSaver.Integration.App;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkNetwork.Integration.App;
|
||||
|
||||
namespace ShrinkCommand.Integration.Network.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// 阶段 3 集成组件模式(论文 6.5):
|
||||
/// 核心(command/network/datasaver)互不依赖,集成组件注入双方服务键;
|
||||
/// 全部提供者就绪才激活,任一退役即停用——依赖接线由响应式余效应完成。
|
||||
/// </summary>
|
||||
public class IntegrationComponentsTests
|
||||
{
|
||||
private ShrinkContextRuntime _runtime = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_runtime = new ShrinkContextRuntime();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_runtime.ShutdownAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void Run(UniTask task) => task.GetAwaiter().GetResult();
|
||||
|
||||
[Test]
|
||||
public void Integration_ActivatesOnlyWhenBothProvidersActive()
|
||||
{
|
||||
var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "两个服务键都缺失时等待");
|
||||
|
||||
var command = _runtime.Use(new ShrinkCommandAppComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, command.State);
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "仅命令服务就绪仍等待网络服务");
|
||||
|
||||
var network = _runtime.Use(new ShrinkNetworkAppComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, network.State);
|
||||
Assert.AreEqual(ShrinkFiberState.Active, integration.State,
|
||||
"双键齐备后集成组件激活并完成桥注册");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EitherProviderRetire_DeactivatesIntegration_AndReactivatesOnReturn()
|
||||
{
|
||||
var command = _runtime.Use(new ShrinkCommandAppComponent());
|
||||
var network = _runtime.Use(new ShrinkNetworkAppComponent());
|
||||
var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, integration.State);
|
||||
|
||||
Run(_runtime.RetireAsync(network));
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, integration.State,
|
||||
"网络提供者退役,集成组件自动停用(服务键撤回触发)");
|
||||
Assert.AreEqual(ShrinkFiberState.Active, command.State, "命令核心不受影响");
|
||||
|
||||
var networkAgain = _runtime.Use(new ShrinkNetworkAppComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, integration.State,
|
||||
"提供者回归后集成组件自动重新激活");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CommandComponent_ProvidesDefaultServiceKey()
|
||||
{
|
||||
var fiber = _runtime.Use(new ShrinkCommandAppComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, fiber.State);
|
||||
Assert.IsTrue(_runtime.TryGetRaw<ShrinkCommandService>(_runtime.RootContext,
|
||||
ShrinkCommandAppComponent.ServiceKey, out var service));
|
||||
Assert.AreSame(ShrinkCommandRuntime.Default, service);
|
||||
|
||||
Run(_runtime.RetireAsync(fiber));
|
||||
Assert.IsFalse(_runtime.TryGetRaw<ShrinkCommandService>(_runtime.RootContext,
|
||||
ShrinkCommandAppComponent.ServiceKey, out _), "退役撤回服务键");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DataSaverComponent_Activates_FlushesOnDeactivate_WithdrawsKey()
|
||||
{
|
||||
var fiber = _runtime.Use(new ShrinkDataSaverAppComponent());
|
||||
Assert.AreEqual(ShrinkFiberState.Active, fiber.State);
|
||||
Assert.IsTrue(_runtime.TryGetRaw<object>(_runtime.RootContext,
|
||||
ShrinkDataSaverAppComponent.ServiceKey, out _));
|
||||
|
||||
Assert.DoesNotThrow(() => Run(_runtime.RetireAsync(fiber)),
|
||||
"停用执行补偿式收尾(设置最终落盘;未初始化环境安全跳过)");
|
||||
Assert.IsFalse(_runtime.TryGetRaw<object>(_runtime.RootContext,
|
||||
ShrinkDataSaverAppComponent.ServiceKey, out _));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c9445def3a931642955715272969a72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "ShrinkCommand.Integration.Network.Tests",
|
||||
"rootNamespace": "ShrinkCommand.Integration.Network.Tests",
|
||||
"references": [
|
||||
"ShrinkCommand.Integration.Network",
|
||||
"ShrinkCommand.Integration.App",
|
||||
"ShrinkDataSaver.Integration.App",
|
||||
"ShrinkNetwork.Integration.App",
|
||||
"ShrinkCommand.Runtime",
|
||||
"ShrinkNetwork.Runtime",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"UniTask",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd3e105f80569534b9ba2d6fa913a2cc
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-command-integration-network",
|
||||
"version": "0.1.0",
|
||||
"displayName": "ShrinkCommand - Network Integration",
|
||||
"description": "ShrinkCommand 与 ShrinkNetwork 的桥接层,允许远程会话通过 RPC 执行命令。",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-command": "0.1.0",
|
||||
"com.cneicy.shrink-network": "0.1.0",
|
||||
"com.cneicy.shrink-context-core": "0.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"command",
|
||||
"network",
|
||||
"integration",
|
||||
"rpc"
|
||||
],
|
||||
"author": {
|
||||
"name": "cneicy",
|
||||
"url": "https://github.com/cneicy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73eb22917e75fcf4b8f1c7df5490014e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 724b7b3c1a3a5324f88b60aaa08071f1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkCommand` 模块在当前工作区中的变更。
|
||||
|
||||
## [0.2.0] - 2026-05-18
|
||||
|
||||
### Changed
|
||||
|
||||
- 静态命令自动发现从运行时全域反射扫描切换为编译期注册表 `ShrinkCommandGeneratedRegistry`。
|
||||
- `ShrinkCommandRuntime.Default` 启动时优先消费编译期清单,降低默认服务的注册开销。
|
||||
- `RegisterCommands(object target)` 的实例注册路径保持不变,继续支持模组实例和运行时对象。
|
||||
|
||||
## [当前工作区版] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- 正式抽成独立命令运行时,核心不再直接耦合 `ShrinkEventBus` 和 `ShrinkNetwork`。
|
||||
- 建立属性式注册模型:`[ShrinkCommandSubscriber]`、`[ShrinkCommand]`、`AutoRegisterAll()`。
|
||||
- 命令路径支持字面量段、普通参数段与贪婪尾参数段,补齐 `help` / `?` 内建命令。
|
||||
- 支持命令来源限制、权限检查、建议与常见基础类型转换,供 Unity 与纯 `.NET` 宿主复用。
|
||||
|
||||
### Changed
|
||||
|
||||
- `ShrinkCommandService`、`ShrinkCommandTypes` 等共享运行时类型显式启用 `#nullable enable`。
|
||||
- 公共类型中的初始化属性由 `init` 收口为 `set`,避免 Unity 2022 编译链再次触发 `IsExternalInit` 缺失。
|
||||
- 宿主与运行时链路统一复用 `ShrinkCommandService`,控制台与网络入口共享同一套命令注册与执行逻辑。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6b7cf1f4ef5d9749ad8ab464b2d4efd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
# ShrinkCommand
|
||||
|
||||
独立于 `ShrinkNetwork` 的命令系统,目标是提供类似 Minecraft/Brigadier 风格的命令路径、权限、帮助输出和属性式自动注册。
|
||||
|
||||
当前版本已把**静态命令发现**切到编译期注册表:运行时默认服务不再通过 `AppDomain.GetAssemblies() + GetTypes()` 全域扫描静态命令类型,而是直接消费编译期注入的清单。实例命令注册路径保持不变,仍适合模组实例和运行时对象。
|
||||
|
||||
## 特性
|
||||
|
||||
- 路径式命令定义:`whitelist add <player>`、`say <message...>`
|
||||
- 支持字面量段和贪婪参数段
|
||||
- 内建 `help` / `?`
|
||||
- 支持命令来源限制:控制台专用 / 非控制台专用 / 任意来源
|
||||
- 支持权限校验
|
||||
- 支持属性式自动注册,也支持手工注册
|
||||
- 可在 Unity 和纯 .NET 宿主中复用
|
||||
|
||||
## 快速开始
|
||||
|
||||
```csharp
|
||||
using ShrinkCommand;
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class DemoCommands
|
||||
{
|
||||
[ShrinkCommand("say <message...>", Description = "广播一段文本")]
|
||||
private static string Say(string message)
|
||||
{
|
||||
return "[say] " + message;
|
||||
}
|
||||
}
|
||||
|
||||
var service = new ShrinkCommandService();
|
||||
service.AutoRegisterAll();
|
||||
```
|
||||
|
||||
手工执行:
|
||||
|
||||
```csharp
|
||||
var result = await service.ExecuteAsync(source, "say hello world");
|
||||
```
|
||||
|
||||
## 方法签名
|
||||
|
||||
支持的参数:
|
||||
|
||||
- `ShrinkCommandContext`
|
||||
- `IShrinkCommandSource` 或其派生类型
|
||||
- 路径中声明的普通参数,按顺序绑定到方法参数
|
||||
|
||||
支持的返回类型:
|
||||
|
||||
- `void`
|
||||
- `string`
|
||||
- `ShrinkCommandExecutionResult`
|
||||
- `UniTask`
|
||||
- `UniTask<string>`
|
||||
- `UniTask<ShrinkCommandExecutionResult>`
|
||||
- `Task`
|
||||
- `Task<string>`
|
||||
- `Task<ShrinkCommandExecutionResult>`
|
||||
|
||||
## 内建命令
|
||||
|
||||
- `help`
|
||||
- `help <path...>`
|
||||
- `?`
|
||||
- `? <path...>`
|
||||
|
||||
## 自动注册说明
|
||||
|
||||
### 静态命令
|
||||
|
||||
默认运行时会直接读取编译期生成的 `ShrinkCommandGeneratedRegistry`,注册所有静态 `[ShrinkCommandSubscriber]` + `[ShrinkCommand]`。
|
||||
|
||||
### 实例命令
|
||||
|
||||
```csharp
|
||||
service.RegisterCommands(myCommandInstance);
|
||||
```
|
||||
|
||||
实例注册仍然会扫描对象本身的方法签名,不依赖编译期清单,适合:
|
||||
|
||||
- 运行时创建的对象
|
||||
- 模组实例
|
||||
- 需要带状态的命令宿主
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user