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

This commit is contained in:
2026-08-26 02:49:36 +08:00
commit efe3a22a06
36 changed files with 1736 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Publish UPM package
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
steps:
- name: Fetch tagged revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
test -n "$ref"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkCommand.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Validate immutable release version
shell: bash
run: |
set -eu
tag="$(git describe --exact-match --tags HEAD)"
version="$(node -p "require('./package.json').version")"
test "$tag" = "v$version"
npm pack --dry-run
- name: Publish to ShrinkSDK registry
shell: bash
run: |
set -eu
: "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
npmrc="$HOME/.npmrc"
cleanup() { rm -f "$npmrc"; }
trap cleanup EXIT
printf '%s\n' \
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/
+39
View File
@@ -0,0 +1,39 @@
name: Verify standalone Unity package
on:
workflow_dispatch:
jobs:
editmode:
runs-on: unity-2022.3.62f3
container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
volumes:
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
steps:
- name: Fetch selected revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkCommand.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Run package EditMode tests
shell: bash
run: |
set -eu
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
"$unity_bin" \
-batchmode \
-nographics \
-quit \
-projectPath "$PWD/Development~/UnityProject" \
-runTests \
-testPlatform EditMode \
-testResults "$PWD/TestResults/editmode.xml" \
-logFile "$PWD/TestResults/unity.log"
+10
View File
@@ -0,0 +1,10 @@
/Development~/UnityProject/[Ll]ibrary/
/Development~/UnityProject/[Tt]emp/
/Development~/UnityProject/[Oo]bj/
/Development~/UnityProject/[Ll]ogs/
/Development~/UnityProject/[Uu]ser[Ss]ettings/
/Development~/UnityProject/TestResults/
/Tools~/**/[Bb]in/
/Tools~/**/[Oo]bj/
*.user
*.DotSettings.user
+8
View File
@@ -0,0 +1,8 @@
.git/
.gitea/
Development~/
Tools~/
*.csproj
*.sln
*.user
*.DotSettings.user
+26
View File
@@ -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`,控制台与网络入口共享同一套命令注册与执行逻辑。
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a6b7cf1f4ef5d9749ad8ab464b2d4efd
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+6
View File
@@ -0,0 +1,6 @@
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Ll]ogs/
[Uu]ser[Ss]ettings/
TestResults/
@@ -0,0 +1,15 @@
{
"scopedRegistries": [
{
"name": "ShrinkSDK",
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
"scopes": [
"com.cneicy"
]
}
],
"dependencies": {
"com.unity.test-framework": "1.1.33",
"com.cneicy.shrink-command": "file:../../.."
}
}
@@ -0,0 +1,2 @@
m_EditorVersion: 2022.3.62f3
m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7)
+85
View File
@@ -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);
```
实例注册仍然会扫描对象本身的方法签名,不依赖编译期清单,适合:
- 运行时创建的对象
- 模组实例
- 需要带状态的命令宿主
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5d2771608e5865b47b8223f2384c112f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9c0ba4b9b3aa21a429e63ee94f25af83
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: afe6c0228c3894b44a1bd06d8fe7b7e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+53
View File
@@ -0,0 +1,53 @@
using System;
namespace ShrinkCommand
{
public static class ShrinkCommandLogger
{
public static Action<string> InfoHandler { get; set; } = DefaultInfo;
public static Action<string> WarningHandler { get; set; } = DefaultWarning;
public static Action<string> ErrorHandler { get; set; } = DefaultError;
public static Action<Exception> ExceptionHandler { get; set; } = DefaultException;
public static void Info(string message) => InfoHandler?.Invoke(message);
public static void Warn(string message) => WarningHandler?.Invoke(message);
public static void Error(string message) => ErrorHandler?.Invoke(message);
public static void Exception(Exception ex) => ExceptionHandler?.Invoke(ex);
private static void DefaultInfo(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.Log(message);
#else
Console.WriteLine(message);
#endif
}
private static void DefaultWarning(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogWarning(message);
#else
Console.WriteLine("[Warn] " + message);
#endif
}
private static void DefaultError(string message)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogError(message);
#else
Console.Error.WriteLine(message);
#endif
}
private static void DefaultException(Exception ex)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogException(ex);
#else
Console.Error.WriteLine(ex);
#endif
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78ac47ab23518d94e896f195b5fd56f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+22
View File
@@ -0,0 +1,22 @@
namespace ShrinkCommand
{
public static class ShrinkCommandRuntime
{
public static ShrinkCommandService Default { get; private set; } = BuildDefault();
#if UNITY_5_3_OR_NEWER
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetOnPlayModeEnter()
{
Default = BuildDefault();
}
#endif
private static ShrinkCommandService BuildDefault()
{
var service = new ShrinkCommandService();
ShrinkCommandGeneratedRegistry.RegisterAll(service);
return service;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f95938eff193024b85822d1815b4953
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+884
View File
@@ -0,0 +1,884 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
namespace ShrinkCommand
{
public sealed class ShrinkCommandService
{
private sealed class CommandDefinition
{
public ShrinkCommandDescriptor Descriptor { get; set; } = new();
public IReadOnlyList<string> ArgumentNames { get; set; } = Array.Empty<string>();
public IReadOnlyList<PathPattern> Patterns { get; set; } = Array.Empty<PathPattern>();
public Func<ShrinkCommandContext, UniTask<ShrinkCommandExecutionResult>> Handler { get; set; } =
_ => UniTask.FromResult(ShrinkCommandExecutionResult.Success());
}
private sealed class PathPattern
{
public string Text { get; set; } = string.Empty;
public IReadOnlyList<PathSegment> Segments { get; set; } = Array.Empty<PathSegment>();
}
private sealed class PathSegment
{
public bool IsArgument { get; set; }
public bool IsGreedy { get; set; }
public string Token { get; set; } = string.Empty;
}
private sealed class MatchCandidate
{
public CommandDefinition Definition { get; set; } = null!;
public PathPattern Pattern { get; set; } = null!;
public IReadOnlyList<string> RawArgumentValues { get; set; } = Array.Empty<string>();
public int LiteralScore { get; set; }
}
private readonly List<CommandDefinition> _commands = new();
private readonly Dictionary<Type, Func<string, object?>> _converters = new();
public event Action<ShrinkCommandExecutingInfo>? OnCommandExecuting;
public event Action<ShrinkCommandExecutedInfo>? OnCommandExecuted;
public ShrinkCommandService()
{
RegisterDefaultConverters();
RegisterBuiltInCommands();
}
public IReadOnlyList<ShrinkCommandDescriptor> Commands => _commands
.Select(command => command.Descriptor)
.OrderBy(command => command.Path, StringComparer.OrdinalIgnoreCase)
.ToArray();
public void RegisterConverter<T>(Func<string, T> converter)
{
if (converter == null)
throw new ArgumentNullException(nameof(converter));
_converters[typeof(T)] = raw => converter(raw);
}
public void RegisterCommand(ShrinkCommandRegistration registration)
{
if (registration == null)
throw new ArgumentNullException(nameof(registration));
if (registration.Handler == null)
throw new ArgumentNullException(nameof(registration.Handler));
var primaryPattern = ParsePattern(registration.Path);
var argumentNames = primaryPattern.Segments
.Where(segment => segment.IsArgument)
.Select(segment => segment.Token)
.ToArray();
var patterns = new List<PathPattern> { primaryPattern };
foreach (var alias in registration.Aliases ?? Array.Empty<string>())
{
if (string.IsNullOrWhiteSpace(alias))
continue;
var aliasPattern = ParsePattern(alias);
ValidateAliasShape(primaryPattern, aliasPattern);
patterns.Add(aliasPattern);
}
var descriptor = new ShrinkCommandDescriptor
{
Path = primaryPattern.Text,
Aliases = (registration.Aliases ?? Array.Empty<string>())
.Where(alias => !string.IsNullOrWhiteSpace(alias))
.Select(alias => alias.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray(),
Description = registration.Description?.Trim() ?? string.Empty,
Permission = registration.Permission?.Trim() ?? string.Empty,
SourceKind = registration.SourceKind,
Hidden = registration.Hidden
};
var duplicate = _commands.FirstOrDefault(existing =>
existing.Patterns.Any(existingPattern =>
patterns.Any(newPattern => string.Equals(existingPattern.Text, newPattern.Text, StringComparison.OrdinalIgnoreCase))));
if (duplicate != null)
throw new InvalidOperationException($"Command path already registered: {duplicate.Descriptor.Path}");
_commands.Add(new CommandDefinition
{
Descriptor = descriptor,
ArgumentNames = argumentNames,
Patterns = patterns,
Handler = registration.Handler
});
}
public void AutoRegisterStaticCommands()
{
ShrinkCommandRegHelper.RegisterStaticCommands(this);
}
public void RegisterCommands(object target)
{
ShrinkCommandRegHelper.RegisterCommands(this, target);
}
public void AutoRegisterAll()
{
AutoRegisterStaticCommands();
}
public async UniTask<ShrinkCommandExecutionResult> ExecuteAsync(
IShrinkCommandSource source,
string rawInput,
CancellationToken cancellationToken = default)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
var input = (rawInput ?? string.Empty).Trim().TrimStart('\uFEFF');
if (string.IsNullOrWhiteSpace(input))
return ShrinkCommandExecutionResult.Success(BuildHelp(source));
List<string> tokens;
try
{
tokens = Tokenize(input);
}
catch (Exception ex)
{
return CompleteEarly(source, input, null, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
ShrinkCommandExecutionResult.Failure("命令解析失败: " + ex.Message));
}
var matches = FindMatches(tokens);
if (matches.Count == 0)
{
var suggestions = BuildSuggestions(tokens, source);
return CompleteEarly(source, input, null, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
string.IsNullOrWhiteSpace(suggestions)
? ShrinkCommandExecutionResult.Failure($"未知命令: {input}\n输入 `help` 查看可用命令。")
: ShrinkCommandExecutionResult.Failure($"未知命令: {input}\n{suggestions}"));
}
var selected = SelectBestMatch(matches);
if (!CanRunSourceKind(selected.Definition.Descriptor.SourceKind, source))
{
return CompleteEarly(source, input, selected.Definition.Descriptor,
BuildArgumentMap(selected.Definition.ArgumentNames, selected.RawArgumentValues),
ShrinkCommandExecutionResult.Failure("该命令当前来源不可执行。"));
}
if (!string.IsNullOrWhiteSpace(selected.Definition.Descriptor.Permission) &&
!source.HasPermission(selected.Definition.Descriptor.Permission))
{
return CompleteEarly(source, input, selected.Definition.Descriptor,
BuildArgumentMap(selected.Definition.ArgumentNames, selected.RawArgumentValues),
ShrinkCommandExecutionResult.Failure(
$"没有权限执行该命令: {selected.Definition.Descriptor.Permission}"));
}
var argumentMap = BuildArgumentMap(selected.Definition.ArgumentNames, selected.RawArgumentValues);
var context = new ShrinkCommandContext(this, source, selected.Definition.Descriptor, input, argumentMap,
cancellationToken);
var executingInfo = new ShrinkCommandExecutingInfo
{
Service = this,
Source = source,
Command = selected.Definition.Descriptor,
RawInput = input,
Arguments = argumentMap
};
try
{
OnCommandExecuting?.Invoke(executingInfo);
var result = await selected.Definition.Handler(context);
OnCommandExecuted?.Invoke(new ShrinkCommandExecutedInfo
{
Service = this,
Source = source,
Command = selected.Definition.Descriptor,
RawInput = input,
Arguments = argumentMap,
Result = result
});
return result;
}
catch (Exception ex)
{
ShrinkCommandLogger.Exception(ex);
var failure = ShrinkCommandExecutionResult.Failure("命令执行失败: " + ex.Message);
OnCommandExecuted?.Invoke(new ShrinkCommandExecutedInfo
{
Service = this,
Source = source,
Command = selected.Definition.Descriptor,
RawInput = input,
Arguments = argumentMap,
Result = failure
});
return failure;
}
}
public string BuildHelp(IShrinkCommandSource? source, string? prefix = null)
{
var entries = Commands
.Where(command => !command.Hidden)
.Where(command => source == null || CanRunSourceKind(command.SourceKind, source))
.Where(command =>
{
if (string.IsNullOrWhiteSpace(command.Permission) || source == null)
return true;
return source.HasPermission(command.Permission);
})
.Where(command =>
string.IsNullOrWhiteSpace(prefix) ||
command.Path.StartsWith(prefix.Trim(), StringComparison.OrdinalIgnoreCase) ||
command.Aliases.Any(alias => alias.StartsWith(prefix.Trim(), StringComparison.OrdinalIgnoreCase)))
.OrderBy(command => command.Path, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (entries.Length == 0)
return "没有找到可用命令。";
var builder = new StringBuilder();
builder.AppendLine("可用命令:");
foreach (var command in entries)
{
builder.Append("- ");
builder.Append(command.Path);
if (!string.IsNullOrWhiteSpace(command.Description))
{
builder.Append(" : ");
builder.Append(command.Description);
}
if (command.Aliases.Count > 0)
{
builder.Append(" [别名: ");
builder.Append(string.Join(", ", command.Aliases));
builder.Append(']');
}
if (!string.IsNullOrWhiteSpace(command.Permission))
{
builder.Append(" [权限: ");
builder.Append(command.Permission);
builder.Append(']');
}
builder.AppendLine();
}
return builder.ToString().TrimEnd();
}
internal object? ConvertArgument(Type targetType, string raw)
{
if (targetType == typeof(string))
return raw;
var underlyingType = Nullable.GetUnderlyingType(targetType);
if (underlyingType != null)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
return ConvertArgument(underlyingType, raw);
}
if (targetType.IsEnum)
return Enum.Parse(targetType, raw, true);
if (_converters.TryGetValue(targetType, out var converter))
return converter(raw);
throw new InvalidOperationException($"Unsupported command argument type: {targetType.FullName}");
}
private void RegisterBuiltInCommands()
{
RegisterCommand(new ShrinkCommandRegistration
{
Path = "help",
Aliases = new[] { "?" },
Description = "显示所有可用命令。",
Handler = context => UniTask.FromResult(
ShrinkCommandExecutionResult.Success(BuildHelp(context.Source)))
});
RegisterCommand(new ShrinkCommandRegistration
{
Path = "help <path...>",
Aliases = new[] { "? <path...>" },
Description = "显示某个前缀下的可用命令。",
Hidden = true,
Handler = context =>
{
var prefix = context.GetArgument("path");
return UniTask.FromResult(
ShrinkCommandExecutionResult.Success(BuildHelp(context.Source, prefix)));
}
});
}
private ShrinkCommandExecutionResult CompleteEarly(
IShrinkCommandSource source,
string rawInput,
ShrinkCommandDescriptor? command,
IReadOnlyDictionary<string, string> arguments,
ShrinkCommandExecutionResult result)
{
OnCommandExecuted?.Invoke(new ShrinkCommandExecutedInfo
{
Service = this,
Source = source,
Command = command,
RawInput = rawInput,
Arguments = arguments,
Result = result
});
return result;
}
private static bool CanRunSourceKind(ShrinkCommandSourceKind sourceKind, IShrinkCommandSource source)
{
switch (sourceKind)
{
case ShrinkCommandSourceKind.ConsoleOnly:
return source.IsConsole;
case ShrinkCommandSourceKind.NonConsoleOnly:
return !source.IsConsole;
default:
return true;
}
}
private static IReadOnlyDictionary<string, string> BuildArgumentMap(
IReadOnlyList<string> argumentNames,
IReadOnlyList<string> values)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (var index = 0; index < argumentNames.Count && index < values.Count; index++)
map[argumentNames[index]] = values[index];
return map;
}
private List<MatchCandidate> FindMatches(IReadOnlyList<string> tokens)
{
var matches = new List<MatchCandidate>();
foreach (var command in _commands)
{
foreach (var pattern in command.Patterns)
{
if (!TryMatchPattern(pattern, tokens, out var arguments, out var literalScore))
continue;
matches.Add(new MatchCandidate
{
Definition = command,
Pattern = pattern,
RawArgumentValues = arguments,
LiteralScore = literalScore
});
}
}
return matches;
}
private MatchCandidate SelectBestMatch(IReadOnlyList<MatchCandidate> matches)
{
var selected = matches
.OrderByDescending(match => match.Pattern.Segments.Count)
.ThenByDescending(match => match.LiteralScore)
.ThenBy(match => match.Definition.Descriptor.Path, StringComparer.OrdinalIgnoreCase)
.First();
var ambiguous = matches
.Where(match => !ReferenceEquals(match, selected))
.FirstOrDefault(match =>
match.Pattern.Segments.Count == selected.Pattern.Segments.Count &&
match.LiteralScore == selected.LiteralScore);
if (ambiguous != null)
{
throw new InvalidOperationException(
$"命令匹配不明确: {selected.Pattern.Text} / {ambiguous.Pattern.Text}");
}
return selected;
}
private string BuildSuggestions(IReadOnlyList<string> tokens, IShrinkCommandSource source)
{
var entries = Commands
.Where(command => !command.Hidden)
.Where(command => CanRunSourceKind(command.SourceKind, source))
.Where(command =>
string.IsNullOrWhiteSpace(command.Permission) || source.HasPermission(command.Permission))
.Where(command => IsPrefixCandidate(command.Path, tokens) || command.Aliases.Any(alias => IsPrefixCandidate(alias, tokens)))
.OrderBy(command => command.Path, StringComparer.OrdinalIgnoreCase)
.Take(8)
.ToArray();
if (entries.Length == 0)
return string.Empty;
var builder = new StringBuilder();
builder.AppendLine("你可能想输入:");
foreach (var entry in entries)
{
builder.Append("- ");
builder.Append(entry.Path);
if (!string.IsNullOrWhiteSpace(entry.Description))
{
builder.Append(" : ");
builder.Append(entry.Description);
}
builder.AppendLine();
}
return builder.ToString().TrimEnd();
}
private static bool IsPrefixCandidate(string path, IReadOnlyList<string> inputTokens)
{
if (inputTokens.Count == 0)
return true;
var pathTokens = path.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (pathTokens.Length < inputTokens.Count)
return false;
for (var index = 0; index < inputTokens.Count; index++)
{
var input = inputTokens[index];
var pathToken = pathTokens[index];
if (pathToken.StartsWith("<", StringComparison.Ordinal) && pathToken.EndsWith(">", StringComparison.Ordinal))
continue;
if (!pathToken.StartsWith(input, StringComparison.OrdinalIgnoreCase))
return false;
}
return true;
}
private static bool TryMatchPattern(
PathPattern pattern,
IReadOnlyList<string> tokens,
out IReadOnlyList<string> arguments,
out int literalScore)
{
var rawArguments = new List<string>();
literalScore = 0;
arguments = rawArguments;
var tokenIndex = 0;
for (var segmentIndex = 0; segmentIndex < pattern.Segments.Count; segmentIndex++)
{
var segment = pattern.Segments[segmentIndex];
if (segment.IsArgument)
{
if (segment.IsGreedy)
{
if (tokenIndex >= tokens.Count)
return false;
rawArguments.Add(string.Join(" ", tokens.Skip(tokenIndex)));
tokenIndex = tokens.Count;
continue;
}
if (tokenIndex >= tokens.Count)
return false;
rawArguments.Add(tokens[tokenIndex]);
tokenIndex++;
continue;
}
if (tokenIndex >= tokens.Count)
return false;
if (!string.Equals(segment.Token, tokens[tokenIndex], StringComparison.OrdinalIgnoreCase))
return false;
literalScore++;
tokenIndex++;
}
return tokenIndex == tokens.Count;
}
private static void ValidateAliasShape(PathPattern primaryPattern, PathPattern aliasPattern)
{
if (primaryPattern.Segments.Count != aliasPattern.Segments.Count)
{
throw new InvalidOperationException(
$"Alias path shape mismatch: {aliasPattern.Text} vs {primaryPattern.Text}");
}
for (var index = 0; index < primaryPattern.Segments.Count; index++)
{
var primary = primaryPattern.Segments[index];
var alias = aliasPattern.Segments[index];
if (primary.IsArgument != alias.IsArgument || primary.IsGreedy != alias.IsGreedy)
{
throw new InvalidOperationException(
$"Alias path shape mismatch: {aliasPattern.Text} vs {primaryPattern.Text}");
}
}
}
private static PathPattern ParsePattern(string path)
{
var normalized = path?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(normalized))
throw new InvalidOperationException("Command path cannot be empty.");
var tokens = normalized.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var segments = new List<PathSegment>(tokens.Length);
for (var index = 0; index < tokens.Length; index++)
{
var token = tokens[index].Trim();
if (token.Length >= 3 && token[0] == '<' && token[token.Length - 1] == '>')
{
var name = token.Substring(1, token.Length - 2).Trim();
var isGreedy = name.EndsWith("...", StringComparison.Ordinal);
if (isGreedy)
{
name = name.Substring(0, name.Length - 3).Trim();
if (index != tokens.Length - 1)
throw new InvalidOperationException($"Greedy argument must be the last segment: {path}");
}
if (string.IsNullOrWhiteSpace(name))
throw new InvalidOperationException($"Invalid argument segment: {token}");
segments.Add(new PathSegment
{
IsArgument = true,
IsGreedy = isGreedy,
Token = name
});
continue;
}
segments.Add(new PathSegment
{
IsArgument = false,
IsGreedy = false,
Token = token
});
}
return new PathPattern
{
Text = normalized,
Segments = segments
};
}
private static List<string> Tokenize(string input)
{
var tokens = new List<string>();
var builder = new StringBuilder();
var inQuote = false;
var quoteChar = '\0';
var escaping = false;
foreach (var character in input)
{
if (escaping)
{
builder.Append(character);
escaping = false;
continue;
}
if (character == '\\')
{
escaping = true;
continue;
}
if (inQuote)
{
if (character == quoteChar)
{
inQuote = false;
continue;
}
builder.Append(character);
continue;
}
if (character == '"' || character == '\'')
{
inQuote = true;
quoteChar = character;
continue;
}
if (char.IsWhiteSpace(character))
{
if (builder.Length <= 0)
continue;
tokens.Add(builder.ToString());
builder.Clear();
continue;
}
builder.Append(character);
}
if (escaping)
builder.Append('\\');
if (inQuote)
throw new InvalidOperationException("存在未闭合的引号。");
if (builder.Length > 0)
tokens.Add(builder.ToString());
return tokens;
}
private void RegisterDefaultConverters()
{
RegisterConverter<int>(raw => int.Parse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture));
RegisterConverter<long>(raw => long.Parse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture));
RegisterConverter<float>(raw => float.Parse(raw, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture));
RegisterConverter<double>(raw => double.Parse(raw, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture));
RegisterConverter<decimal>(raw => decimal.Parse(raw, NumberStyles.Number, CultureInfo.InvariantCulture));
RegisterConverter<Guid>(raw => Guid.Parse(raw));
RegisterConverter<DateTime>(raw => DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind));
RegisterConverter<TimeSpan>(raw => TimeSpan.Parse(raw, CultureInfo.InvariantCulture));
RegisterConverter<bool>(ParseBool);
}
private static bool ParseBool(string raw)
{
if (bool.TryParse(raw, out var value))
return value;
switch (raw.Trim().ToLowerInvariant())
{
case "1":
case "yes":
case "on":
return true;
case "0":
case "no":
case "off":
return false;
default:
throw new FormatException($"无法把 `{raw}` 解析为布尔值。");
}
}
}
internal static class ShrinkCommandMethodAdapter
{
private static readonly MethodInfo AwaitUniTaskGenericMethod =
typeof(ShrinkCommandMethodAdapter).GetMethod(nameof(AwaitUniTaskGeneric),
BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly MethodInfo AwaitTaskGenericMethod =
typeof(ShrinkCommandMethodAdapter).GetMethod(nameof(AwaitTaskGeneric),
BindingFlags.NonPublic | BindingFlags.Static)!;
public static Func<ShrinkCommandContext, UniTask<ShrinkCommandExecutionResult>> BuildHandler(
object? target,
MethodInfo method,
IReadOnlyList<string> argumentNames)
{
if (method == null)
throw new ArgumentNullException(nameof(method));
var parameterDescriptors = method.GetParameters();
var specialCount = 0;
foreach (var parameter in parameterDescriptors)
{
if (parameter.ParameterType == typeof(ShrinkCommandContext) ||
typeof(IShrinkCommandSource).IsAssignableFrom(parameter.ParameterType))
{
specialCount++;
}
}
if (parameterDescriptors.Length - specialCount != argumentNames.Count)
{
throw new InvalidOperationException(
$"Command parameter count does not match path arguments: {method.DeclaringType?.FullName}.{method.Name}");
}
ValidateReturnType(method);
return async context =>
{
var invokeArguments = new object?[parameterDescriptors.Length];
var rawArgumentIndex = 0;
for (var index = 0; index < parameterDescriptors.Length; index++)
{
var parameter = parameterDescriptors[index];
if (parameter.ParameterType == typeof(ShrinkCommandContext))
{
invokeArguments[index] = context;
continue;
}
if (typeof(IShrinkCommandSource).IsAssignableFrom(parameter.ParameterType))
{
if (!parameter.ParameterType.IsInstanceOfType(context.Source))
{
throw new InvalidOperationException(
$"Command source type mismatch. Required={parameter.ParameterType.FullName}, Actual={context.Source.GetType().FullName}");
}
invokeArguments[index] = context.Source;
continue;
}
var argumentName = argumentNames[rawArgumentIndex];
var rawValue = context.GetArgument(argumentName);
try
{
invokeArguments[index] = context.Service.ConvertArgument(parameter.ParameterType, rawValue);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"参数 `{argumentName}` 解析失败: {ex.Message}", ex);
}
rawArgumentIndex++;
}
object? returnValue;
try
{
returnValue = method.Invoke(target, invokeArguments);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException ?? ex;
}
return await NormalizeReturnAsync(method.ReturnType, returnValue);
};
}
private static void ValidateReturnType(MethodInfo method)
{
var returnType = method.ReturnType;
if (returnType == typeof(void) ||
returnType == typeof(string) ||
returnType == typeof(ShrinkCommandExecutionResult) ||
returnType == typeof(UniTask) ||
returnType == typeof(Task))
{
return;
}
if (returnType.IsGenericType)
{
var genericTypeDefinition = returnType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(UniTask<>) ||
genericTypeDefinition == typeof(Task<>))
{
var innerType = returnType.GetGenericArguments()[0];
if (innerType == typeof(string) || innerType == typeof(ShrinkCommandExecutionResult))
return;
}
}
throw new InvalidOperationException(
$"Unsupported command return type: {method.DeclaringType?.FullName}.{method.Name}");
}
private static async UniTask<ShrinkCommandExecutionResult> NormalizeReturnAsync(Type returnType, object? returnValue)
{
if (returnType == typeof(void))
return ShrinkCommandExecutionResult.Success();
if (returnType == typeof(string))
return WrapObject(returnValue);
if (returnType == typeof(ShrinkCommandExecutionResult))
return WrapObject(returnValue);
if (returnType == typeof(UniTask))
{
await (UniTask)(returnValue ?? throw new InvalidOperationException("Command returned null UniTask."));
return ShrinkCommandExecutionResult.Success();
}
if (returnType == typeof(Task))
{
await (Task)(returnValue ?? throw new InvalidOperationException("Command returned null Task."));
return ShrinkCommandExecutionResult.Success();
}
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(UniTask<>))
{
return await (UniTask<ShrinkCommandExecutionResult>)AwaitUniTaskGenericMethod
.MakeGenericMethod(returnType.GetGenericArguments()[0])
.Invoke(null, new[] { returnValue! })!;
}
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
return await (Task<ShrinkCommandExecutionResult>)AwaitTaskGenericMethod
.MakeGenericMethod(returnType.GetGenericArguments()[0])
.Invoke(null, new[] { returnValue! })!;
}
throw new InvalidOperationException($"Unsupported command return type: {returnType.FullName}");
}
private static async UniTask<ShrinkCommandExecutionResult> AwaitUniTaskGeneric<T>(UniTask<T> task)
{
var result = await task;
return WrapObject(result);
}
private static async Task<ShrinkCommandExecutionResult> AwaitTaskGeneric<T>(Task<T> task)
{
var result = await task;
return WrapObject(result);
}
private static ShrinkCommandExecutionResult WrapObject(object? value)
{
if (value == null)
return ShrinkCommandExecutionResult.Success();
if (value is ShrinkCommandExecutionResult commandResult)
return commandResult;
if (value is string text)
return ShrinkCommandExecutionResult.Success(text);
throw new InvalidOperationException(
$"Unsupported command result payload type: {value.GetType().FullName}");
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5b65f098031f3943890f8eb7cb8e4d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7d9c2b8d57862d2408d63b64fee53ff2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using System;
namespace ShrinkCommand
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkCommandSubscriberAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class ShrinkCommandAttribute : Attribute
{
public ShrinkCommandAttribute(string path)
{
Path = string.IsNullOrWhiteSpace(path) ? string.Empty : path.Trim();
}
public string Path { get; }
public string Description { get; set; } = string.Empty;
public string Permission { get; set; } = string.Empty;
public ShrinkCommandSourceKind SourceKind { get; set; } = ShrinkCommandSourceKind.Any;
public bool Hidden { get; set; }
public string[] Aliases { get; set; } = Array.Empty<string>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7bdd28a64d1899849b6cfce21ad2e91f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace ShrinkCommand
{
public static class ShrinkCommandConstants
{
public const string DefaultServiceName = "default";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 438cadef623271d4d82e99c08a4e3b68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+144
View File
@@ -0,0 +1,144 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkCommand
{
public enum ShrinkCommandSourceKind
{
Any = 0,
ConsoleOnly = 1,
NonConsoleOnly = 2
}
public interface IShrinkCommandSource
{
string SourceId { get; }
string DisplayName { get; }
bool IsConsole { get; }
bool HasPermission(string permission);
UniTask WriteLineAsync(string message, CancellationToken cancellationToken = default);
}
public sealed class ShrinkCommandExecutionResult
{
public bool IsSuccess { get; private set; }
public string Message { get; private set; } = string.Empty;
public static ShrinkCommandExecutionResult Success(string message = "")
{
return new ShrinkCommandExecutionResult
{
IsSuccess = true,
Message = message ?? string.Empty
};
}
public static ShrinkCommandExecutionResult Failure(string message)
{
return new ShrinkCommandExecutionResult
{
IsSuccess = false,
Message = message ?? string.Empty
};
}
}
public sealed class ShrinkCommandDescriptor
{
public string Path { get; set; } = string.Empty;
public IReadOnlyList<string> Aliases { get; set; } = Array.Empty<string>();
public string Description { get; set; } = string.Empty;
public string Permission { get; set; } = string.Empty;
public ShrinkCommandSourceKind SourceKind { get; set; }
public bool Hidden { get; set; }
public IReadOnlyList<string> GetAllPaths()
{
if (Aliases == null || Aliases.Count == 0)
return new[] { Path };
return new[] { Path }.Concat(Aliases).ToArray();
}
}
public sealed class ShrinkCommandRegistration
{
public string Path { get; set; } = string.Empty;
public string[] Aliases { get; set; } = Array.Empty<string>();
public string Description { get; set; } = string.Empty;
public string Permission { get; set; } = string.Empty;
public ShrinkCommandSourceKind SourceKind { get; set; } = ShrinkCommandSourceKind.Any;
public bool Hidden { get; set; }
public Func<ShrinkCommandContext, UniTask<ShrinkCommandExecutionResult>> Handler { get; set; } =
_ => UniTask.FromResult(ShrinkCommandExecutionResult.Success());
}
public sealed class ShrinkCommandContext
{
private readonly IReadOnlyDictionary<string, string> _arguments;
internal ShrinkCommandContext(
ShrinkCommandService service,
IShrinkCommandSource source,
ShrinkCommandDescriptor command,
string rawInput,
IReadOnlyDictionary<string, string> arguments,
CancellationToken cancellationToken)
{
Service = service;
Source = source;
Command = command;
RawInput = rawInput ?? string.Empty;
_arguments = arguments ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
CancellationToken = cancellationToken;
}
public ShrinkCommandService Service { get; }
public IShrinkCommandSource Source { get; }
public ShrinkCommandDescriptor Command { get; }
public string RawInput { get; }
public CancellationToken CancellationToken { get; }
public IReadOnlyDictionary<string, string> Arguments => _arguments;
public string GetArgument(string name, string fallback = "")
{
if (string.IsNullOrWhiteSpace(name))
return fallback;
return _arguments.TryGetValue(name.Trim(), out var value) ? value : fallback;
}
public UniTask ReplyAsync(string message)
{
return Source.WriteLineAsync(message ?? string.Empty, CancellationToken);
}
}
public sealed class ShrinkCommandExecutingInfo
{
public ShrinkCommandService Service { get; set; } = null!;
public IShrinkCommandSource Source { get; set; } = null!;
public ShrinkCommandDescriptor Command { get; set; } = null!;
public string RawInput { get; set; } = string.Empty;
public IReadOnlyDictionary<string, string> Arguments { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed class ShrinkCommandExecutedInfo
{
public ShrinkCommandService Service { get; set; } = null!;
public IShrinkCommandSource Source { get; set; } = null!;
public ShrinkCommandDescriptor? Command { get; set; }
public string RawInput { get; set; } = string.Empty;
public IReadOnlyDictionary<string, string> Arguments { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public ShrinkCommandExecutionResult Result { get; set; } = ShrinkCommandExecutionResult.Success();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 895c916a18310be4fb635652a6c84e9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3a6aafcf1653c874c839f81d45002298
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ShrinkCommand
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ShrinkCommandStaticRegistryAttribute : Attribute
{
public ShrinkCommandStaticRegistryAttribute(params Type[] subscriberTypes)
{
SubscriberTypes = subscriberTypes ?? Array.Empty<Type>();
}
public Type[] SubscriberTypes { get; }
}
internal static class ShrinkCommandGeneratedRegistry
{
public static IReadOnlyList<Type> GetStaticSubscriberTypes()
{
var types = new List<Type>();
var seen = new HashSet<Type>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
object[] attributes;
try
{
attributes = assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false);
}
catch
{
continue;
}
foreach (var attribute in attributes.OfType<ShrinkCommandStaticRegistryAttribute>())
{
foreach (var subscriberType in attribute.SubscriberTypes ?? Array.Empty<Type>())
{
if (subscriberType == null || !seen.Add(subscriberType))
continue;
types.Add(subscriberType);
}
}
}
return types;
}
public static void RegisterAll(ShrinkCommandService service)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
ShrinkCommandRegHelper.RegisterStaticCommandSubscriberTypes(service, GetStaticSubscriberTypes());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf0049e5e80e56c469f020266fbe0ace
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+116
View File
@@ -0,0 +1,116 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ShrinkCommand
{
public static class ShrinkCommandRegHelper
{
public static void RegisterStaticCommands(ShrinkCommandService service)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
RegisterStaticCommandSubscriberTypes(service, ShrinkCommandGeneratedRegistry.GetStaticSubscriberTypes());
}
public static void RegisterCommands(ShrinkCommandService service, object? target)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
RegisterCommandsInternal(service, target);
}
public static void RegisterStaticCommandSubscriberTypes(
ShrinkCommandService service,
IEnumerable<Type> subscriberTypes)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
if (subscriberTypes == null)
throw new ArgumentNullException(nameof(subscriberTypes));
foreach (var type in subscriberTypes)
{
if (type == null || type.GetCustomAttribute<ShrinkCommandSubscriberAttribute>(false) == null)
continue;
ScanMethodsAndRegister(service, null, type,
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
}
}
private static void RegisterCommandsInternal(ShrinkCommandService service, object? target)
{
if (target == null)
return;
var ownerType = target.GetType();
if (ownerType.GetCustomAttribute<ShrinkCommandSubscriberAttribute>(false) == null)
return;
ScanMethodsAndRegister(service, target, ownerType,
ownerType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
}
private static void ScanMethodsAndRegister(
ShrinkCommandService service,
object? target,
Type ownerType,
MethodInfo[] methods)
{
foreach (var method in methods)
{
if (target != null && method.IsStatic)
continue;
var attributes = method.GetCustomAttributes(typeof(ShrinkCommandAttribute), false)
.Cast<ShrinkCommandAttribute>()
.ToArray();
if (attributes.Length == 0)
continue;
foreach (var attribute in attributes)
{
try
{
service.RegisterCommand(new ShrinkCommandRegistration
{
Path = attribute.Path,
Aliases = attribute.Aliases ?? Array.Empty<string>(),
Description = attribute.Description,
Permission = attribute.Permission,
SourceKind = attribute.SourceKind,
Hidden = attribute.Hidden,
Handler = ShrinkCommandMethodAdapter.BuildHandler(target, method,
ParseArgumentNames(attribute.Path))
});
}
catch (Exception ex)
{
ShrinkCommandLogger.Warn(
$"[ShrinkCommand] Failed to register {ownerType.FullName}.{method.Name}: {ex.Message}");
}
}
}
}
private static string[] ParseArgumentNames(string path)
{
return path.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Where(token => token.Length >= 3 && token[0] == '<' && token[token.Length - 1] == '>')
.Select(token =>
{
var name = token.Substring(1, token.Length - 2).Trim();
if (name.EndsWith("...", StringComparison.Ordinal))
name = name.Substring(0, name.Length - 3).Trim();
return name;
})
.ToArray();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14f2978c6779a9842bb62ac369b0cf53
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ShrinkCommand.Runtime",
"rootNamespace": "ShrinkCommand",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 00b9caa355e76a348b031a7aafa927a4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+23
View File
@@ -0,0 +1,23 @@
{
"name": "com.cneicy.shrink-command",
"version": "0.2.0",
"displayName": "ShrinkCommand",
"description": "独立于网络层的 Unity 命令系统,提供路径式命令、权限、帮助输出与属性式注册。",
"unity": "2022.3",
"dependencies": {
"com.cneicy.shrink-shared-codegen": "0.1.0",
"com.cysharp.unitask": "2.5.10"
},
"keywords": [
"command",
"console",
"debug",
"tooling"
],
"author": {
"name": "cneicy",
"url": "https://git.crash.work/ShrinkSDK"
},
"documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand",
"changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand/src/branch/main/CHANGELOG.md"
}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c8ebc8c4a7e8b584b940810fed797795
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: