Files
Genarrative/plugins/agc-unity-editor/dotnet/vendor/Shared/Dcu.cs
T
kdletters 33f5ad68bf
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Native shell tests (push) Successful in 10m54s
Project CI / Backend tests (push) Successful in 12m42s
Project CI / Frontend tests (push) Successful in 13m4s
Project CI / AI game creator shell web tests (push) Successful in 5m1s
Project CI / Repository checks (push) Successful in 12m48s
接入 DotCraft Unity 编辑器插件与受控执行链路 (#423)
AGC 原有插件系统无法直接操作已打开的 Unity Editor。本变更增加内置 `agc-unity-editor`,在 Windows x64 / Unity Mono 上支持当前项目探测、连接与 C# 执行,不向 Unity 工程安装 UPM 桥接包。

## 主要变更

- 固定复用 DotCraft.Unity 0.4.3 的 Attach 核心,提供自包含 .NET helper,保留上游许可证、来源及修改记录。
- GUI、Runtime、DirectProject 共用 Runner 执行服务;补齐项目身份、并发、总期限、回执确认与持久不确定状态阻断。
- 现有打开项目入口支持 Unity,按项目类型及开关暴露插件和 Agent 工具。
- Windows 构建准备 helper 并随包分发;插件 JS/Rust 测试接入现有 CI 组,Jenkins 增加 .NET 10 工具链预检。

## 验证

- .NET helper 27 项测试、自包含发布及最小环境协议 smoke 通过。
- Unity 6000.3.7f1 实机验证通过:连接、C# 执行、编译错误修复、断连重连、Domain Reload 后重新握手;真实 Runner 的 ACK、并发拒绝和跨重启阻断通过。
- 宿主 Unity、PluginHost、Cocos、MCP、工具目录与引擎识别定向回归通过;前端类型检查、插件 JS/Rust、CI 配置、格式、编码和文档门禁通过。

Linux CI 不代替 Windows helper/实机验证;发行安装包 UI smoke、其它 Unity 版本和 Unity CoreCLR 未验证。Unity 演示工程中的场景和组件已撤销,不在此 PR 范围内。

---------

Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/423
2026-09-19 12:29:27 +08:00

539 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UComponent = UnityEngine.Component;
using UObject = UnityEngine.Object;
#if DOTCRAFT_ATTACH
namespace DotCraft.Unity
#else
namespace DotCraft.Editor
#endif
{
/// <summary>
/// Small helper surface for Unity Editor snippets executed through dotcraft-unity.
/// </summary>
public static class Dcu
{
private const BindingFlags AnyInstance =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private const BindingFlags AnyStatic =
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
/// <summary>
/// Finds a loaded type by assembly-qualified name, full name, or unique short name.
/// </summary>
public static Type Type(string typeName, bool throwIfMissing = true)
{
if (string.IsNullOrWhiteSpace(typeName))
return throwIfMissing ? throw new InvalidOperationException("Type name is empty.") : null;
var trimmed = typeName.Trim();
var direct = System.Type.GetType(trimmed, false);
if (direct != null)
return direct;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic))
{
Type exact;
try
{
exact = assembly.GetType(trimmed, false);
}
catch
{
continue;
}
if (exact != null)
return exact;
}
var matches = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(GetLoadableTypes)
.Where(t => string.Equals(t.FullName, trimmed, StringComparison.Ordinal)
|| string.Equals(t.Name, trimmed, StringComparison.Ordinal))
.Distinct()
.OrderBy(t => t.FullName, StringComparer.Ordinal)
.Take(4)
.ToArray();
if (matches.Length == 1)
return matches[0];
if (matches.Length > 1)
{
throw new InvalidOperationException(
$"Type '{trimmed}' is ambiguous: {string.Join(", ", matches.Select(t => t.FullName))}");
}
if (throwIfMissing)
throw new InvalidOperationException($"Type not found: {trimmed}");
return null;
}
/// <summary>
/// Finds scene components of a loaded component type by type name.
/// </summary>
public static UComponent[] Components(string typeName, bool includeInactive = true)
{
return Components(Type(typeName), includeInactive);
}
/// <summary>
/// Finds scene components of a loaded component type.
/// </summary>
public static UComponent[] Components(Type componentType, bool includeInactive = true)
{
if (componentType == null)
throw new InvalidOperationException("Component type is null.");
if (!typeof(UComponent).IsAssignableFrom(componentType))
throw new InvalidOperationException($"{componentType.FullName} is not a Unity Component type.");
return FindComponentObjects(componentType, includeInactive)
.OfType<UComponent>()
.Where(c => c != null && !EditorUtility.IsPersistent(c) && c.gameObject.scene.IsValid())
.OrderBy(c => c.gameObject.scene.path, StringComparer.Ordinal)
.ThenBy(c => TransformPath(c.transform), StringComparer.Ordinal)
.ThenBy(c => c.GetInstanceID())
.ToArray();
}
private static UObject[] FindComponentObjects(Type componentType, bool includeInactive)
{
#if UNITY_6000_0_OR_NEWER
return UObject.FindObjectsByType(
componentType,
includeInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude,
FindObjectsSortMode.None);
#else
return includeInactive
? Resources.FindObjectsOfTypeAll(componentType)
: UObject.FindObjectsOfType(componentType);
#endif
}
/// <summary>
/// Reads a field or property from an object instance or a static type.
/// </summary>
public static object Get(object targetOrType, string memberName)
{
var type = ResolveTarget(targetOrType, out var target, out var flags);
var field = FindField(type, memberName, flags);
if (field != null)
return field.GetValue(target);
var property = FindProperty(type, memberName, flags);
if (property != null)
return property.GetValue(target, null);
throw MissingMember(type, memberName);
}
/// <summary>
/// Writes a field or property on an object instance or a static type.
/// </summary>
public static void Set(object targetOrType, string memberName, object value)
{
var type = ResolveTarget(targetOrType, out var target, out var flags);
var field = FindField(type, memberName, flags);
if (field != null)
{
field.SetValue(target, ConvertValue(value, field.FieldType));
return;
}
var property = FindProperty(type, memberName, flags);
if (property != null && property.CanWrite)
{
property.SetValue(target, ConvertValue(value, property.PropertyType), null);
return;
}
throw MissingMember(type, memberName);
}
/// <summary>
/// Calls a method on an object instance or a static type.
/// </summary>
public static object Call(object targetOrType, string methodName, params object[] args)
{
var type = ResolveTarget(targetOrType, out var target, out var flags);
var arguments = args ?? Array.Empty<object>();
var candidates = EnumerateMethods(type, flags)
.Where(m => string.Equals(m.Name, methodName, StringComparison.Ordinal)
&& !m.ContainsGenericParameters)
.Select(m => TryBuildInvocation(m, arguments, out var converted, out var score)
? new MethodMatch(m, converted, score)
: null)
.Where(m => m != null)
.OrderBy(m => m.Score)
.ToArray();
if (candidates.Length == 0)
throw MissingMember(type, methodName);
var best = candidates[0];
if (candidates.Length > 1 && candidates[1].Score == best.Score)
throw new InvalidOperationException($"Method '{methodName}' is ambiguous on {type.FullName}.");
try
{
return best.Method.Invoke(target, best.Arguments);
}
catch (TargetInvocationException ex) when (ex.InnerException != null)
{
throw new InvalidOperationException(
$"{methodName} threw {ex.InnerException.GetType().Name}: {ex.InnerException.Message}",
ex.InnerException);
}
}
/// <summary>
/// Lists fields, properties, and methods on a loaded type.
/// </summary>
public static DcuMember[] Members(
string typeName,
string filter = null,
bool includeNonPublic = true,
int limit = 80)
{
return Members(Type(typeName), filter, includeNonPublic, limit);
}
/// <summary>
/// Lists fields, properties, and methods on an object instance or a static type.
/// </summary>
public static DcuMember[] Members(
object targetOrType,
string filter = null,
bool includeNonPublic = true,
int limit = 80)
{
var type = ResolveTarget(targetOrType, out _, out _);
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
if (includeNonPublic)
flags |= BindingFlags.NonPublic;
var loweredFilter = string.IsNullOrWhiteSpace(filter) ? null : filter.Trim();
var max = Math.Max(1, limit);
var members = new List<DcuMember>();
foreach (var field in EnumerateFields(type, flags))
{
if (!NameMatches(field.Name, loweredFilter))
continue;
members.Add(new DcuMember("field", field.Name, FormatType(field.FieldType), field.IsStatic));
}
foreach (var property in EnumerateProperties(type, flags))
{
if (!NameMatches(property.Name, loweredFilter))
continue;
var accessor = property.GetGetMethod(true) ?? property.GetSetMethod(true);
members.Add(new DcuMember(
"property",
property.Name,
FormatType(property.PropertyType),
accessor != null && accessor.IsStatic,
property.CanRead,
property.CanWrite));
}
foreach (var method in EnumerateMethods(type, flags).Where(m => !m.IsSpecialName))
{
if (!NameMatches(method.Name, loweredFilter))
continue;
members.Add(new DcuMember(
"method",
method.Name,
FormatType(method.ReturnType),
method.IsStatic,
parameters: string.Join(", ", method.GetParameters().Select(FormatParameter))));
}
return members
.OrderBy(m => m.Kind, StringComparer.Ordinal)
.ThenBy(m => m.Name, StringComparer.Ordinal)
.Take(max)
.ToArray();
}
private static Type ResolveTarget(object targetOrType, out object target, out BindingFlags flags)
{
if (targetOrType == null)
throw new InvalidOperationException("Target is null.");
if (targetOrType is Type type)
{
target = null;
flags = AnyStatic;
return type;
}
target = targetOrType;
flags = AnyInstance;
return targetOrType.GetType();
}
private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null);
}
catch
{
return Array.Empty<Type>();
}
}
private static FieldInfo FindField(Type type, string name, BindingFlags flags)
{
return EnumerateFields(type, flags)
.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal));
}
private static PropertyInfo FindProperty(Type type, string name, BindingFlags flags)
{
return EnumerateProperties(type, flags)
.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.Ordinal)
&& p.GetIndexParameters().Length == 0
&& p.CanRead);
}
private static IEnumerable<FieldInfo> EnumerateFields(Type type, BindingFlags flags)
{
for (var current = type; current != null; current = current.BaseType)
{
foreach (var field in current.GetFields(flags | BindingFlags.DeclaredOnly))
yield return field;
}
}
private static IEnumerable<PropertyInfo> EnumerateProperties(Type type, BindingFlags flags)
{
for (var current = type; current != null; current = current.BaseType)
{
foreach (var property in current.GetProperties(flags | BindingFlags.DeclaredOnly))
yield return property;
}
}
private static IEnumerable<MethodInfo> EnumerateMethods(Type type, BindingFlags flags)
{
for (var current = type; current != null; current = current.BaseType)
{
foreach (var method in current.GetMethods(flags | BindingFlags.DeclaredOnly))
yield return method;
}
}
private static bool TryBuildInvocation(
MethodInfo method,
object[] args,
out object[] converted,
out int score)
{
var parameters = method.GetParameters();
converted = null;
score = 0;
if (parameters.Length != args.Length)
return false;
var values = new object[args.Length];
for (var i = 0; i < args.Length; i++)
{
if (!TryConvertValue(args[i], parameters[i].ParameterType, out values[i], out var itemScore))
return false;
score += itemScore;
}
converted = values;
return true;
}
private static object ConvertValue(object value, Type targetType)
{
if (TryConvertValue(value, targetType, out var converted, out _))
return converted;
throw new InvalidOperationException($"Cannot convert value to {FormatType(targetType)}.");
}
private static bool TryConvertValue(object value, Type targetType, out object converted, out int score)
{
converted = null;
score = 0;
var nullableType = Nullable.GetUnderlyingType(targetType);
var actualType = nullableType ?? targetType;
if (value == null)
{
if (!targetType.IsValueType || nullableType != null)
return true;
return false;
}
if (actualType.IsInstanceOfType(value))
{
converted = value;
return true;
}
try
{
if (actualType.IsEnum)
{
converted = value is string text
? Enum.Parse(actualType, text)
: Enum.ToObject(actualType, value);
score = 2;
return true;
}
if (actualType == typeof(string))
{
converted = value.ToString();
score = 2;
return true;
}
if (value is IConvertible && typeof(IConvertible).IsAssignableFrom(actualType))
{
converted = Convert.ChangeType(value, actualType);
score = 2;
return true;
}
}
catch
{
return false;
}
return false;
}
private static InvalidOperationException MissingMember(Type type, string memberName)
{
return new InvalidOperationException($"Member not found on {type.FullName}: {memberName}");
}
private static bool NameMatches(string name, string filter)
{
return filter == null || name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string TransformPath(Transform transform)
{
if (transform == null)
return string.Empty;
var names = new Stack<string>();
for (var current = transform; current != null; current = current.parent)
names.Push(current.name);
return "/" + string.Join("/", names);
}
private static string FormatType(Type type)
{
return type == null ? string.Empty : type.FullName ?? type.Name;
}
private static string FormatParameter(ParameterInfo parameter)
{
return $"{FormatType(parameter.ParameterType)} {parameter.Name}";
}
private sealed class MethodMatch
{
public MethodMatch(MethodInfo method, object[] arguments, int score)
{
Method = method;
Arguments = arguments;
Score = score;
}
public MethodInfo Method { get; }
public object[] Arguments { get; }
public int Score { get; }
}
}
/// <summary>
/// Describes a reflected member returned by <see cref="Dcu.Members(object, string, bool, int)"/>.
/// </summary>
public sealed class DcuMember
{
/// <summary>
/// Creates a reflected member description.
/// </summary>
public DcuMember(
string kind,
string name,
string typeName,
bool isStatic,
bool canRead = true,
bool canWrite = false,
string parameters = "")
{
Kind = kind;
Name = name;
TypeName = typeName;
IsStatic = isStatic;
CanRead = canRead;
CanWrite = canWrite;
Parameters = parameters ?? string.Empty;
}
/// <summary>
/// Member kind: field, property, or method.
/// </summary>
public string Kind { get; }
/// <summary>
/// Member name.
/// </summary>
public string Name { get; }
/// <summary>
/// Field, property, or return type name.
/// </summary>
public string TypeName { get; }
/// <summary>
/// Whether the member is static.
/// </summary>
public bool IsStatic { get; }
/// <summary>
/// Whether the member can be read.
/// </summary>
public bool CanRead { get; }
/// <summary>
/// Whether the member can be written.
/// </summary>
public bool CanWrite { get; }
/// <summary>
/// Method parameter list, or empty for fields and properties.
/// </summary>
public string Parameters { get; }
}
}