Compare commits

..

2 Commits

Author SHA1 Message Date
kdletters b96473836d 补齐 Unity 与 Godot 常用操作指导
新增两种编辑器的内置 Skill 和场景、资源、UI、保存撤销示例
接入 DirectProject 按需读取与 Runtime 同源操作参考
补齐指南原文实机测试、安装投影及工具说明完整性检查
记录 Godot 图形补验结果并保留尚未验收的边界
2026-09-20 17:26:34 +08:00
kdletters 54d0fb75ea 接入 Godot 编辑器插件与受控执行链路
新增 GDExtension 自动引导、GDScript 执行和实例隔离缓存
接入 AGC 插件开关、Runner、Agent 工具与权限审计
完善执行回执确认、不确定状态阻断及卸载恢复
补齐 Windows 分发资源、定向测试与实机验收文档
2026-09-20 16:35:04 +08:00
88 changed files with 12810 additions and 594 deletions
+17
View File
@@ -302,6 +302,23 @@ jobs:
sleep $((attempt * 2))
done
- name: Prepare Godot plugin Rust dependencies
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo 'Godot plugin Cargo dependency fetch failed after 5 attempts.' >&2
exit 1
fi
sleep $((attempt * 2))
done
- name: Run AI game creator shell shared crate gates
run: npm run check:native-shells:agc-rust-crates
@@ -106,6 +106,7 @@ export const agcReleasePathPatterns = [
'server-rs/crates/',
'plugins/agc-cocos-editor/',
'plugins/agc-unity-editor/',
'plugins/agc-godot-editor/',
'apps/desktop-shell/src-tauri/icons/',
'package.json',
'package-lock.json',
@@ -452,7 +452,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
spawn: (_binary, command) => {
assert.ok(
command.includes(
'--features=cocos-editor-execute,unity-editor-execute',
'--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute',
),
);
assert.ok(command.includes('user-config.json'));
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
export function defaultEditorFeatures(target) {
return target === 'win32' || target.includes('windows')
? ['cocos-editor-execute', 'unity-editor-execute']
? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute']
: [];
}
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
[
'build',
'--features=cocos-editor-execute,unity-editor-execute',
'--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute',
'--target',
'x86_64-pc-windows-msvc',
],
@@ -9,7 +9,9 @@ export const EXPECTED_SKILL_NAMES = Object.freeze([
'agc-browser-playtest',
'agc-client-projection',
'agc-game-production-workflow',
'agc-godot-editor',
'agc-project-structure',
'agc-unity-editor',
'agc-web-game-development',
'taonier-art-assets',
]);
+13
View File
@@ -1755,6 +1755,7 @@ dependencies = [
"editor-adapter-api",
"futures",
"getrandom 0.3.4",
"godot-editor-bridge",
"http",
"image",
"jsonschema",
@@ -1981,6 +1982,18 @@ dependencies = [
"system-deps",
]
[[package]]
name = "godot-editor-bridge"
version = "0.1.0"
dependencies = [
"editor-adapter-api",
"serde",
"serde_json",
"sha2",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "gtk"
version = "0.18.2"
@@ -13,6 +13,7 @@ cocos-editor = ["cocos-editor-bridge/process-discovery"]
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
unity-editor-execute = []
godot-editor-execute = []
[build-dependencies]
serde = { version = "1", features = ["derive"] }
@@ -29,6 +30,7 @@ agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
unity-editor-bridge = { path = "../../../plugins/agc-unity-editor/native/unity-editor-bridge" }
godot-editor-bridge = { path = "../../../plugins/agc-godot-editor/native/godot-editor-bridge" }
base64 = "0.22"
axum = "0.8"
chromiumoxide = "0.9.1"
@@ -2,6 +2,8 @@
mod codex_bundle;
#[path = "build_support/frontend_dist_guard.rs"]
mod frontend_dist_guard;
#[path = "build_support/godot_bundle.rs"]
mod godot_bundle;
#[path = "build_support/runtime_prompt_bundle.rs"]
mod runtime_prompt_bundle;
@@ -197,6 +199,7 @@ fn main() {
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
stage_bundled_codex_cli(&manifest_dir);
prepare_unity_editor_helper(&manifest_dir);
prepare_godot_editor_extension(&manifest_dir);
stage_plugin_workspace(&manifest_dir);
stage_cocos_editor_payload(&manifest_dir);
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
@@ -368,6 +371,39 @@ fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBu
}
}
fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_GODOT_EDITOR_EXECUTE");
if env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_none()
|| env::var("TARGET").expect("Cargo TARGET") != "x86_64-pc-windows-msvc"
{
return;
}
let root = manifest_dir.join("../../../plugins/agc-godot-editor/native/gdextension");
for source in godot_bundle::source_files(&root).unwrap_or_else(|error| panic!("{error}")) {
println!("cargo:rerun-if-changed={}", source.display());
}
assert!(
cfg!(windows),
"构建 Godot 原生扩展需要 Windows x64 C 编译器"
);
let status = std::process::Command::new("powershell.exe")
// Cargo 可能从 PowerShell 7 启动,Windows PowerShell 应使用自身模块目录。
.env_remove("PSModulePath")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
])
.arg(root.join("build.ps1"))
.current_dir(&root)
.status()
.expect("无法启动 Godot 原生扩展构建脚本");
assert!(status.success(), "Godot 原生扩展构建失败");
godot_bundle::validate(&root).unwrap_or_else(|error| panic!("{error}"));
}
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
///
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
@@ -428,6 +464,15 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
}
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
}
if name == "agc-godot-editor" {
godot_bundle::stage(
&plugin_root.join("native/gdextension"),
&destination.join("native/gdextension"),
&target,
env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_some(),
)
.unwrap_or_else(|error| panic!("{error}"));
}
println!("cargo:rerun-if-changed={}", plugin_root.display());
}
}
@@ -0,0 +1,215 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub const BUNDLE_FILES: [&str; 4] = [
"bin/win-x64/agc_godot_editor.dll",
"bin/win-x64/metadata.json",
"vendor/LICENSE.txt",
"vendor/provenance.json",
];
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("Godot 资源不可读 {}{error}", path.display()))?;
#[cfg(windows)]
let linked = {
use std::os::windows::fs::MetadataExt;
metadata.file_attributes() & 0x400 != 0
};
#[cfg(not(windows))]
let linked = metadata.file_type().is_symlink();
if linked {
return Err(format!("Godot 资源不能经过链接:{}", path.display()));
}
Ok(metadata)
}
fn read_bundle_file(root: &Path, relative: &str) -> Result<Vec<u8>, String> {
plain_metadata(root)?;
let mut path = root.to_path_buf();
for component in Path::new(relative).components() {
path.push(component);
plain_metadata(&path)?;
}
let metadata = plain_metadata(&path)?;
if !metadata.is_file() || metadata.len() == 0 {
return Err(format!("Godot 随包资源缺失或为空:{}", path.display()));
}
if relative.ends_with("metadata.json") && metadata.len() > 64 * 1024 {
return Err("Godot 构建元数据超过 64 KiB".to_string());
}
fs::read(&path).map_err(|error| format!("读取 Godot 资源失败:{error}"))
}
pub fn validate(root: &Path) -> Result<Vec<(&'static str, Vec<u8>)>, String> {
let files = BUNDLE_FILES
.iter()
.map(|relative| read_bundle_file(root, relative).map(|bytes| (*relative, bytes)))
.collect::<Result<Vec<_>, _>>()?;
let metadata: serde_json::Value = serde_json::from_slice(&files[1].1)
.map_err(|error| format!("Godot 构建元数据无效:{error}"))?;
for (field, expected) in [
("protocol", "agc.godot.editor.v1"),
("platform", "windows"),
("arch", "x86_64"),
("entrySymbol", "agc_godot_editor_init"),
("minimumGodotVersion", "4.7"),
] {
if metadata[field].as_str() != Some(expected) {
return Err(format!("Godot 构建元数据 {field} 不匹配"));
}
}
if !metadata["buildId"].as_str().is_some_and(|value| {
value.strip_prefix("sha256:").is_some_and(|digest| {
digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
})
}) {
return Err("Godot 构建身份无效".to_string());
}
let actual_sha256 = format!("{:x}", Sha256::digest(&files[0].1));
if metadata["sha256"].as_str() != Some(actual_sha256.as_str()) {
return Err("Godot DLL 与构建元数据 SHA256 不匹配".to_string());
}
Ok(files)
}
pub fn stage(root: &Path, destination: &Path, target: &str, enabled: bool) -> Result<(), String> {
if target != "x86_64-pc-windows-msvc" || !enabled {
return Ok(());
}
for (relative, bytes) in validate(root)? {
let path = destination.join(relative);
fs::create_dir_all(path.parent().expect("Godot resource parent"))
.map_err(|error| format!("创建 Godot 资源目录失败:{error}"))?;
fs::write(&path, bytes).map_err(|error| format!("写入 Godot 资源失败:{error}"))?;
}
Ok(())
}
pub fn source_files(root: &Path) -> Result<Vec<PathBuf>, String> {
plain_metadata(root)?;
let mut sources = Vec::new();
for entry in fs::read_dir(root).map_err(|error| format!("读取 Godot 源码失败:{error}"))?
{
let entry = entry.map_err(|error| format!("读取 Godot 源码目录项失败:{error}"))?;
if matches!(entry.file_name().to_str(), Some("bin" | ".build")) {
continue;
}
let metadata = plain_metadata(&entry.path())?;
if metadata.is_dir() {
sources.extend(source_files(&entry.path())?);
} else if metadata.is_file() {
sources.push(entry.path());
}
}
sources.sort();
Ok(sources)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(root: &Path) {
for relative in BUNDLE_FILES {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, b"fixture").unwrap();
}
fs::write(
root.join(BUNDLE_FILES[1]),
serde_json::to_vec(&serde_json::json!({
"protocol": "agc.godot.editor.v1",
"platform": "windows",
"arch": "x86_64",
"entrySymbol": "agc_godot_editor_init",
"minimumGodotVersion": "4.7",
"buildId": format!("sha256:{}", "a".repeat(64)),
"sha256": format!("{:x}", Sha256::digest(b"fixture")),
}))
.unwrap(),
)
.unwrap();
}
#[test]
fn stage_only_verified_windows_runtime_and_not_build_inputs() {
let source = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
fixture(source.path());
fs::write(source.path().join("bridge.gd"), "source").unwrap();
fs::write(source.path().join("bin/win-x64/extra.dll"), "excluded").unwrap();
stage(
source.path(),
destination.path(),
"x86_64-pc-windows-msvc",
true,
)
.unwrap();
for relative in BUNDLE_FILES {
assert_eq!(
fs::read(source.path().join(relative)).unwrap(),
fs::read(destination.path().join(relative)).unwrap()
);
}
assert!(!destination.path().join("bridge.gd").exists());
assert!(!destination.path().join("bin/win-x64/extra.dll").exists());
}
#[test]
fn unsupported_or_disabled_targets_need_no_native_artifacts() {
let destination = tempfile::tempdir().unwrap();
for (target, enabled) in [
("aarch64-apple-darwin", true),
("x86_64-apple-darwin", true),
("x86_64-unknown-linux-gnu", true),
("aarch64-pc-windows-msvc", true),
("x86_64-pc-windows-msvc", false),
] {
stage(
Path::new("missing-godot-native"),
destination.path(),
target,
enabled,
)
.unwrap();
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
}
}
#[test]
fn incomplete_or_tampered_bundle_fails_before_copying() {
let source = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
fixture(source.path());
fs::write(source.path().join(BUNDLE_FILES[0]), b"tampered").unwrap();
assert!(stage(
source.path(),
destination.path(),
"x86_64-pc-windows-msvc",
true
)
.unwrap_err()
.contains("SHA256"));
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
fixture(source.path());
fs::remove_file(source.path().join("vendor/LICENSE.txt")).unwrap();
assert!(validate(source.path()).is_err());
}
#[test]
fn source_watch_list_excludes_build_outputs() {
let source = tempfile::tempdir().unwrap();
fixture(source.path());
fs::create_dir(source.path().join(".build")).unwrap();
fs::write(source.path().join(".build/bridge.obj"), "generated").unwrap();
fs::write(source.path().join("bridge.gd"), "source").unwrap();
let sources = source_files(source.path()).unwrap();
assert_eq!(sources.len(), 3);
assert!(sources.contains(&source.path().join("bridge.gd")));
assert!(!sources.iter().any(|path| path
.components()
.any(|component| component.as_os_str() == "bin" || component.as_os_str() == ".build")));
}
}
@@ -0,0 +1,14 @@
---
name: agc-godot-editor
description: 在 AGC 中通过已连接的 Godot 编辑器读取、修改和保存场景、节点、资源与 UI,运行项目并诊断 GDScript 执行结果。
---
# Godot 编辑器操作
使用当前环境实际提供的 Godot 执行工具:DirectProject 为 `agc_godot_execute`Runtime 使用 `godot.editor.execute` 对应的已发现工具。执行载荷只含 GDScript **函数体** `code`Direct 传 `{code:...}`Runtime 按实际 schema 包装为 `{reason:"...",input:{code:...}}`。项目、编辑器和连接身份由 AGC 管理。
开始操作前读取 [Godot 编辑器常用操作](references/【操作指南】Godot编辑器常用操作-2026-09-20.md),按当前任务选取查询、节点、撤销、资源、UI、保存或运行示例。先查询真实编辑场景与目标节点,再做有限修改并回读结果。
DLL 随 AGC 分发,首次连接需要 Godot 扫描时重新聚焦编辑器即可;无需手动复制 DLL、配置端口或运行引导脚本。不要读取或返回连接凭据。
明确失败也可能已经修改场景;先检查日志和真实状态再修复。超时、断线或 `needs-reconciliation` 表示结果待核对,不自动重放,不通过重连绕过执行阻断。保存、运行和删除范围以用户任务为准;局部 `UndoRedo` 不等于编辑器撤销历史。
@@ -0,0 +1,257 @@
# Godot 编辑器常用操作
面向 AGC 内置 Godot 工具,仅支持 Windows x64 标准编辑器;不推断 .NET 或其他平台支持。缺少执行工具时报告不可用。目录:执行、查询、节点、撤销、资源、UI、保存、运行、诊断。
## 执行合同
- Direct 的 `agc_godot_execute``{code:...}`Runtime 先发现 `godot.editor.execute`,按实际 schema 传 `{reason:"操作原因",input:{code:...}}`。执行载荷只含 `code`,不增加项目路径等字段。以下是函数体,不增加 `extends``@tool``func run()`,保留内部缩进。
- 上下文是临时 `RefCounted.run()``self` 不是场景 Node,不能直接 `get_tree()`。用 `EditorInterface.get_edited_scene_root()` 取得编辑场景根;`EditorInterface.get_base_control().get_tree().root` 是编辑器根,不是用户场景。
- 各次调用不共享局部变量。返回 `null`、布尔、整数、有限浮点、字符串、数组、字符串键字典。Node、Resource、Vector2、Color 等需投影为路径、数值数组或字典;不要直接返回 Godot 对象。用 `return` 返回结果,`print` 只写有界日志。
-`await EditorInterface.get_base_control().get_tree().process_frame` 或短计时器;不要死循环、长阻塞,也不要派发未等待的后台修改。一次只执行一段有界操作。
- DLL 原件由 AGC 安装资源提供,私有缓存按编辑器实例隔离;首次发现扩展时重新聚焦 Godot 即可。不手改 `.gdextension`、DLL、端口、令牌或 `.godot/agc`
## 读取当前场景、选中节点和树
先核对 `scene`、类型和相对路径。无打开场景时返回空结果。遍历最多 256 节点,`truncated` 为 true 时按目标子树继续查。
<!-- example:query -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
if root == null:
return {"scene": null, "nodes": [], "selected": []}
var selected: Array = []
for node in EditorInterface.get_selection().get_selected_nodes():
if node == root or root.is_ancestor_of(node):
selected.append(str(root.get_path_to(node)))
var nodes: Array = []
var pending: Array[Node] = [root]
while not pending.is_empty() and nodes.size() < 256:
var node: Node = pending.pop_back()
nodes.append({"path": str(root.get_path_to(node)), "type": node.get_class()})
for child in node.get_children():
pending.append(child)
return {"scene": root.scene_file_path, "root": str(root.name), "nodes": nodes,
"selected": selected, "truncated": not pending.is_empty()}
```
`get_node_or_null("Player/Sprite2D")` 相对于场景根。选择用 `EditorInterface.get_selection().clear()` / `add_node(node)`;检查器用 `EditorInterface.edit_node(node)`,均不保存场景。
## 创建、改属性、删除节点
`AGCGuideMarker` 替换为任务指定且不冲突的名称。示例直接修改,不自动加入编辑器撤销历史。`add_child` 后设 `owner = root` 才随当前场景保存;新子树逐个设置 owner,不重写实例场景内部 owner。
<!-- example:create-node -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null and root.get_node_or_null("AGCGuideMarker") == null)
var marker := Node2D.new()
marker.name = "AGCGuideMarker"
root.add_child(marker)
marker.owner = root
marker.position = Vector2(12, 24)
EditorInterface.mark_scene_as_unsaved()
return {"path": str(root.get_path_to(marker)), "position": [marker.position.x, marker.position.y],
"owned": marker.owner == root}
```
<!-- example:update-node -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null)
var marker := root.get_node_or_null("AGCGuideMarker") as Node2D
assert(marker != null)
marker.position = Vector2(24, 48)
EditorInterface.mark_scene_as_unsaved()
return {"path": str(root.get_path_to(marker)), "position": [marker.position.x, marker.position.y]}
```
删除前核对目标及后代;`queue_free()` 连同后代删除,下一帧完成后对象失效。不要删除场景根。
<!-- example:delete-node -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null)
var marker := root.get_node_or_null("AGCGuideMarker")
assert(marker != null and marker != root)
root.remove_child(marker)
marker.queue_free()
EditorInterface.mark_scene_as_unsaved()
await EditorInterface.get_base_control().get_tree().process_frame
return {"removed": root.get_node_or_null("AGCGuideMarker") == null}
```
其它属性如 `Sprite2D.texture``Node3D.position``Label.text`,先确认实际类型。向量和颜色返回数值数组。
## 撤销:局部事务与编辑器历史
局部 `UndoRedo.new()` 不进入 Ctrl+Z 菜单,调用结束即失去历史。下例同一次调用改位置为 `(80, 90)`,随后撤销并回读。
<!-- example:local-undo -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null)
var marker := root.get_node_or_null("AGCGuideMarker") as Node2D
assert(marker != null)
var previous := marker.position
var undo := UndoRedo.new()
undo.create_action("验证位置撤销")
undo.add_do_property(marker, "position", Vector2(80, 90))
undo.add_undo_property(marker, "position", previous)
undo.commit_action()
var changed := marker.position
assert(undo.undo())
return {"changed": [changed.x, changed.y], "restored": [marker.position.x, marker.position.y],
"matches": marker.position == previous}
```
Ctrl+Z 需复用已有 `EditorPlugin.get_undo_redo()``EditorUndoRedoManager``create_action(..., UndoRedo.MERGE_DISABLE, root)` 指定场景历史。局部 UndoRedo 方法操作用 Callablemanager 用对象、方法名、参数。不要为取得 manager 擅自安装 addon。
创建历史需登记 `add_child``owner`、逆向 `remove_child``add_do_reference`;删除记录父节点、顺序、owner,用 `add_undo_reference` 保活,禁止 `free/queue_free` 后再承诺恢复。属性成对登记新旧值。无持久 EditorPlugin 时只能承诺直接修改,不能承诺 Ctrl+Z。
保存重开后旧 Node 引用和局部历史不能复用。需重新查询,确认无后续用户改动,再执行逆操作并重新保存;内存 undo 不会恢复磁盘文件。
## PackedScene 与资源
`res://agc_guide_piece.tscn` 改为任务指定新路径,确认不存在并检查 `pack``ResourceSaver.save` 返回值。`owner` 决定子节点能否打包;不照例覆盖已有资源。
<!-- example:pack-resource -->
```gdscript
var target := "res://agc_guide_piece.tscn"
assert(not FileAccess.file_exists(target))
var source := Node2D.new()
source.name = "GuidePiece"
var child := Marker2D.new()
child.name = "Anchor"
source.add_child(child)
child.owner = source
var packed := PackedScene.new()
var packed_error := packed.pack(source)
source.free()
assert(packed_error == OK)
var save_error := ResourceSaver.save(packed, target)
assert(save_error == OK)
EditorInterface.get_resource_filesystem().scan()
return {"path": target, "saved": FileAccess.file_exists(target)}
```
实例化时检查 PackedScene 类型,只把实例根归属于当前根,保留内部所有权。实例局部覆盖不会改写源 `.tscn`
<!-- example:instance-resource -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null and root.get_node_or_null("AGCGuidePiece") == null)
var packed := ResourceLoader.load("res://agc_guide_piece.tscn", "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
assert(packed != null)
var instance := packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
instance.name = "AGCGuidePiece"
root.add_child(instance)
instance.owner = root
EditorInterface.mark_scene_as_unsaved()
return {"path": str(root.get_path_to(instance)), "source": instance.scene_file_path,
"has_anchor": instance.get_node_or_null("Anchor") != null}
```
ResourceLoader 默认缓存可能返回旧对象;外部刚写文件可用 `CACHE_MODE_IGNORE`。共享 Resource 的修改影响所有引用;局部变化先 `duplicate()` 再赋回。图片/音频须等扫描和导入完成,文件存在不代表已导入。
## 基础 Control / Container UI
Container 管理直属子 Control 布局,使用 `custom_minimum_size`、size flags、theme 常量,避免手写子控件 position/size。新节点逐个设置 owner。
<!-- example:create-ui -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null and root.get_node_or_null("AGCGuideHUD") == null)
var layer := CanvasLayer.new()
layer.name = "AGCGuideHUD"
root.add_child(layer)
layer.owner = root
var center := CenterContainer.new()
center.name = "Center"
layer.add_child(center)
center.owner = root
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var column := VBoxContainer.new()
column.name = "Column"
center.add_child(column)
column.owner = root
column.custom_minimum_size = Vector2(240, 96)
column.add_theme_constant_override("separation", 8)
var label := Label.new()
label.name = "Title"
label.text = "关卡目标"
column.add_child(label)
label.owner = root
var button := Button.new()
button.name = "Start"
button.text = "开始"
button.custom_minimum_size = Vector2(200, 40)
column.add_child(button)
button.owner = root
EditorInterface.mark_scene_as_unsaved()
return {"path": str(root.get_path_to(layer)), "title": label.text, "button": button.text,
"anchors": [center.anchor_left, center.anchor_top, center.anchor_right, center.anchor_bottom],
"owned": [layer.owner == root, center.owner == root, column.owner == root, label.owner == root, button.owner == root]}
```
持久信号应连接游戏脚本的方法,不把临时执行器 Callable 当运行时回调。此例只建布局;尺寸、层级、输入仍需实际试玩验收。
## 保存、重新打开与新场景
`mark_scene_as_unsaved()` 不写盘。仅在获准保存全部当前改动时执行。`save_scene_as(path,false)` 跳过缩略图但返回 void;旧文件可加载不代表本次保存成功。下例依赖前文三个分支,先核验磁盘节点和位置再重开;实际任务须覆盖所有待保存变更,无法证明时只保存、不 reload。
<!-- example:save-reopen -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
assert(root != null and not root.scene_file_path.is_empty())
var scene_path := root.scene_file_path
var expected: Vector2 = root.get_node("AGCGuideMarker").position
EditorInterface.save_scene_as(scene_path, false)
var saved := ResourceLoader.load(scene_path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
assert(saved != null)
var probe := saved.instantiate()
var marker := probe.get_node_or_null("AGCGuideMarker") as Node2D
var matches := marker != null and marker.position == expected and probe.has_node("AGCGuidePiece/Anchor") and probe.has_node("AGCGuideHUD/Center/Column/Title")
probe.free()
if not matches:
return {"reloaded": false, "reason": "磁盘内容未验证,保留当前编辑场景"}
EditorInterface.reload_scene_from_path(scene_path)
await EditorInterface.get_base_control().get_tree().process_frame
var reopened := EditorInterface.get_edited_scene_root()
assert(reopened != null and reopened.scene_file_path == scene_path)
return {"scene": reopened.scene_file_path, "saved": true, "reloaded": true,
"has_piece": reopened.get_node_or_null("AGCGuidePiece/Anchor") != null,
"has_ui": reopened.get_node_or_null("AGCGuideHUD/Center/Column/Title") != null}
```
打开场景用 `open_scene_from_path("res://...")`,等一帧重新取根核对路径;`get_open_scenes()` 查已打开路径,均属 EditorInterface。未命名场景用 `save_scene_as(path)`;常规 GUI 用 `save_scene()` 检查 `OK`,headless 缩略图可能报错。不要覆盖未知未保存工作。
## 运行与停止
EditorInterface 的 `play_current_scene()` 运行当前场景,`play_main_scene()` 运行主场景,`play_custom_scene("res://...")` 运行指定场景。仅需试玩时调用,先核对路径、主场景与未保存改动。`is_playing_scene()` / `get_playing_scene()` 只报告启动状态,不证明玩法正确;编辑根不是游戏 Remote SceneTree。
<!-- example:stop-play -->
```gdscript
var was_playing := EditorInterface.is_playing_scene()
if was_playing:
EditorInterface.stop_playing_scene()
await EditorInterface.get_base_control().get_tree().process_frame
return {"was_playing": was_playing, "playing": EditorInterface.is_playing_scene()}
```
## 错误诊断与回执
- 读取执行回执的 `ok/status/result/error/logs`。编译错误先检查函数体包装、类型推断和真实 API;确定运行失败也可能已经执行前半段修改,先读回节点/资源,再修复剩余步骤。
- `godot_result_not_serializable` 可能只是返回了对象,不能据此认定修改未发生;改用只读查询返回路径和标量。`assert` 失败不会替你回滚此前副作用。
- 超时、断线、`needs-reconciliation` 或发送后的身份不明不能自动重放;先核对编辑器真实状态,按 AGC 现有恢复流程处理阻断。重新连接、启停插件或重启 Runner 都不是“确认没有执行”。
- 捕获日志只覆盖这次编辑器执行且有长度上限;成功启动游戏不等于运行时无错误。结合 Godot Output/Debugger、游戏日志与实际试玩核验,不将空日志当作无故障。
<!-- example:diagnostics -->
```gdscript
var root := EditorInterface.get_edited_scene_root()
return {"version": Engine.get_version_info().string,
"editor": Engine.is_editor_hint(), "scene": root.scene_file_path if root != null else null,
"open_scenes": Array(EditorInterface.get_open_scenes()), "playing": EditorInterface.is_playing_scene(),
"playing_scene": EditorInterface.get_playing_scene()}
```
示例已在 Godot 4.7.2 标准版 headless 验证;停止仅验证已停止状态。GUI 缩略图保存、Ctrl+Z 历史、运行中停止及 UI 视觉效果未在此指南测试中验收。
@@ -0,0 +1,10 @@
---
name: agc-unity-editor
description: 通过 AGC 的 Unity 编辑器执行工具读取和修改当前项目的场景、对象、组件、Prefab、Canvas 与资源,并保存、撤销和检查播放状态。
---
# Unity 编辑器操作
使用当前会话提供的 Unity 执行工具,提交 C# 方法正文。开始操作前读取[常用操作指南](references/【操作指南】Unity编辑器常用操作-2026-09-20.md),按任务选择其中的示例。指南包含调用格式、目标定位、返回值投影和可执行代码。
先查询目标与编辑状态,修改后回读;写操作显式登记 Undo,保存操作检查返回值。执行失败可能留下部分修改,结果未知时不得重放。插件不会自动把任意代码变成可撤销事务。
@@ -0,0 +1,225 @@
# Unity 编辑器常用操作
## 调用与结果
当前接入支持 Windows x64 的 Mono 编辑器。工具缺失时报告不可用,不推断 .NET/CoreCLR 或其他平台已支持。
连接当前项目的 Unity 后提交仅含 `code` 的执行载荷。DirectProject 工具 `agc_unity_execute``{"code":"return 42;"}`Runtime 的 `unity.editor.execute` 按实际 schema 传 `{"reason":"读取编辑器状态","input":{"code":"return 42;"}}`。以会话工具清单为准。
`code` 是主线程执行的方法正文,直接 `return`,不加 `using`、类或 `Main`。使用完整 API 名称。Unity 对象先投影为普通数据;返回集合最多保留 32 项,嵌套深度达到 4 会转字符串,采用浅层投影、每批 30 项及显式截断标记。跨调用保留路径/GUID,实例 ID 仅当前 Editor 生命周期内有效。
先确认场景、选择、编辑模式和待修改资源。遍历 `GetRootGameObjects()``GetComponentsInChildren(..., true)` 可包含未激活对象;`GameObject.Find` 会漏掉它们。结合场景路径、层级路径和实例 ID 回读目标,重名时不要任取首个。
`completed` 只证明代码返回,仍要回读。`failed` 可能已部分修改,检查 `dispatched` 与现场后修复;编译失败且 `dispatched=false` 表示未执行。`needs-reconciliation`、超时或断线后结果未知时不重放,保留执行 ID 并核对现场,重连不等于允许重试。工具不自动撤销/回滚,不能中断死循环;保持调用短小,不在主线程等待编译/播放切换。
## 当前场景、选择和层级
返回当前场景及最多 30 个节点。其他场景用 `SceneManager.sceneCount/GetSceneAt` 枚举。
<!-- example:inspect -->
```csharp
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
var rows = new System.Collections.Generic.List<object>();
var queue = new System.Collections.Generic.Queue<UnityEngine.Transform>();
foreach (var root in scene.GetRootGameObjects()) queue.Enqueue(root.transform);
while (queue.Count > 0 && rows.Count < 30) {
var t = queue.Dequeue();
var path = t.name;
for (var p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
rows.Add(new { id = t.gameObject.GetInstanceID(), path, active = t.gameObject.activeSelf,
x = t.localPosition.x, y = t.localPosition.y, z = t.localPosition.z });
for (int i = 0; i < t.childCount; i++) queue.Enqueue(t.GetChild(i));
}
var selected = UnityEditor.Selection.activeGameObject;
return new { scene = scene.path, dirty = scene.isDirty, nodes = rows.ToArray(), truncated = queue.Count > 0,
selectedId = selected == null ? 0 : selected.GetInstanceID(),
playing = UnityEditor.EditorApplication.isPlaying, compiling = UnityEditor.EditorApplication.isCompiling };
```
## 创建、修改、删除与 Undo
示例对象 `AGC_Guide_Object` 应替换成任务目标。编辑先退出播放模式。属性写入前 `Undo.RecordObject`;创建用 `RegisterCreatedObjectUndo`,加组件用 `Undo.AddComponent`,删除用 `Undo.DestroyObjectImmediate`,改父级用 `Undo.SetTransformParent`。磁盘写入、外部副作用及未登记修改不会自动撤销。
创建对象和组件并选中它;检查重复名是防误建措施,不是结果未知后重试的许可。
<!-- example:create -->
```csharp
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
foreach (var root in scene.GetRootGameObjects())
if (root.name == "AGC_Guide_Object") throw new System.Exception("目标已存在,请先核对");
UnityEditor.Undo.IncrementCurrentGroup();
int group = UnityEditor.Undo.GetCurrentGroup();
UnityEditor.Undo.SetCurrentGroupName("AGC 创建对象");
var go = new UnityEngine.GameObject("AGC_Guide_Object");
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "AGC 创建对象");
UnityEditor.Undo.AddComponent<UnityEngine.BoxCollider>(go);
UnityEditor.Selection.activeGameObject = go;
UnityEditor.Undo.CollapseUndoOperations(group);
return new { id = go.GetInstanceID(), name = go.name, collider = go.GetComponent<UnityEngine.BoxCollider>() != null };
```
确认选择是目标后修改。Prefab 实例属性写入后记录 override。改 Prefab 资产用 `LoadPrefabContents/SaveAsPrefabAsset/UnloadPrefabContents` 并在 `finally` 释放,不能当场景对象保存。
<!-- example:modify -->
```csharp
var go = UnityEditor.Selection.activeGameObject;
if (go == null || !go.scene.IsValid() || UnityEditor.EditorUtility.IsPersistent(go)) throw new System.Exception("请选中场景对象");
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
UnityEditor.Undo.IncrementCurrentGroup();
int group = UnityEditor.Undo.GetCurrentGroup();
UnityEditor.Undo.SetCurrentGroupName("AGC 修改对象");
UnityEditor.Undo.RecordObject(go.transform, "AGC 移动对象");
go.transform.localPosition = new UnityEngine.Vector3(1, 2, 3);
var collider = go.GetComponent<UnityEngine.BoxCollider>();
if (collider == null) collider = UnityEditor.Undo.AddComponent<UnityEngine.BoxCollider>(go);
UnityEditor.Undo.RecordObject(collider, "AGC 修改碰撞体");
collider.size = new UnityEngine.Vector3(2, 3, 4);
if (UnityEditor.PrefabUtility.IsPartOfPrefabInstance(go)) {
UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(go.transform);
UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(collider);
}
UnityEditor.Undo.FlushUndoRecordObjects();
UnityEditor.Undo.CollapseUndoOperations(group);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene);
return new { id = go.GetInstanceID(), x = go.transform.localPosition.x, colliderX = collider.size.x };
```
删除选择对象上的碰撞体;删除整个已核对对象时把 `collider` 替换为 `go`,并提前回读待删除子树。
<!-- example:remove_component -->
```csharp
var go = UnityEditor.Selection.activeGameObject;
if (go == null || !go.scene.IsValid() || UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("需要编辑模式中的场景对象");
var collider = go.GetComponent<UnityEngine.BoxCollider>();
if (collider == null) throw new System.Exception("没有 BoxCollider");
UnityEditor.Undo.IncrementCurrentGroup();
UnityEditor.Undo.SetCurrentGroupName("AGC 删除碰撞体");
UnityEditor.Undo.DestroyObjectImmediate(collider);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene);
return new { removed = go.GetComponent<UnityEngine.BoxCollider>() == null };
```
只在确认最后一条 Undo 就是本次操作时执行撤销,避免撤销用户插入的编辑。撤销后重新运行查询检查对象/属性。
<!-- example:undo -->
```csharp
UnityEditor.Undo.PerformUndo();
var go = UnityEditor.Selection.activeGameObject;
return new { selectedId = go == null ? 0 : go.GetInstanceID(), collider = go != null && go.GetComponent<UnityEngine.BoxCollider>() != null };
```
## 资源查找与 Prefab 实例化
按类型和目录查询,拿到 GUID/路径后加载。下例返回前 30 个 Prefab;过滤器可换成 `t:Material``t:Texture2D` 等。
<!-- example:assets -->
```csharp
var ids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
var rows = new System.Collections.Generic.List<object>();
for (int i = 0; i < ids.Length && i < 30; i++)
rows.Add(new { guid = ids[i], path = UnityEditor.AssetDatabase.GUIDToAssetPath(ids[i]) });
return new { assets = rows.ToArray(), total = ids.Length, truncated = ids.Length > 30 };
```
路径替换为已查到的 Prefab`InstantiatePrefab` 保持 Prefab 联系,后续修改登记 override。
<!-- example:prefab -->
```csharp
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
var path = "Assets/AGCGuide/Guide.prefab";
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (asset == null || UnityEditor.PrefabUtility.GetPrefabAssetType(asset) == UnityEditor.PrefabAssetType.NotAPrefab) throw new System.Exception("未找到 Prefab");
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
UnityEditor.Undo.IncrementCurrentGroup();
var instance = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(asset, scene);
UnityEditor.Undo.RegisterCreatedObjectUndo(instance, "AGC 实例化 Prefab");
UnityEditor.Selection.activeGameObject = instance;
return new { id = instance.GetInstanceID(), source = UnityEditor.PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(instance) };
```
## 基础 Canvas 与布局
先查询并复用现有 UI。下例创建 Canvas 与居中布局容器,不依赖 uGUI/TMP,容器无可见图形。添加 `Image``Button`、文本或 `EventSystem` 前确认项目 UI 体系和包,避免重复事件系统。
<!-- example:canvas -->
```csharp
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
foreach (var root in scene.GetRootGameObjects())
if (root.name == "AGC_Guide_Canvas") throw new System.Exception("示例 Canvas 已存在");
UnityEditor.Undo.IncrementCurrentGroup();
int group = UnityEditor.Undo.GetCurrentGroup();
var canvasObject = new UnityEngine.GameObject("AGC_Guide_Canvas", typeof(UnityEngine.RectTransform), typeof(UnityEngine.Canvas));
UnityEditor.Undo.RegisterCreatedObjectUndo(canvasObject, "AGC 创建 Canvas");
canvasObject.GetComponent<UnityEngine.Canvas>().renderMode = UnityEngine.RenderMode.ScreenSpaceOverlay;
var panel = new UnityEngine.GameObject("Content", typeof(UnityEngine.RectTransform));
UnityEditor.Undo.RegisterCreatedObjectUndo(panel, "AGC 创建布局");
UnityEditor.Undo.SetTransformParent(panel.transform, canvasObject.transform, "AGC 设置 UI 父级");
var rect = (UnityEngine.RectTransform)panel.transform;
rect.anchorMin = rect.anchorMax = rect.pivot = new UnityEngine.Vector2(0.5f, 0.5f);
rect.anchoredPosition = UnityEngine.Vector2.zero;
rect.sizeDelta = new UnityEngine.Vector2(320, 180);
UnityEditor.Undo.CollapseUndoOperations(group);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene);
return new { canvasId = canvasObject.GetInstanceID(), panelId = panel.GetInstanceID(), width = rect.sizeDelta.x, height = rect.sizeDelta.y };
```
## 保存与打开场景
确认目标路径及对象所属场景,多场景时用 `go.scene` 而非默认 active scene;已有场景通常沿用 `scene.path``MarkSceneDirty` 不是保存;独立资源用 `SetDirty``AssetDatabase.SaveAssetIfDirty` 保存。磁盘保存不由 Undo 回滚。
<!-- example:save -->
```csharp
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
var path = "Assets/AGCGuide/Guide.unity";
if (!UnityEditor.AssetDatabase.IsValidFolder("Assets/AGCGuide")) UnityEditor.AssetDatabase.CreateFolder("Assets", "AGCGuide");
if (!UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, path)) throw new System.Exception("场景保存失败");
return new { path = scene.path, dirty = scene.isDirty };
```
Single 会关闭当前场景;存在未保存修改时先停下处理,不默默丢弃。要保留场景则用 `OpenSceneMode.Additive`,并明确后续目标场景。
<!-- example:open -->
```csharp
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) throw new System.Exception("请先停止播放");
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
if (UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).isDirty) throw new System.Exception("存在未保存场景,请先处理");
var path = "Assets/AGCGuide/Guide.unity";
if (UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) == null) throw new System.Exception("场景文件不存在");
var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, UnityEditor.SceneManagement.OpenSceneMode.Single);
return new { path = scene.path, loaded = scene.isLoaded, roots = scene.rootCount };
```
## 播放、停止与编译诊断
播放/停止在下一次 Editor update 调度,`requested` 不代表已切换,稍后查询。播放和修改脚本可能触发编译/Domain Reload 使连接失效,稳定后重连核对,不重发操作。退出播放通常不保留运行期改动。
<!-- example:play -->
```csharp
if (UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.Exception("编辑器正在编译或导入");
UnityEditor.EditorApplication.delayCall += () => { UnityEditor.EditorApplication.isPlaying = true; };
return new { requested = "play" };
```
<!-- example:stop -->
```csharp
UnityEditor.EditorApplication.delayCall += () => { UnityEditor.EditorApplication.isPlaying = false; };
return new { requested = "stop" };
```
状态查询不能证明编译成功。代码编译错误由工具回执返回;项目编译详情查看 Console/Editor 日志,回执不含全量 Console。不要依赖未公开的 `LogEntries` API。
<!-- example:diagnostics -->
```csharp
return new { compiling = UnityEditor.EditorApplication.isCompiling,
importing = UnityEditor.EditorApplication.isUpdating,
playing = UnityEditor.EditorApplication.isPlaying,
changingPlayMode = UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode,
version = UnityEngine.Application.unityVersion };
```
## 验证范围
以上 13 个代码块已从本文提取,在 Windows x64 Unity 6000.3.7f1 Mono 的独立无包依赖项目中经 AGC Attach 实测,包含修改回读、Undo、Prefab override 保存重开及播放/停止。采用 batchmode/nographics;未验收 UI 视觉、第三方包或其他 Unity 版本。
@@ -1,7 +1,39 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.25",
"version": "2026-08-26.27",
"skills": [
{
"name": "agc-unity-editor",
"purpose": "通过 AGC 内置 Unity 插件查询和修改场景、对象、资源与 UI,正确处理撤销、保存和回执",
"triggers": [
"操作已打开的 Unity 工程",
"编写 Unity 编辑器执行代码"
],
"requiredTools": [
"agc_tools.agc_unity_execute"
],
"files": [
"SKILL.md",
"references/【操作指南】Unity编辑器常用操作-2026-09-20.md"
],
"sha256": "9599fa1884db9c4f3eeab20d18871d4dafc845f5ecfe0f9ac9ba7417e65062fc"
},
{
"name": "agc-godot-editor",
"purpose": "通过 AGC 内置 Godot 插件查询和修改场景、节点、资源与 UI,正确处理 owner、撤销和回执",
"triggers": [
"操作已打开的 Godot 工程",
"编写 Godot 编辑器执行代码"
],
"requiredTools": [
"agc_tools.agc_godot_execute"
],
"files": [
"SKILL.md",
"references/【操作指南】Godot编辑器常用操作-2026-09-20.md"
],
"sha256": "b5d76c8685c49e0cd1b0a243a2f46f00daa1a7c8a37c5137b6f31917df9a0af0"
},
{
"name": "agc-game-production-workflow",
"purpose": "把完整游戏从策划案按阶段推进到真实素材接入、构建、试玩和交付",
@@ -6092,7 +6092,7 @@ case "$extra_roots" in *'"method":"skills/extraRoots/set"'*) ;; *) exit 87 ;; es
printf '%s\n' '{"id":2,"result":{}}'
IFS= read -r skills_list
case "$skills_list" in *'"method":"skills/list"'*) ;; *) exit 88 ;; esac
printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}'
printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-godot-editor"},{"name":"agc-unity-editor"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}'
while IFS= read -r line; do :; done
"#,
)
@@ -6833,7 +6833,7 @@ while IFS= read -r line; do
case "$line" in
*'"method":"initialize"'*) printf '{"id":%s,"result":{"codexHome":"/tmp","platformFamily":"unix","platformOs":"linux","userAgent":"fixture"}}\n' "$id" ;;
*'"method":"skills/extraRoots/set"'*) printf '{"id":%s,"result":{}}\n' "$id" ;;
*'"method":"skills/list"'*) printf '{"id":%s,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}\n' "$id" ;;
*'"method":"skills/list"'*) printf '{"id":%s,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-godot-editor"},{"name":"agc-unity-editor"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}\n' "$id" ;;
*'"method":"thread/start"'*) printf '{"id":%s,"result":{"thread":{"id":"thread-echo"}}}\n' "$id" ;;
*'"method":"thread/inject_items"'*) printf '{"id":%s,"result":{}}\n' "$id" ;;
*'"method":"turn/start"'*)
File diff suppressed because one or more lines are too long
@@ -2610,16 +2610,56 @@ async fn bridge_web_search_at(root: &Path, arguments: &Value, search_url: &str)
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
async fn bridge_unity_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
bridge_editor_execute(
state,
arguments,
"unity.editor.execute",
"Unity",
"C# 代码",
crate::builtin_plugins::unity_editor_agent_tool_available_for_project,
crate::editor_adapters::execute_unity_editor_code,
)
.await
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
async fn bridge_godot_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
bridge_editor_execute(
state,
arguments,
"godot.editor.execute",
"Godot",
"GDScript 函数体",
crate::builtin_plugins::godot_editor_agent_tool_available_for_project,
crate::editor_adapters::execute_godot_editor_code,
)
.await
}
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
async fn bridge_editor_execute(
state: &DirectToolBridgeState,
arguments: &Value,
tool: &'static str,
editor: &'static str,
language: &str,
available: fn(&Path) -> bool,
execute: fn(&Path, &str) -> Result<Value, String>,
) -> Value {
let prepared = (|| {
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(&state.root) {
return Err("当前项目不是 Unity 项目或 Unity 插件不可用".to_string());
if !available(&state.root) {
return Err(format!("当前项目不是 {editor} 项目或 {editor} 插件不可用"));
}
enforce_project_permission_policy(&state.root, "unity.editor.execute")?;
enforce_project_permission_policy(&state.root, tool)?;
bridge_reject_unknown_fields(arguments, &["code"])?;
let code = arguments
.get("code")
.and_then(Value::as_str)
.ok_or_else(|| "code 必须是 C# 代码".to_string())?;
.ok_or_else(|| format!("code 必须是 {language}"))?;
if code.trim().is_empty() || code.len() > 131072 || code.contains('\0') {
return Err("code 不能为空、包含 NUL 或超过 128 KiB".to_string());
}
@@ -2637,10 +2677,10 @@ async fn bridge_unity_execute(state: &DirectToolBridgeState, arguments: &Value)
};
let root = state.root.clone();
let result = tokio::task::spawn_blocking(move || {
if !crate::builtin_plugins::unity_editor_agent_tool_available_for_project(&root) {
return Err("当前 Unity 插件不可用".to_string());
if !available(&root) {
return Err(format!("当前 {editor} 插件不可用"));
}
crate::editor_adapters::execute_unity_editor_code(&root, &code)
execute(&root, &code)
})
.await;
match result {
@@ -2649,7 +2689,7 @@ async fn bridge_unity_execute(state: &DirectToolBridgeState, arguments: &Value)
bridge_tool_result(redact_agent_runtime_error(&state.root, &response.to_string(), 32_000), Vec::new(), failed)
}
Ok(Err(error)) => bridge_tool_result(redact_agent_runtime_error(&state.root, &error, 480), Vec::new(), true),
Err(_) => bridge_tool_result(json!({"ok":false,"status":"needs-reconciliation","retryAllowed":false,"error":"Unity 执行任务异常,请人工核对结果"}).to_string(), Vec::new(), true),
Err(_) => bridge_tool_result(json!({"ok":false,"status":"needs-reconciliation","dispatched":true,"retryAllowed":false,"error":format!("{editor} 执行任务异常,请人工核对结果")}).to_string(), Vec::new(), true),
}
}
@@ -2855,6 +2895,8 @@ async fn handle_direct_tool_bridge(
"agc_cocos_execute" => bridge_cocos_execute(&state, &request.arguments).await,
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
"agc_unity_execute" => bridge_unity_execute(&state, &request.arguments).await,
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
"agc_godot_execute" => bridge_godot_execute(&state, &request.arguments).await,
#[cfg(all(windows, feature = "cocos-editor-execute"))]
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
bridge_cocos_call(&state, &request.arguments, Some(operation)).await
@@ -76,12 +76,18 @@ pub(crate) fn run_direct_tools_mcp_if_requested(args: &[String]) -> Option<i32>
async fn direct_tools_mcp_specs() -> Value {
let mut cocos_editor_available = false;
let mut unity_editor_available = false;
let mut godot_editor_available = false;
if cfg!(all(windows, feature = "cocos-editor-execute"))
|| cfg!(all(
windows,
target_arch = "x86_64",
feature = "unity-editor-execute"
))
|| cfg!(all(
windows,
target_arch = "x86_64",
feature = "godot-editor-execute"
))
{
// 每次 tools/list 询问绑定的宿主;失败时不广告可选插件工具。
if let Ok(result) = tokio::time::timeout(
@@ -111,6 +117,14 @@ async fn direct_tools_mcp_specs() -> Value {
.iter()
.any(|tool| tool == crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME)
});
godot_editor_available = availability
.as_ref()
.and_then(|v| v["tools"].as_array())
.is_some_and(|tools| {
tools
.iter()
.any(|tool| tool == crate::builtin_plugins::AGC_GODOT_EDITOR_TOOL_NAME)
});
}
}
}
@@ -118,6 +132,7 @@ async fn direct_tools_mcp_specs() -> Value {
controlled_web_search_enabled(),
cocos_editor_available,
unity_editor_available,
godot_editor_available,
)
}
@@ -149,13 +164,14 @@ fn resource_tool_prompt_schema_max_chars() -> usize {
#[cfg(test)]
fn direct_tools_mcp_specs_for(controlled_web_search: bool, cocos_editor_available: bool) -> Value {
direct_tools_mcp_specs_for_plugins(controlled_web_search, cocos_editor_available, false)
direct_tools_mcp_specs_for_plugins(controlled_web_search, cocos_editor_available, false, false)
}
fn direct_tools_mcp_specs_for_plugins(
controlled_web_search: bool,
_cocos_editor_available: bool,
_unity_editor_available: bool,
_godot_editor_available: bool,
) -> Value {
let tools = vec![
json!({
@@ -604,6 +620,14 @@ fn direct_tools_mcp_specs_for_plugins(
"inputSchema": {"type":"object", "properties":{"code":{"type":"string", "minLength":1, "maxLength":131072}}, "required":["code"], "additionalProperties":false}
}));
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
if _godot_editor_available {
tools.push(json!({
"name": "agc_godot_execute",
"description": "在当前项目已打开的 Windows x64 Godot 4.7+ 标准编辑器执行支持 return/await 的 GDScript 函数体。宿主管理安装目录 DLL 和受管描述文件,聚焦自动加载,无需手跑脚本。仅提交 code;结果不确定时禁止自动重发。",
"inputSchema": {"type":"object", "properties":{"code":{"type":"string", "minLength":1, "maxLength":131072}}, "required":["code"], "additionalProperties":false}
}));
}
if controlled_web_search {
tools.push(json!({
"name": "agc_web_search",
@@ -693,11 +717,25 @@ async fn call_agc_cocos_execute(arguments: &Value) -> Value {
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
async fn call_agc_unity_execute(arguments: &Value) -> Value {
call_agc_editor_execute("agc_unity_execute", "C# 代码", arguments).await
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
async fn call_agc_godot_execute(arguments: &Value) -> Value {
call_agc_editor_execute("agc_godot_execute", "GDScript 函数体", arguments).await
}
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
async fn call_agc_editor_execute(tool: &str, language: &str, arguments: &Value) -> Value {
let validated = validate_tool_object_fields(arguments, &["code"]).and_then(|()| {
let code = arguments
.get("code")
.and_then(Value::as_str)
.ok_or_else(|| "code 必须是 C# 代码".to_string())?;
.ok_or_else(|| format!("code 必须是 {language}"))?;
if code.trim().is_empty() || code.len() > 131072 || code.contains('\0') {
return Err("code 不能为空、包含 NUL 或超过 128 KiB".to_string());
}
@@ -706,7 +744,7 @@ async fn call_agc_unity_execute(arguments: &Value) -> Value {
if let Err(error) = validated {
return mcp_tool_result(error, Vec::new(), true);
}
call_client_tool_bridge("agc_unity_execute", arguments).await
call_client_tool_bridge(tool, arguments).await
}
fn mcp_success(id: Value, result: Value) -> Value {
@@ -1813,6 +1851,8 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
"agc_unity_execute" => call_agc_unity_execute(&arguments).await,
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
"agc_godot_execute" => call_agc_godot_execute(&arguments).await,
#[cfg(all(windows, feature = "cocos-editor-execute"))]
operation if cocos_editor_bridge::is_cocos_operation(operation) => {
call_client_tool_bridge(operation, &arguments).await
@@ -2012,6 +2052,115 @@ pub(crate) fn stop_game_creator_external_mcp() -> Result<(), String> {
mod tests {
use super::*;
#[test]
fn godot_tool_schema_is_code_only_and_follows_host_availability() {
for available in [false, true] {
let specs = direct_tools_mcp_specs_for_plugins(false, false, false, available);
let tool = specs["tools"]
.as_array()
.unwrap()
.iter()
.find(|tool| tool["name"] == "agc_godot_execute");
assert_eq!(
tool.is_some(),
available
&& cfg!(all(
windows,
target_arch = "x86_64",
feature = "godot-editor-execute"
))
);
if let Some(tool) = tool {
assert_eq!(tool["inputSchema"]["additionalProperties"], false);
assert_eq!(tool["inputSchema"]["required"], json!(["code"]));
assert_eq!(
tool["inputSchema"]["properties"].as_object().unwrap().len(),
1
);
}
}
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
#[tokio::test]
async fn godot_mcp_rejects_target_override_and_invalid_code_before_bridge() {
for arguments in [
json!({"code":"return 42", "projectPath":"C:/other"}),
json!({"code":"return 42", "processId":123}),
json!({"code":"return 42", "dllPath":"C:/other.dll"}),
json!({"code":""}),
json!({"code":"a\u{0}b"}),
json!({"code":"".repeat(44_000)}),
] {
let response = call_agc_godot_execute(&arguments).await;
assert_eq!(response["isError"], true);
let text = response["content"][0]["text"].as_str().unwrap();
assert!(!text.contains("bridge"), "输入校验不应访问 bridge{text}");
}
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
#[tokio::test]
async fn godot_tools_follow_bound_host_project_and_plugin_switch() {
let _guard = crate::builtin_plugins::test_lock();
let config = tempfile::tempdir().unwrap();
crate::builtin_plugins::initialize(config.path()).unwrap();
let project = crate::tests::canonical_test_tempdir("godot-mcp-project-");
std::fs::create_dir(project.path().join("game")).unwrap();
std::fs::write(
project.path().join("game/project.godot"),
"config_version=5\n",
)
.unwrap();
std::fs::create_dir(project.path().join(".agent")).unwrap();
std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap();
let bridge =
super::super::direct_tool_bridge::start_direct_tool_bridge(project.path(), false)
.await
.unwrap();
for enabled in [false, true, false, true] {
crate::builtin_plugins::set_enabled(
crate::builtin_plugins::AGC_GODOT_EDITOR_PLUGIN_ID,
enabled,
)
.unwrap();
let specs = EXTERNAL_MCP_BRIDGE_URL
.scope(bridge.url().to_string(), direct_tools_mcp_specs())
.await;
assert_eq!(
specs["tools"]
.as_array()
.unwrap()
.iter()
.any(|tool| tool["name"] == "agc_godot_execute"),
enabled
);
if !enabled {
let response = EXTERNAL_MCP_BRIDGE_URL
.scope(
bridge.url().to_string(),
call_agc_godot_execute(&json!({"code":"return 42"})),
)
.await;
assert_eq!(response["isError"], true);
assert!(response.to_string().contains("不可用"));
}
}
std::fs::remove_file(project.path().join("game/project.godot")).unwrap();
let specs = EXTERNAL_MCP_BRIDGE_URL
.scope(bridge.url().to_string(), direct_tools_mcp_specs())
.await;
assert!(!specs.to_string().contains("agc_godot_execute"));
drop(bridge);
let specs = EXTERNAL_MCP_BRIDGE_URL
.scope(
"http://127.0.0.1:1/unavailable".to_string(),
direct_tools_mcp_specs(),
)
.await;
assert!(!specs.to_string().contains("agc_godot_execute"));
}
#[test]
fn remove_background_arguments_enforce_mode_color_contract() {
for fields in [
@@ -2860,6 +3009,22 @@ mod tests {
assert_eq!(denied_windows_absolute["isError"], true);
}
#[test]
fn editor_guides_are_available_through_the_existing_skill_resource_tool() {
for (skill, engine) in [("agc-unity-editor", "Unity"), ("agc-godot-editor", "Godot")] {
let relative = format!("references/【操作指南】{engine}编辑器常用操作-2026-09-20.md");
let expected = read_agc_skill_resource(&format!("{skill}/{relative}")).unwrap();
let response =
call_agc_read_skill_resource(&json!({"skillName":skill,"relativePath":relative}));
assert_eq!(response["isError"], false);
assert_eq!(response["content"][0]["text"], expected);
let denied = call_agc_read_skill_resource(
&json!({"skillName":skill,"relativePath":"references/not-in-manifest.md"}),
);
assert_eq!(denied["isError"], true);
}
}
#[test]
fn external_codex_response_redacts_sensitive_lines_and_keeps_safe_text() {
let response = redact_external_mcp_response(
@@ -1620,7 +1620,7 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
.unwrap_or(160)
),
"command.run_limited" => format!("commandId={}", text(&["commandId", "command_id", "id"])),
"cocos.editor.execute" | "unity.editor.execute" => format!(
"cocos.editor.execute" | "unity.editor.execute" | "godot.editor.execute" => format!(
"codeChars={} · codeSha256={:x}",
chars(&["code"]),
Sha256::digest(
@@ -393,6 +393,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
true,
|| observe_agent_runtime_unity_editor_execute(root, action, pending_action),
),
"godot.editor.execute" => observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
run_id,
action,
&action_fingerprint,
pending_action,
true,
|| observe_agent_runtime_godot_editor_execute(root, action, pending_action),
),
"preview.validate" => {
observe_agent_runtime_preview_validate(
root,
@@ -98,6 +98,7 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
#[cfg(feature = "cocos-editor-execute")]
"cocos.editor.execute" => Some("cocos.editor.execute"),
"unity.editor.execute" => Some("unity.editor.execute"),
"godot.editor.execute" => Some("godot.editor.execute"),
"preview.start" => Some("preview.start"),
"preview.validate" => Some("preview.validate"),
"image.inspect" => Some("image.inspect"),
@@ -78,6 +78,9 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
if crate::builtin_plugins::unity_editor_agent_tool_available() {
tools.push(crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME);
}
if crate::builtin_plugins::godot_editor_agent_tool_available() {
tools.push(crate::builtin_plugins::AGC_GODOT_EDITOR_TOOL_NAME);
}
tools
}
@@ -167,6 +170,11 @@ pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
{
continue;
}
if tool == crate::builtin_plugins::AGC_GODOT_EDITOR_TOOL_NAME
&& !crate::builtin_plugins::godot_editor_agent_tool_available_for_project(root)
{
continue;
}
if isolated && ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) {
denied_tools.push(tool.to_string());
continue;
@@ -213,6 +221,10 @@ pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
*tool != crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME
|| crate::builtin_plugins::unity_editor_agent_tool_available_for_project(root)
})
.filter(|tool| {
*tool != crate::builtin_plugins::AGC_GODOT_EDITOR_TOOL_NAME
|| crate::builtin_plugins::godot_editor_agent_tool_available_for_project(root)
})
.map(str::to_string)
.collect(),
auto_tools,
@@ -6,12 +6,12 @@ mod command_ops;
mod context;
mod delegation;
mod delivery;
mod editor_execute;
mod file_ops;
mod goal_contract;
mod helpers;
mod isolated_joins;
mod media;
mod unity_editor;
pub(in crate::agent) use media::design_foundation_ui_page_output_path_is_valid;
mod policy;
mod preview;
@@ -27,6 +27,7 @@ pub(in crate::agent) use command_ops::*;
pub(in crate::agent) use context::*;
pub(in crate::agent) use delegation::*;
pub(in crate::agent) use delivery::*;
pub(in crate::agent) use editor_execute::*;
pub(in crate::agent) use file_ops::*;
pub(in crate::agent) use goal_contract::*;
pub(in crate::agent) use helpers::*;
@@ -39,7 +40,6 @@ pub(in crate::agent) use project_ops::*;
pub(in crate::agent) use run_status::*;
pub(in crate::agent) use task_ops::*;
pub(in crate::agent) use ui_workflow::*;
pub(in crate::agent) use unity_editor::*;
#[cfg(test)]
pub(crate) use delegation::observe_agent_runtime_agent_delegate_at_locked;
@@ -0,0 +1,135 @@
use super::*;
use serde_json::Value;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct EditorExecuteInput {
code: String,
}
pub(in crate::agent) fn observe_agent_runtime_unity_editor_execute(
root: &Path,
action: &AgentRuntimeToolAction,
pending_action: Option<&AgentRuntimePendingToolAction>,
) -> AgentRuntimeToolObservation {
observe_agent_runtime_editor_execute(
root,
action,
pending_action,
"unity.editor.execute",
"Unity",
crate::builtin_plugins::unity_editor_agent_tool_available_for_project,
crate::editor_adapters::execute_unity_editor_code,
)
}
pub(in crate::agent) fn observe_agent_runtime_godot_editor_execute(
root: &Path,
action: &AgentRuntimeToolAction,
pending_action: Option<&AgentRuntimePendingToolAction>,
) -> AgentRuntimeToolObservation {
observe_agent_runtime_editor_execute(
root,
action,
pending_action,
"godot.editor.execute",
"Godot",
crate::builtin_plugins::godot_editor_agent_tool_available_for_project,
crate::editor_adapters::execute_godot_editor_code,
)
}
fn observe_agent_runtime_editor_execute(
root: &Path,
action: &AgentRuntimeToolAction,
pending_action: Option<&AgentRuntimePendingToolAction>,
tool: &str,
editor: &str,
available: fn(&Path) -> bool,
execute: fn(&Path, &str) -> Result<Value, String>,
) -> AgentRuntimeToolObservation {
let execution = (|| {
let input: EditorExecuteInput = serde_json::from_value(action.input.clone())
.map_err(|error| format!("{tool} 输入无效:{error}"))?;
if pending_action.is_none() {
return Err(format!("{tool} 必须绑定 durable pending action"));
}
if !available(root) {
return Err(format!("当前项目不是 {editor} 项目或 {editor} 插件不可用"));
}
execute(root, &input.code)
})();
match execution {
Ok(response) => {
let status = match response["status"].as_str() {
Some("completed") if response["ok"] == true => "ok",
Some("needs-reconciliation") => {
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
}
_ => "failed",
};
AgentRuntimeToolObservation {
tool: tool.to_string(),
status: status.to_string(),
summary: match status {
"ok" => format!("{editor} 编辑器已返回执行成功回执"),
"needs-reconciliation" => format!("{editor} 执行结果待人工核对,禁止自动重发"),
_ => format!("{editor} 编辑器执行失败"),
},
detail: Some(redact_agent_runtime_project_paths(
root,
&response.to_string(),
32_000,
)),
}
}
Err(error) => AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 480),
detail: None,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unity_execute_requires_pending_action_and_rejects_project_override() {
for input in [
serde_json::json!({"code":"return 2;"}),
serde_json::json!({"code":"return 2;", "projectPath":"C:/other"}),
] {
let action = AgentRuntimeToolAction {
tool: "unity.editor.execute".to_string(),
reason: None,
input,
};
let observation =
observe_agent_runtime_unity_editor_execute(Path::new("C:/unity"), &action, None);
assert_eq!(observation.status, "failed");
}
}
#[test]
fn godot_execute_requires_pending_action_and_rejects_target_overrides() {
for input in [
serde_json::json!({"code":"return 42"}),
serde_json::json!({"code":"return 42", "projectPath":"C:/other"}),
serde_json::json!({"code":"return 42", "processId":123}),
serde_json::json!({"code":"return 42", "dllPath":"C:/other.dll"}),
] {
let action = AgentRuntimeToolAction {
tool: "godot.editor.execute".to_string(),
reason: None,
input,
};
let observation =
observe_agent_runtime_godot_editor_execute(Path::new("C:/godot"), &action, None);
assert_eq!(observation.tool, "godot.editor.execute");
assert_eq!(observation.status, "failed");
}
}
}

Some files were not shown because too many files have changed in this diff Show More