合并 codex/agc-godot-templates-20260921 到 master:新增 Godot 模板与按模板建项分流
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- 新增 Godot 模板源 godot-empty-2d / godot-empty-3d / godot-hello-world / godot-platformer-2d - 按模板建项新增 Godot 分流:改写 project.godot 的 config/name 后走既有 Godot 导入并写入相对根 - Godot 工作区发现放宽:一层多命中按目录名排序取首个,project.godot 允许链接,内置插件行去掉手动启动 - 解决 decision-log.md 冲突:保留渠道安装身份与项目快照按渠道分区两条记录,并合入 Godot 两条记录
This commit is contained in:
@@ -652,7 +652,7 @@ pub(crate) fn import_local_godot_project(
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "project.create")?;
|
||||
if discover_local_godot_project_root(root)?.is_none() {
|
||||
return Err("所选工作区未在根目录或一层子目录发现有效的普通文件 project.godot".to_string());
|
||||
return Err("所选工作区未在根目录或一层子目录发现 project.godot".to_string());
|
||||
}
|
||||
let _lock = acquire_project_write_lock(root, "project.create")?;
|
||||
import_local_godot_project_at(root, project_id.trim(), name.trim())
|
||||
|
||||
@@ -84,9 +84,15 @@ fn godot_metadata_is_link(metadata: &fs::Metadata) -> bool {
|
||||
metadata.file_type().is_symlink() || godot_metadata_is_reparse_point(metadata)
|
||||
}
|
||||
|
||||
/// Godot 工程标记:`project.godot` 解析为普通文件即命中。
|
||||
///
|
||||
/// 2026-09-21 解除“必须是普通文件”的限制:符号链接、Windows reparse point 与
|
||||
/// 硬链接一律跟随,不再因为 `project.godot` 本身是链接而拒绝整个工作区。判据只
|
||||
/// 保留“解析后仍是文件”,目录或悬空链接仍然不算命中。
|
||||
fn inspect_godot_project_marker(root: &Path) -> Result<bool, String> {
|
||||
let project_file = root.join("project.godot");
|
||||
let metadata = match fs::symlink_metadata(&project_file) {
|
||||
// `fs::metadata` 跟随符号链接 / reparse point,因此链接指向的真实对象才是判据。
|
||||
let metadata = match fs::metadata(&project_file) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
||||
Err(error) => {
|
||||
@@ -96,64 +102,12 @@ fn inspect_godot_project_marker(root: &Path) -> Result<bool, String> {
|
||||
));
|
||||
}
|
||||
};
|
||||
if godot_metadata_is_link(&metadata) {
|
||||
return Err(format!(
|
||||
"Godot 项目文件不能是符号链接或 reparse point:{}",
|
||||
project_file.display()
|
||||
));
|
||||
}
|
||||
if !metadata.is_file() {
|
||||
return Err(format!(
|
||||
"Godot 项目文件必须是普通文件:{}",
|
||||
"Godot 项目文件必须是文件:{}",
|
||||
project_file.display()
|
||||
));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if metadata.nlink() != 1 {
|
||||
return Err(format!(
|
||||
"Godot 项目文件不能是硬链接文件:{}",
|
||||
project_file.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
|
||||
};
|
||||
|
||||
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
|
||||
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
|
||||
let file = fs::File::open(&project_file).map_err(|error| {
|
||||
format!(
|
||||
"打开 Godot 项目文件失败:{}: {error}",
|
||||
project_file.display()
|
||||
)
|
||||
})?;
|
||||
// SAFETY: the structure is plain data initialized by GetFileInformationByHandle.
|
||||
let mut information = unsafe { std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>() };
|
||||
// SAFETY: file owns a live handle and information is a valid output pointer.
|
||||
// 取不到句柄信息、目录与 reparse point 一律按拒绝处理,保持 fail-closed。
|
||||
if unsafe { GetFileInformationByHandle(file.as_raw_handle().cast(), &mut information) } == 0
|
||||
|| information.dwFileAttributes
|
||||
& (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
!= 0
|
||||
{
|
||||
return Err(format!(
|
||||
"读取 Godot 项目文件 Windows 身份失败:{}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
if information.nNumberOfLinks != 1 {
|
||||
return Err(format!(
|
||||
"Godot 项目文件不能是硬链接文件:{}",
|
||||
project_file.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -166,6 +120,62 @@ fn validate_godot_project_child_name(name: &std::ffi::OsStr) -> Result<String, S
|
||||
Ok(name.to_string())
|
||||
}
|
||||
|
||||
/// 把模板自带的 Godot 工程显示名改成用户选择的项目名。
|
||||
///
|
||||
/// 只改 `[application]` 段里的 `config/name` 一行:Godot 用它当工程显示名与窗口标题,
|
||||
/// 工程身份仍是 `project.godot` 所在目录,改显示名不影响工程发现或相对根。找不到该行
|
||||
/// 时保持模板原样,不猜测插入位置;除这一行以外的字节逐字保留。
|
||||
pub(crate) fn apply_godot_project_display_name(root: &Path, name: &str) -> Result<(), String> {
|
||||
let path = root.join("project.godot");
|
||||
let text = match fs::read_to_string(&path) {
|
||||
Ok(text) => text,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"读取 Godot 工程配置失败:{}: {error}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
};
|
||||
let newline = if text.contains("\r\n") { "\r\n" } else { "\n" };
|
||||
let ends_with_newline = text.ends_with('\n');
|
||||
let mut replaced = false;
|
||||
let mut lines = Vec::new();
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if !replaced {
|
||||
if let Some(rest) = trimmed.strip_prefix("config/name") {
|
||||
let rest = rest.trim_start();
|
||||
if let Some(value) = rest.strip_prefix('=') {
|
||||
let value = value.trim();
|
||||
if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') {
|
||||
let indent = &line[..line.len() - trimmed.len()];
|
||||
lines.push(format!(
|
||||
"{indent}config/name=\"{}\"",
|
||||
escape_godot_project_string(name)
|
||||
));
|
||||
replaced = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push(line.to_string());
|
||||
}
|
||||
if !replaced {
|
||||
return Ok(());
|
||||
}
|
||||
let mut updated = lines.join(newline);
|
||||
if ends_with_newline {
|
||||
updated.push_str(newline);
|
||||
}
|
||||
write_game_creator_private_file(&path, updated.as_bytes(), "Godot 工程配置")
|
||||
}
|
||||
|
||||
fn escape_godot_project_string(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
}
|
||||
|
||||
fn validate_manifest_godot_project_root(value: Option<&str>) -> Result<(), String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(());
|
||||
@@ -346,13 +356,9 @@ pub(crate) fn discover_local_godot_project_root(
|
||||
matches.push(validate_godot_project_child_name(&entry.file_name())?);
|
||||
}
|
||||
matches.sort();
|
||||
if matches.len() > 1 {
|
||||
return Err(format!(
|
||||
"工作区一层子目录中发现多个 Godot 项目:{}",
|
||||
matches.join("、")
|
||||
));
|
||||
}
|
||||
let Some(relative_root) = matches.pop() else {
|
||||
// 2026-09-21 解除“必须唯一”的限制:一层子目录出现多个 Godot 工程时按名称排序
|
||||
// 取第一个,结果确定且可复现,不再因为存在第二个工程而整体失败关闭。
|
||||
let Some(relative_root) = matches.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -694,9 +700,8 @@ pub(crate) fn import_local_godot_project_at(
|
||||
if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
|
||||
return Err("Godot 工作区目录不存在或不是普通文件夹".to_string());
|
||||
}
|
||||
let godot_project_root = discover_local_godot_project_root(root)?.ok_or_else(|| {
|
||||
"所选工作区未在根目录或一层子目录发现有效的普通文件 project.godot".to_string()
|
||||
})?;
|
||||
let godot_project_root = discover_local_godot_project_root(root)?
|
||||
.ok_or_else(|| "所选工作区未在根目录或一层子目录发现 project.godot".to_string())?;
|
||||
if project_id.is_empty() {
|
||||
return Err("项目 ID 不能为空".to_string());
|
||||
}
|
||||
|
||||
@@ -133,17 +133,18 @@ fn root_godot_project_takes_priority_over_direct_child_projects() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_direct_child_godot_projects_before_writing_agent_metadata() {
|
||||
fn picks_the_first_direct_child_godot_project_in_name_order() {
|
||||
let workspace = godot_import_test_path("multiple-children");
|
||||
write_godot_project(&workspace.join("alpha"), "Alpha");
|
||||
write_godot_project(&workspace.join("beta"), "Beta");
|
||||
|
||||
let error = import_local_godot_project_at(&workspace, "ambiguous", "Ambiguous")
|
||||
.expect_err("multiple direct child Godot projects must fail");
|
||||
let result = import_local_godot_project_at(&workspace, "ambiguous", "Ambiguous")
|
||||
.expect("multiple direct child Godot projects must resolve deterministically");
|
||||
|
||||
assert!(error.contains("多个 Godot 项目"), "{error}");
|
||||
assert!(!workspace.join(".agent").exists());
|
||||
assert!(!workspace.join("alpha/.agent").exists());
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("alpha"));
|
||||
assert_manifest_godot_root(&workspace, "alpha");
|
||||
assert!(workspace.join(".agent/manifest.json").is_file());
|
||||
// 未选中的候选工程不写任何 AGC 元数据。
|
||||
assert!(!workspace.join("beta/.agent").exists());
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
@@ -187,23 +188,21 @@ fn calibrates_existing_manifest_to_the_discovered_godot_root() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambiguous_layout_does_not_rewrite_an_existing_manifest() {
|
||||
fn ambiguous_layout_calibrates_an_existing_manifest_deterministically() {
|
||||
let workspace = godot_import_test_path("ambiguous-existing");
|
||||
init_local_game_project_at(&workspace, "existing-project", "Existing Project")
|
||||
.expect("initialize existing workspace");
|
||||
write_godot_project(&workspace.join("game"), "Game");
|
||||
write_godot_project(&workspace.join("other"), "Other");
|
||||
let manifest_path = workspace.join(".agent/manifest.json");
|
||||
let original = fs::read(&manifest_path).expect("read original manifest");
|
||||
|
||||
let error = import_local_godot_project_at(&workspace, "ignored", "Ignored")
|
||||
.expect_err("ambiguous existing workspace must fail");
|
||||
let result = import_local_godot_project_at(&workspace, "ignored", "Ignored")
|
||||
.expect("ambiguous existing workspace must calibrate to one candidate");
|
||||
|
||||
assert!(error.contains("多个 Godot 项目"), "{error}");
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read unchanged manifest"),
|
||||
original
|
||||
);
|
||||
assert_eq!(result.manifest.project_id, "existing-project");
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("game"));
|
||||
assert_manifest_godot_root(&workspace, "game");
|
||||
assert!(!workspace.join("game/.agent").exists());
|
||||
assert!(!workspace.join("other/.agent").exists());
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
|
||||
@@ -286,7 +285,7 @@ fn manifest_read_rejects_unsafe_persisted_godot_project_root() {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_symbolic_link_project_marker_without_writing_agent_metadata() {
|
||||
fn accepts_symbolic_link_project_marker() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let workspace = godot_import_test_path("linked-marker");
|
||||
@@ -294,11 +293,51 @@ fn rejects_symbolic_link_project_marker_without_writing_agent_metadata() {
|
||||
fs::write(workspace.join("real.godot"), "[application]\n").expect("write real marker");
|
||||
symlink("real.godot", workspace.join("project.godot")).expect("link project marker");
|
||||
|
||||
let error = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect_err("linked project.godot must fail");
|
||||
let result = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect("linked project.godot must be accepted");
|
||||
|
||||
assert!(error.contains("符号链接"), "{error}");
|
||||
assert!(!workspace.join(".agent").exists());
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("."));
|
||||
assert_manifest_godot_root(&workspace, ".");
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn accepts_windows_hard_link_project_marker() {
|
||||
let workspace = godot_import_test_path("windows-hard-link-marker");
|
||||
fs::create_dir_all(&workspace).expect("create hard link marker workspace");
|
||||
fs::write(workspace.join("real.godot"), "[application]\n").expect("write real marker");
|
||||
fs::hard_link(
|
||||
workspace.join("real.godot"),
|
||||
workspace.join("project.godot"),
|
||||
)
|
||||
.expect("hard link project marker");
|
||||
|
||||
let result = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect("hard linked project.godot must be accepted");
|
||||
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("."));
|
||||
assert_manifest_godot_root(&workspace, ".");
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn accepts_windows_reparse_project_marker() {
|
||||
let workspace = godot_import_test_path("windows-reparse-marker");
|
||||
fs::create_dir_all(&workspace).expect("create reparse marker workspace");
|
||||
fs::write(workspace.join("real.godot"), "[application]\n").expect("write real marker");
|
||||
if std::os::windows::fs::symlink_file("real.godot", workspace.join("project.godot")).is_err() {
|
||||
// 未开启开发者模式的机器创建文件符号链接需要额外权限,跳过而不是误报通过。
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let result = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect("reparse point project.godot must be accepted");
|
||||
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("."));
|
||||
assert_manifest_godot_root(&workspace, ".");
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
|
||||
@@ -344,7 +383,7 @@ fn ignores_unrelated_symbolic_link_while_importing_a_unique_regular_child() {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_linked_marker_inside_a_regular_child_candidate() {
|
||||
fn accepts_linked_marker_inside_a_regular_child_candidate() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let workspace = godot_import_test_path("linked-child-marker");
|
||||
@@ -353,11 +392,12 @@ fn rejects_linked_marker_inside_a_regular_child_candidate() {
|
||||
fs::write(godot_root.join("real.godot"), "[application]\n").expect("write real marker");
|
||||
symlink("real.godot", godot_root.join("project.godot")).expect("link project marker");
|
||||
|
||||
let error = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect_err("linked marker in a regular child must fail");
|
||||
let result = import_local_godot_project_at(&workspace, "linked", "Linked")
|
||||
.expect("linked marker in a regular child must be accepted");
|
||||
|
||||
assert!(error.contains("符号链接"), "{error}");
|
||||
assert!(!workspace.join(".agent").exists());
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("game"));
|
||||
assert_manifest_godot_root(&workspace, "game");
|
||||
assert!(!godot_root.join(".agent").exists());
|
||||
fs::remove_dir_all(workspace).ok();
|
||||
}
|
||||
|
||||
@@ -397,6 +437,42 @@ fn rejects_non_godot_directory_without_writing_agent_metadata() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrites_only_the_godot_display_name_line() {
|
||||
let root = godot_import_test_path("display-name");
|
||||
fs::create_dir_all(&root).expect("create project root");
|
||||
fs::write(
|
||||
root.join("project.godot"),
|
||||
"config_version=5\n\n[application]\n\nconfig/name=\"模板名\"\nrun/main_scene=\"res://scenes/main.tscn\"\n",
|
||||
)
|
||||
.expect("write project.godot");
|
||||
|
||||
apply_godot_project_display_name(&root, "我的\"平台跳跃\"").expect("rewrite display name");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("project.godot")).expect("read project.godot"),
|
||||
"config_version=5\n\n[application]\n\nconfig/name=\"我的\\\"平台跳跃\\\"\"\nrun/main_scene=\"res://scenes/main.tscn\"\n"
|
||||
);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_a_godot_project_without_a_display_name_line_untouched() {
|
||||
let root = godot_import_test_path("no-display-name");
|
||||
fs::create_dir_all(&root).expect("create project root");
|
||||
let original =
|
||||
"config_version=5\n\n[application]\n\nrun/main_scene=\"res://scenes/main.tscn\"\n";
|
||||
fs::write(root.join("project.godot"), original).expect("write project.godot");
|
||||
|
||||
apply_godot_project_display_name(&root, "我的项目").expect("no display name is not an error");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("project.godot")).expect("read project.godot"),
|
||||
original
|
||||
);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
fn write_raw_manifest_fixture(workspace: &Path, payload: &serde_json::Value) -> (PathBuf, String) {
|
||||
let manifest_path = workspace.join(".agent/manifest.json");
|
||||
fs::create_dir_all(manifest_path.parent().expect("manifest parent"))
|
||||
|
||||
@@ -990,6 +990,16 @@ pub(crate) fn create_project_from_installed_template_at(
|
||||
&project_name,
|
||||
);
|
||||
}
|
||||
// Godot 模板同样按工程文件识别:走既有 Godot 导入流程,写入
|
||||
// `godotProjectRoot` 并按用户输入改写工程显示名,不生成 Web 占位入口。
|
||||
if discover_local_godot_project_root(&project_root)?.is_some() {
|
||||
apply_godot_project_display_name(&project_root, &project_name)?;
|
||||
return import_local_godot_project_at(
|
||||
&project_root,
|
||||
&format!("gameagent-{workspace_id}"),
|
||||
&project_name,
|
||||
);
|
||||
}
|
||||
init_local_game_project_at(
|
||||
&project_root,
|
||||
&format!("gameagent-{workspace_id}"),
|
||||
@@ -1492,6 +1502,49 @@ mod tests {
|
||||
fs::remove_dir_all(&projects_root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn godot_template_creates_native_project_with_relative_root_and_display_name() {
|
||||
let cache_root = tempfile::tempdir().expect("temp dir");
|
||||
let projects_root = unique_projects_root();
|
||||
let config_source = "config_version=5\n\n[application]\n\nconfig/name=\"Godot 模板\"\nrun/main_scene=\"res://scenes/main.tscn\"\n";
|
||||
let archive = build_archive(&[
|
||||
("project.godot", config_source.as_bytes()),
|
||||
("scenes/main.tscn", b"[gd_scene format=3]\n"),
|
||||
]);
|
||||
let mut summary = sample_summary();
|
||||
summary.id = "godot-fixture-template".to_string();
|
||||
summary.runtime = "godot".to_string();
|
||||
summary.entry = "project.godot".to_string();
|
||||
summary.zip_size_bytes = archive.len() as u64;
|
||||
summary.zip_sha256 = sha256_hex(&archive);
|
||||
let record = install_template_archive(cache_root.path(), &summary, &archive)
|
||||
.expect("install Godot template");
|
||||
|
||||
let result = create_project_from_installed_template_at(
|
||||
&projects_root,
|
||||
Path::new(&record.project_dir),
|
||||
Some("我的平台跳跃"),
|
||||
false,
|
||||
)
|
||||
.expect("create Godot project from template");
|
||||
|
||||
// Godot 模板走 Godot 导入:记录相对根,且不生成 Web 占位入口与并行目录。
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("."));
|
||||
assert_eq!(result.manifest.name, "我的平台跳跃");
|
||||
let project_root = Path::new(&result.project_path);
|
||||
assert!(!project_root.join("game").exists());
|
||||
assert!(project_root.join("scenes/main.tscn").is_file());
|
||||
let config =
|
||||
fs::read_to_string(project_root.join("project.godot")).expect("read project.godot");
|
||||
assert!(config.contains("config/name=\"我的平台跳跃\""), "{config}");
|
||||
assert!(
|
||||
config.contains("run/main_scene=\"res://scenes/main.tscn\""),
|
||||
"只改显示名,其余行逐字保留:{config}"
|
||||
);
|
||||
assert!(!project_root.join(TEMPLATE_INSTALLED_MARKER_FILE).exists());
|
||||
fs::remove_dir_all(&projects_root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_to_create_project_when_template_is_not_installed() {
|
||||
let projects_root = tempfile::tempdir().expect("temp dir");
|
||||
|
||||
@@ -2532,42 +2532,45 @@ fn project_directory_status_reports_workspace_relative_godot_root() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_directory_status_rejects_ambiguous_direct_child_godot_projects() {
|
||||
fn project_directory_status_resolves_ambiguous_direct_child_godot_projects() {
|
||||
let root = unique_project_path();
|
||||
for child in ["alpha", "beta"] {
|
||||
// 目录枚举顺序不可信:故意倒序创建,证明选择按名称而不是按创建顺序。
|
||||
for child in ["beta", "alpha"] {
|
||||
let godot_root = root.join(child);
|
||||
fs::create_dir_all(&godot_root).expect("create nested Godot project");
|
||||
fs::write(godot_root.join("project.godot"), "[application]\n")
|
||||
.expect("write nested project.godot");
|
||||
}
|
||||
|
||||
let error = inspect_local_project_directory_sync(root.to_string_lossy().to_string())
|
||||
.expect_err("ambiguous Godot workspace must fail inspection");
|
||||
let status = inspect_local_project_directory_sync(root.to_string_lossy().to_string())
|
||||
.expect("multiple direct child Godot projects must resolve deterministically");
|
||||
|
||||
assert!(error.contains("多个 Godot 项目"), "{error}");
|
||||
assert!(status.is_godot_project);
|
||||
assert_eq!(status.godot_project_root.as_deref(), Some("alpha"));
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn godot_import_command_rejects_ambiguity_before_creating_the_project_lock() {
|
||||
fn godot_import_command_resolves_child_ambiguity_and_releases_the_lock() {
|
||||
let root = unique_project_path();
|
||||
for child in ["alpha", "beta"] {
|
||||
for child in ["beta", "alpha"] {
|
||||
let godot_root = root.join(child);
|
||||
fs::create_dir_all(&godot_root).expect("create nested Godot project");
|
||||
fs::write(godot_root.join("project.godot"), "[application]\n")
|
||||
.expect("write nested project.godot");
|
||||
}
|
||||
|
||||
let error = import_local_godot_project(
|
||||
let result = import_local_godot_project(
|
||||
root.to_string_lossy().into_owned(),
|
||||
"ambiguous".to_string(),
|
||||
"Ambiguous".to_string(),
|
||||
)
|
||||
.expect_err("ambiguous Godot workspace must fail before locking");
|
||||
.expect("ambiguous Godot workspace must import the first candidate deterministically");
|
||||
|
||||
assert!(error.contains("多个 Godot 项目"), "{error}");
|
||||
assert_eq!(result.manifest.godot_project_root.as_deref(), Some("alpha"));
|
||||
assert!(root.join(".agent/manifest.json").is_file());
|
||||
assert!(!root.join("beta/.agent").exists());
|
||||
assert!(!root.join(PROJECT_WRITE_LOCK_PATH).exists());
|
||||
assert!(!root.join(".agent").exists());
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,12 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
};
|
||||
|
||||
type RuntimeSettingsSection =
|
||||
'general' | 'workspace' | 'agents' | 'extensions' | 'advanced' | 'about';
|
||||
| 'general'
|
||||
| 'workspace'
|
||||
| 'agents'
|
||||
| 'extensions'
|
||||
| 'advanced'
|
||||
| 'about';
|
||||
|
||||
type RuntimeConfigToast = {
|
||||
tone: 'success' | 'error';
|
||||
@@ -1076,30 +1081,16 @@ export function RuntimeConfigDialog({
|
||||
: '已启用'}
|
||||
</span>
|
||||
{plugin.enabled && plugin.hasRuntime ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
agcPluginsBusy ||
|
||||
plugin.status === 'invalid'
|
||||
}
|
||||
onClick={() => void toggleAgcPlugin(plugin)}
|
||||
>
|
||||
{plugin.status === 'running'
|
||||
? '停止'
|
||||
: '启动'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
agcPluginsBusy ||
|
||||
plugin.status !== 'running'
|
||||
}
|
||||
onClick={() => void reloadPlugin(plugin)}
|
||||
>
|
||||
重载
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
agcPluginsBusy ||
|
||||
plugin.status !== 'running'
|
||||
}
|
||||
onClick={() => void reloadPlugin(plugin)}
|
||||
>
|
||||
重载
|
||||
</button>
|
||||
) : null}
|
||||
{plugin.status === 'running'
|
||||
? plugin.panels.map((panel) => (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-label="Godot 空白 2D 工程">
|
||||
<defs><linearGradient id="bg" x2="1" y2="1"><stop stop-color="#1b2733"/><stop offset="1" stop-color="#0b1220"/></linearGradient></defs>
|
||||
<rect width="960" height="540" fill="url(#bg)"/>
|
||||
<circle cx="812" cy="132" r="204" fill="#478cbf" opacity=".14"/>
|
||||
<rect x="672" y="232" width="184" height="128" rx="18" fill="none" stroke="#478cbf" stroke-width="4" opacity=".65"/>
|
||||
<circle cx="728" cy="296" r="13" fill="#478cbf" opacity=".75"/>
|
||||
<circle cx="800" cy="296" r="13" fill="#478cbf" opacity=".75"/>
|
||||
<rect x="72" y="132" width="6" height="196" rx="3" fill="#478cbf"/>
|
||||
<text x="104" y="197" fill="#fff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="42" font-weight="700">Godot 空白 2D 工程</text>
|
||||
<text x="108" y="253" fill="#c9dbed" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="22">Godot 4.7 · GDScript</text>
|
||||
<text x="108" y="302" fill="#8ca8c5" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="18">1280×720 窗口 · GL Compatibility</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "godot-empty-2d",
|
||||
"title": "Godot 空白 2D 工程",
|
||||
"summary": "Godot 4.7 原生二维空白工程:1280×720 窗口、GL Compatibility 渲染、单场景入口与占位说明节点已就绪。",
|
||||
"tags": [
|
||||
"空白",
|
||||
"起步工程",
|
||||
"2d",
|
||||
"godot"
|
||||
],
|
||||
"runtime": "godot",
|
||||
"engine": "godot",
|
||||
"engineVersion": "4.7",
|
||||
"templateVersion": "0.1.0",
|
||||
"entry": "project.godot",
|
||||
"coverWidth": 960,
|
||||
"coverHeight": 540
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Godot 4+ 编辑器与导入缓存
|
||||
.godot/
|
||||
|
||||
# 导出产物
|
||||
export/
|
||||
build/
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="24" fill="#1b2733"/>
|
||||
<rect x="26" y="30" width="76" height="60" rx="10" fill="none" stroke="#478cbf" stroke-width="6"/>
|
||||
<circle cx="48" cy="60" r="7" fill="#478cbf"/>
|
||||
<circle cx="80" cy="60" r="7" fill="#478cbf"/>
|
||||
<rect x="46" y="100" width="36" height="6" rx="3" fill="#478cbf"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,16 @@
|
||||
; Engine configuration file.
|
||||
; AGC 模板库:Godot 4.7 空白 2D 工程。解压后即为工程根。
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Godot 空白 2D 工程"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="Hint" type="Label" parent="."]
|
||||
offset_left = 48.0
|
||||
offset_top = 48.0
|
||||
offset_right = 1232.0
|
||||
offset_bottom = 160.0
|
||||
text = "AGC · Godot 空白 2D 工程"
|
||||
theme_override_font_sizes/font_size = 28
|
||||
@@ -0,0 +1,12 @@
|
||||
extends Node2D
|
||||
|
||||
## AGC · Godot 空白 2D 工程
|
||||
##
|
||||
## 只保留最小可运行骨架:一个 Node2D 根节点加一段说明文字。
|
||||
## 继续开发时把新场景放进 `scenes/`,其余节点从 `scenes/main.tscn` 挂载。
|
||||
|
||||
@onready var _hint: Label = $Hint
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_hint.text = "AGC · Godot 空白 2D 工程\n把场景挂到 scenes/ 即可开始,入口在 project.godot 的 run/main_scene"
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-label="Godot 空白 3D 场景">
|
||||
<defs><linearGradient id="bg" x2="1" y2="1"><stop stop-color="#16202c"/><stop offset="1" stop-color="#080d16"/></linearGradient></defs>
|
||||
<rect width="960" height="540" fill="url(#bg)"/>
|
||||
<circle cx="806" cy="150" r="200" fill="#478cbf" opacity=".12"/>
|
||||
<path d="M764 262 828 300 828 376 764 414 700 376 700 300Z" fill="none" stroke="#478cbf" stroke-width="3" opacity=".7"/>
|
||||
<path d="M700 300 764 338 828 300M764 338V414" fill="none" stroke="#478cbf" stroke-width="3" opacity=".45"/>
|
||||
<rect x="72" y="132" width="6" height="196" rx="3" fill="#478cbf"/>
|
||||
<text x="104" y="197" fill="#fff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="42" font-weight="700">Godot 空白 3D 场景</text>
|
||||
<text x="108" y="253" fill="#c9dbed" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="22">Godot 4.7 · GDScript</text>
|
||||
<text x="108" y="302" fill="#8ca8c5" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="18">相机 · 平行光 · 天空环境</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "godot-empty-3d",
|
||||
"title": "Godot 空白 3D 场景",
|
||||
"summary": "Godot 4.7 原生三维空白工程:相机、平行光、天空环境与一个自转立方体已就绪,可直接开始搭建场景。",
|
||||
"tags": [
|
||||
"空白",
|
||||
"起步工程",
|
||||
"3d",
|
||||
"godot"
|
||||
],
|
||||
"runtime": "godot",
|
||||
"engine": "godot",
|
||||
"engineVersion": "4.7",
|
||||
"templateVersion": "0.1.0",
|
||||
"entry": "project.godot",
|
||||
"coverWidth": 960,
|
||||
"coverHeight": 540
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Godot 4+ 编辑器与导入缓存
|
||||
.godot/
|
||||
|
||||
# 导出产物
|
||||
export/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="24" fill="#1b2733"/>
|
||||
<path d="M64 30 96 48 96 84 64 102 32 84 32 48Z" fill="none" stroke="#478cbf" stroke-width="6"/>
|
||||
<path d="M32 48 64 66 96 48M64 66V102" fill="none" stroke="#478cbf" stroke-width="4" opacity=".7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
@@ -0,0 +1,16 @@
|
||||
; Engine configuration file.
|
||||
; AGC 模板库:Godot 4.7 空白 3D 场景。解压后即为工程根。
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Godot 空白 3D 场景"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
|
||||
[sub_resource type="Environment" id="Environment_main"]
|
||||
background_mode = 1
|
||||
background_color = Color(0.09, 0.11, 0.16, 1)
|
||||
ambient_light_source = 1
|
||||
ambient_light_color = Color(0.62, 0.71, 0.86, 1)
|
||||
ambient_light_energy = 0.4
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_cube"]
|
||||
size = Vector3(1.6, 1.6, 1.6)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cube"]
|
||||
albedo_color = Color(0.28, 0.55, 0.75, 1)
|
||||
roughness = 0.4
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_main")
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866025, -0.25, 0.433013, 0, 0.866025, 0.5, -0.5, -0.433013, 0.75, 3, 6, 4)
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.951057, 0.309017, 0, -0.309017, 0.951057, 0, 2.4, 6)
|
||||
|
||||
[node name="Cube" type="MeshInstance3D" parent="."]
|
||||
material_override = SubResource("StandardMaterial3D_cube")
|
||||
mesh = SubResource("BoxMesh_cube")
|
||||
|
||||
[node name="Hud" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="Hint" type="Label" parent="Hud"]
|
||||
offset_left = 40.0
|
||||
offset_top = 32.0
|
||||
offset_right = 1000.0
|
||||
offset_bottom = 96.0
|
||||
text = "AGC · Godot 空白 3D 场景"
|
||||
theme_override_font_sizes/font_size = 26
|
||||
@@ -0,0 +1,13 @@
|
||||
extends Node3D
|
||||
|
||||
## AGC · Godot 空白 3D 场景
|
||||
##
|
||||
## 只保留最小可运行骨架:相机、平行光、天空环境与一个缓慢自转的立方体。
|
||||
## 继续开发时把新场景放进 `scenes/`,或在 `Cube` 下挂模型与脚本。
|
||||
|
||||
@onready var _cube: MeshInstance3D = $Cube
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_cube.rotate_y(delta * 0.6)
|
||||
_cube.rotate_x(delta * 0.2)
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-label="Godot Hello World">
|
||||
<defs><linearGradient id="bg" x2="1" y2="1"><stop stop-color="#1b2733"/><stop offset="1" stop-color="#0b1220"/></linearGradient></defs>
|
||||
<rect width="960" height="540" fill="url(#bg)"/>
|
||||
<circle cx="812" cy="140" r="196" fill="#478cbf" opacity=".13"/>
|
||||
<rect x="700" y="300" width="64" height="64" rx="10" fill="#478cbf" opacity=".85"/>
|
||||
<rect x="806" y="228" width="36" height="36" rx="6" fill="#f2c14e" opacity=".9"/>
|
||||
<path d="M764 300 786 250 808 300" fill="none" stroke="#f2c14e" stroke-width="4" opacity=".55"/>
|
||||
<rect x="72" y="132" width="6" height="196" rx="3" fill="#478cbf"/>
|
||||
<text x="104" y="197" fill="#fff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="42" font-weight="700">Godot Hello World</text>
|
||||
<text x="108" y="253" fill="#c9dbed" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="22">Godot 4.7 · GDScript</text>
|
||||
<text x="108" y="302" fill="#8ca8c5" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="18">移动与收集 · 最小可玩示例</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "godot-hello-world",
|
||||
"title": "Godot Hello World",
|
||||
"summary": "Godot 4.7 最小可玩示例:方向键移动方块,碰到金色目标加分,空格重置;只用 GDScript 与内置输入动作。",
|
||||
"tags": [
|
||||
"示例",
|
||||
"起步工程",
|
||||
"2d",
|
||||
"godot"
|
||||
],
|
||||
"runtime": "godot",
|
||||
"engine": "godot",
|
||||
"engineVersion": "4.7",
|
||||
"templateVersion": "0.1.0",
|
||||
"entry": "project.godot",
|
||||
"coverWidth": 960,
|
||||
"coverHeight": 540
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Godot 4+ 编辑器与导入缓存
|
||||
.godot/
|
||||
|
||||
# 导出产物
|
||||
export/
|
||||
build/
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="24" fill="#1b2733"/>
|
||||
<rect x="22" y="52" width="40" height="40" rx="8" fill="#478cbf"/>
|
||||
<circle cx="94" cy="46" r="14" fill="none" stroke="#f2c14e" stroke-width="6"/>
|
||||
<rect x="66" y="84" width="40" height="8" rx="4" fill="#478cbf" opacity=".55"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
+16
@@ -0,0 +1,16 @@
|
||||
; Engine configuration file.
|
||||
; AGC 模板库:Godot 4.7 二维移动与收集示例。解压后即为工程根。
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Godot Hello World"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user