diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index a079cca8b..bf71cb451 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -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()) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs index 2094ef492..ae4ad0550 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs @@ -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 { 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 { )); } }; - 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::() }; - // 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 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()); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs index 434adb727..ab0f99690 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs @@ -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")) diff --git a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs index ced19bec9..fbf8fbc10 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs @@ -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"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index b722582a2..4a86e86cd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -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(); } diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx index de412fabb..57bff26e3 100644 --- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx +++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx @@ -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({ : '已启用'} {plugin.enabled && plugin.hasRuntime ? ( - <> - - - + ) : null} {plugin.status === 'running' ? plugin.panels.map((panel) => ( diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/cover.svg b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/cover.svg new file mode 100644 index 000000000..f22daa939 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/cover.svg @@ -0,0 +1,12 @@ + + + + + + + + + Godot 空白 2D 工程 + Godot 4.7 · GDScript + 1280×720 窗口 · GL Compatibility + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/meta.json b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/meta.json new file mode 100644 index 000000000..c9befb571 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/meta.json @@ -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 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/.gitignore new file mode 100644 index 000000000..a643a2581 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/.gitignore @@ -0,0 +1,6 @@ +# Godot 4+ 编辑器与导入缓存 +.godot/ + +# 导出产物 +export/ +build/ diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/icon.svg b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/icon.svg new file mode 100644 index 000000000..4675426b7 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/project.godot b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/project.godot new file mode 100644 index 000000000..bc60b7c8e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/project.godot @@ -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" diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scenes/main.tscn b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scenes/main.tscn new file mode 100644 index 000000000..52bbc79c8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scenes/main.tscn @@ -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 diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scripts/main.gd b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scripts/main.gd new file mode 100644 index 000000000..cadbd6801 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-2d/project/scripts/main.gd @@ -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" diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/cover.svg b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/cover.svg new file mode 100644 index 000000000..c8ff1405e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/cover.svg @@ -0,0 +1,11 @@ + + + + + + + + Godot 空白 3D 场景 + Godot 4.7 · GDScript + 相机 · 平行光 · 天空环境 + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/meta.json b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/meta.json new file mode 100644 index 000000000..3b3abd3ac --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/meta.json @@ -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 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/.gitignore new file mode 100644 index 000000000..a643a2581 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/.gitignore @@ -0,0 +1,6 @@ +# Godot 4+ 编辑器与导入缓存 +.godot/ + +# 导出产物 +export/ +build/ diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/icon.svg b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/icon.svg new file mode 100644 index 000000000..1f92223a4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/project.godot b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/project.godot new file mode 100644 index 000000000..6a8bb2ea0 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/project.godot @@ -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" diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scenes/main.tscn b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scenes/main.tscn new file mode 100644 index 000000000..cc3aa8ff1 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scenes/main.tscn @@ -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 diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scripts/main.gd b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scripts/main.gd new file mode 100644 index 000000000..bfe52ec9a --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-empty-3d/project/scripts/main.gd @@ -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) diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/cover.svg b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/cover.svg new file mode 100644 index 000000000..ae5f254b7 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/cover.svg @@ -0,0 +1,12 @@ + + + + + + + + + Godot Hello World + Godot 4.7 · GDScript + 移动与收集 · 最小可玩示例 + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/meta.json b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/meta.json new file mode 100644 index 000000000..ad0b84a73 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/meta.json @@ -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 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/.gitignore new file mode 100644 index 000000000..a643a2581 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/.gitignore @@ -0,0 +1,6 @@ +# Godot 4+ 编辑器与导入缓存 +.godot/ + +# 导出产物 +export/ +build/ diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/icon.svg b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/icon.svg new file mode 100644 index 000000000..b46265b60 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/project.godot b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/project.godot new file mode 100644 index 000000000..4804660ed --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/project.godot @@ -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" diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scenes/main.tscn b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scenes/main.tscn new file mode 100644 index 000000000..47043871e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scenes/main.tscn @@ -0,0 +1,34 @@ +[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="Backdrop" type="Polygon2D" parent="."] +polygon = PackedVector2Array(0, 0, 1280, 0, 1280, 720, 0, 720) +color = Color(0.11, 0.15, 0.21, 1) + +[node name="Player" type="Node2D" parent="."] +position = Vector2(320, 360) + +[node name="Body" type="Polygon2D" parent="Player"] +polygon = PackedVector2Array(-24, -24, 24, -24, 24, 24, -24, 24) +color = Color(0.28, 0.55, 0.75, 1) + +[node name="Target" type="Node2D" parent="."] +position = Vector2(900, 240) + +[node name="Body" type="Polygon2D" parent="Target"] +polygon = PackedVector2Array(-18, -18, 18, -18, 18, 18, -18, 18) +color = Color(0.95, 0.76, 0.31, 1) + +[node name="Hud" type="CanvasLayer" parent="."] + +[node name="Score" type="Label" parent="Hud"] +offset_left = 40.0 +offset_top = 32.0 +offset_right = 900.0 +offset_bottom = 120.0 +text = "得分:0" +theme_override_font_sizes/font_size = 26 diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scripts/main.gd b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scripts/main.gd new file mode 100644 index 000000000..76a23f51b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-hello-world/project/scripts/main.gd @@ -0,0 +1,53 @@ +extends Node2D + +## AGC · Godot Hello World +## +## 最小可玩示例:方向键移动方块,碰到金色目标得分并随机刷新目标; +## 空格或回车重置得分。只使用 Godot 内置的 ui_* 输入动作,不需要额外输入映射。 + +const SPEED := 420.0 +const PICKUP_RADIUS := 42.0 +const PLAYER_BOUNDS := Rect2(28.0, 28.0, 1224.0, 664.0) + +var _score := 0 + +@onready var _player: Node2D = $Player +@onready var _target: Node2D = $Target +@onready var _hud: Label = $Hud/Score + + +func _ready() -> void: + _randomize_target() + _update_hud() + + +func _process(delta: float) -> void: + var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + _player.position += direction * SPEED * delta + _player.position.x = clampf( + _player.position.x, PLAYER_BOUNDS.position.x, PLAYER_BOUNDS.end.x + ) + _player.position.y = clampf( + _player.position.y, PLAYER_BOUNDS.position.y, PLAYER_BOUNDS.end.y + ) + if _player.position.distance_to(_target.position) <= PICKUP_RADIUS: + _score += 1 + _randomize_target() + _update_hud() + + +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("ui_accept"): + _score = 0 + _update_hud() + + +func _randomize_target() -> void: + _target.position = Vector2( + randf_range(PLAYER_BOUNDS.position.x, PLAYER_BOUNDS.end.x), + randf_range(PLAYER_BOUNDS.position.y, PLAYER_BOUNDS.end.y), + ) + + +func _update_hud() -> void: + _hud.text = "得分:%d\n方向键移动 · 空格/回车重置" % _score diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/cover.svg b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/cover.svg new file mode 100644 index 000000000..6ee82e63c --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + Godot 2D 平台跳跃 + Godot 4.7 · GDScript + 角色 · 平台 · 相机跟随 · 金币 + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/meta.json b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/meta.json new file mode 100644 index 000000000..4157bd441 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/meta.json @@ -0,0 +1,18 @@ +{ + "id": "godot-platformer-2d", + "title": "Godot 2D 平台跳跃起步工程", + "summary": "Godot 4.7 原生二维平台跳跃骨架:CharacterBody2D 角色、地面与两级平台、相机跟随、三枚金币与重开,全部用 GDScript 与内置输入动作实现。", + "tags": [ + "起步工程", + "2d", + "平台跳跃", + "godot" + ], + "runtime": "godot", + "engine": "godot", + "engineVersion": "4.7", + "templateVersion": "0.1.0", + "entry": "project.godot", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/.gitignore new file mode 100644 index 000000000..a643a2581 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/.gitignore @@ -0,0 +1,6 @@ +# Godot 4+ 编辑器与导入缓存 +.godot/ + +# 导出产物 +export/ +build/ diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/icon.svg b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/icon.svg new file mode 100644 index 000000000..fd4ef4619 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/project.godot b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/project.godot new file mode 100644 index 000000000..d6cb7b5b6 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/project.godot @@ -0,0 +1,16 @@ +; Engine configuration file. +; AGC 模板库:Godot 4.7 二维平台跳跃起步工程。解压后即为工程根。 + +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" diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scenes/main.tscn b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scenes/main.tscn new file mode 100644 index 000000000..4545bf2bd --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scenes/main.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] +[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_player"] +size = Vector2(40, 56) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_ground"] +size = Vector2(1280, 64) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_platform"] +size = Vector2(280, 32) + +[node name="Main" type="Node2D"] +script = ExtResource("1_main") + +[node name="Backdrop" type="Polygon2D" parent="."] +polygon = PackedVector2Array(0, 0, 1280, 0, 1280, 720, 0, 720) +color = Color(0.11, 0.15, 0.21, 1) + +[node name="Ground" type="StaticBody2D" parent="."] +position = Vector2(640, 688) + +[node name="Shape" type="CollisionShape2D" parent="Ground"] +shape = SubResource("RectangleShape2D_ground") + +[node name="Visual" type="Polygon2D" parent="Ground"] +polygon = PackedVector2Array(-640, -32, 640, -32, 640, 32, -640, 32) +color = Color(0.2, 0.27, 0.35, 1) + +[node name="Platform" type="StaticBody2D" parent="."] +position = Vector2(360, 560) + +[node name="Shape" type="CollisionShape2D" parent="Platform"] +shape = SubResource("RectangleShape2D_platform") + +[node name="Visual" type="Polygon2D" parent="Platform"] +polygon = PackedVector2Array(-140, -16, 140, -16, 140, 16, -140, 16) +color = Color(0.24, 0.34, 0.44, 1) + +[node name="Platform2" type="StaticBody2D" parent="."] +position = Vector2(900, 470) + +[node name="Shape" type="CollisionShape2D" parent="Platform2"] +shape = SubResource("RectangleShape2D_platform") + +[node name="Visual" type="Polygon2D" parent="Platform2"] +polygon = PackedVector2Array(-140, -16, 140, -16, 140, 16, -140, 16) +color = Color(0.24, 0.34, 0.44, 1) + +[node name="Coins" type="Node2D" parent="."] + +[node name="Coin1" type="Node2D" parent="Coins"] +position = Vector2(360, 500) + +[node name="Visual" type="Polygon2D" parent="Coins/Coin1"] +polygon = PackedVector2Array(0, -16, 16, 0, 0, 16, -16, 0) +color = Color(0.95, 0.76, 0.31, 1) + +[node name="Coin2" type="Node2D" parent="Coins"] +position = Vector2(900, 410) + +[node name="Visual" type="Polygon2D" parent="Coins/Coin2"] +polygon = PackedVector2Array(0, -16, 16, 0, 0, 16, -16, 0) +color = Color(0.95, 0.76, 0.31, 1) + +[node name="Coin3" type="Node2D" parent="Coins"] +position = Vector2(1160, 620) + +[node name="Visual" type="Polygon2D" parent="Coins/Coin3"] +polygon = PackedVector2Array(0, -16, 16, 0, 0, 16, -16, 0) +color = Color(0.95, 0.76, 0.31, 1) + +[node name="Player" type="CharacterBody2D" parent="."] +position = Vector2(160, 560) +script = ExtResource("2_player") + +[node name="Shape" type="CollisionShape2D" parent="Player"] +shape = SubResource("RectangleShape2D_player") + +[node name="Visual" type="Polygon2D" parent="Player"] +polygon = PackedVector2Array(-20, -28, 20, -28, 20, 28, -20, 28) +color = Color(0.28, 0.55, 0.75, 1) + +[node name="Camera2D" type="Camera2D" parent="Player"] +position_smoothing_enabled = true + +[node name="Hud" type="CanvasLayer" parent="."] + +[node name="Status" type="Label" parent="Hud"] +offset_left = 40.0 +offset_top = 32.0 +offset_right = 1100.0 +offset_bottom = 130.0 +text = "已收集 0 / 3" +theme_override_font_sizes/font_size = 26 diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/main.gd b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/main.gd new file mode 100644 index 000000000..60c7593e0 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/main.gd @@ -0,0 +1,51 @@ +extends Node2D + +## AGC · 平台跳跃起步工程 +## +## 收集三枚金币:踩平台跳上去即可。金币按玩家中心与目标的距离判定, +## 不依赖物理层设置,改关卡时直接挪 Coin 节点位置就行。Esc 重来。 + +const PICKUP_RADIUS := 34.0 +const SPAWN_POSITION := Vector2(160.0, 560.0) + +var _score := 0 + +@onready var _player: CharacterBody2D = $Player +@onready var _coins: Node2D = $Coins +@onready var _status: Label = $Hud/Status + + +func _ready() -> void: + _update_status() + + +func _process(_delta: float) -> void: + for coin in _coins.get_children(): + if not coin.visible: + continue + if _player.global_position.distance_to(coin.global_position) <= PICKUP_RADIUS: + coin.visible = false + _score += 1 + _update_status() + + +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("ui_cancel"): + _restart() + + +func _restart() -> void: + for coin in _coins.get_children(): + coin.visible = true + _player.position = SPAWN_POSITION + _player.velocity = Vector2.ZERO + _score = 0 + _update_status() + + +func _update_status() -> void: + var total := _coins.get_child_count() + if _score >= total: + _status.text = "已收集 %d / %d —— 全部完成!Esc 重来" % [_score, total] + else: + _status.text = "已收集 %d / %d\n方向键移动 · 空格/回车跳跃 · Esc 重来" % [_score, total] diff --git a/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/player.gd b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/player.gd new file mode 100644 index 000000000..cc01695ff --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/godot-platformer-2d/project/scripts/player.gd @@ -0,0 +1,23 @@ +extends CharacterBody2D + +## AGC · 平台跳跃玩家控制 +## +## 方向键左右移动,空格 / 回车跳跃。只使用 Godot 内置的 ui_* 输入动作; +## 需要 WASD 或手柄时在「项目设置 → 输入映射」里给这两个动作补事件即可。 + +const SPEED := 320.0 +const JUMP_VELOCITY := -700.0 +const GRAVITY := 1500.0 + + +func _physics_process(delta: float) -> void: + if not is_on_floor(): + velocity.y += GRAVITY * delta + if is_on_floor() and Input.is_action_just_pressed("ui_accept"): + velocity.y = JUMP_VELOCITY + var direction := Input.get_axis("ui_left", "ui_right") + if direction != 0.0: + velocity.x = direction * SPEED + else: + velocity.x = move_toward(velocity.x, 0.0, SPEED) + move_and_slide() diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index c45bb7ce1..cdf6b4e73 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -1,6 +1,6 @@ # AI 游戏创作项目开发工作台 PRD -更新时间:`2026-09-21`(2026-09-20 音频生成并入图片类那份后台任务账本、派生/修改类任务并入同一「生成任务」侧栏,2026-09-21 卡片浮层改为「提交即关」、**「生成任务」侧栏从画布左侧贴边改为画布右上角锚点(开关常驻)**、**资源卡可拖到对话栏批量 @ 引用**、**替换面板改为非模态浮层并支持在画布上点选目标**(验收现场两条修正:任务锚点改挂画布那一格、展开后开关让位),见 §3.10 / §5.3 / §7.8 / §7.9 与 [`【功能说明】AGC聊天素材引用`](../【功能说明】AGC聊天素材引用-2026-09-08.md);2026-09-14 图片类生成后台化:入口 IPC 为 `start_local_project_asset_generation` + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板;2026-09-13 新增的功能画布底部工具栏入口矩阵 §3.10 / §7.9,以及右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变) +更新时间:`2026-09-21`(2026-09-20 音频生成并入图片类那份后台任务账本、派生/修改类任务并入同一「生成任务」侧栏,2026-09-21 卡片浮层改为「提交即关」、**「生成任务」侧栏从画布左侧贴边改为画布右上角锚点(开关常驻)**、**资源卡可拖到对话栏批量 @ 引用**、**替换面板改为非模态浮层并支持在画布上点选目标**(验收现场两条修正:任务锚点改挂画布那一格、展开后开关让位),见 §3.10 / §5.3 / §7.8 / §7.9 与 [`【功能说明】AGC聊天素材引用`](../【功能说明】AGC聊天素材引用-2026-09-08.md);2026-09-14 图片类生成后台化:入口 IPC 为 `start_local_project_asset_generation` + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板;2026-09-13 新增的功能画布底部工具栏入口矩阵 §3.10 / §7.9,以及右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变;2026-09-21 Godot 工作区发现放宽:一层多命中按目录名排序取第一个,`project.godot` 允许是链接/reparse point/硬链接,见 §3.8) ## 1. 产品定位 @@ -136,7 +136,7 @@ ### 3.8 现有 Godot 项目 -- 首页和项目组统一使用“打开项目”,不提供独立 Godot 按钮。用户选择的目录作为工作区根;系统先检查根目录,再检查一层直接子目录中的普通文件 `project.godot`。根目录命中优先;一层发现多个 Godot 工程时必须提示歧义,不猜测目标。 +- 首页和项目组统一使用“打开项目”,不提供独立 Godot 按钮。用户选择的目录作为工作区根;系统先检查根目录,再检查一层直接子目录中的 `project.godot`。根目录命中优先;一层命中多个 Godot 工程时按目录名排序取第一个,不再因为存在第二个工程而失败或要求用户改选。`project.godot` 本身是符号链接、Windows reparse point 或硬链接时按链接目标判定,解析后是文件即命中;目录与悬空链接仍不算命中。 - 工作区根始终绑定文件读取、修改、命令 cwd、对话、Runtime 和最近项目记录;`.agent/`、Agent DB 与日志也只写在这里。`project.godot` 所在目录额外以 `godotProjectRoot` 记录为相对工作区的 `.` 或单层目录名,不把项目作用域切到 Godot 子目录。 - 首次打开只在工作区根创建 `.agent/` 元数据;已有 `.agent/manifest.json` 时读取并按当前唯一文件布局校准 `godotProjectRoot`。Godot 源码、场景、资源和项目配置继续使用原目录结构,不复制工程,也不建立 `game/`、`assets/`、`memory/`、`exports/` 平行目录。 - Godot 项目使用 `standard` Run Profile,避免套用 Web 原型的 `game/index.html`、本地 HTTP 预览和自主 Web 游戏完成门;本期不提供 Godot 内嵌运行预览。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 490aca0bc..c5c8101dd 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -19,6 +19,23 @@ - 未迁移的历史对象:`agc/project-snapshots/v1/` 下现存对象(只读核对过的两份历史清单)保留在 OSS,但不再写入、不再进入后台列表;需要取回时按旧前缀在 OSS 侧直接读取,确实要在后台看到时再单独开一个只读兼容视图。 - 验证方式:`cargo test -p platform-oss snapshot` 7 passed(渠道校验、键布局、v2 根渠道枚举、v1 历史键仍必须私有);`cargo test -p api-server project_snapshot` 18 passed/1 ignored(渠道失败关闭、游标跨渠道拒绝、用户昵称/陶泥号解析、归档与配额回归);`cargo test -p api-server protected_route_matrix`、`route_contract` 通过(新路由纳入后台鉴权矩阵);`npm run admin-web:typecheck` 与 `npx vitest run apps/admin-web/src` 202 passed。 +## 2026-09-21 Godot 模板入库与按模板建项的 Godot 分流 + +- 背景:模板库此前只有网页(html)、Cocos 与 Unity 占位,`runtime` 白名单早就接受 `godot`,但既没有 Godot 模板,也没有按模板建项的 Godot 分流——Godot 模板即使上传,建项也会落到 Web 分支,写出 `game/index.html` 占位入口并让 `godotProjectRoot` 为空。 +- 决策(模板内容):新增四个仓库内手写的 Godot 4.7 模板 `godot-empty-2d`、`godot-empty-3d`、`godot-hello-world`、`godot-platformer-2d`,`entry` 统一为 `project.godot`。模板只用内置 `ui_*` 输入动作、GL Compatibility 渲染,并用 `Polygon2D` / `BoxMesh` 搭可视骨架,不引入外部贴图或音频二进制;不打包 `.godot/` 缓存与导出产物。 +- 决策(建项分流):`create_project_from_installed_template_at` 在复制模板后按工程文件分流——Cocos 更新自身身份后走 Cocos 导入,Godot 先改写 `project.godot` 里 `[application]` 段的 `config/name`(只改这一行,其余字节逐字保留)再走既有 Godot 导入,写入 `godotProjectRoot: "."`,其余继续走 `init_local_game_project_at`。分流靠工程文件识别,不新增只读 `entry` 或 `runtime` 字段的契约。 +- 原因:Godot 工程身份是 `project.godot` 所在目录,与 Cocos 的 `package.json` 身份同一类问题;复用既有导入流程能同时拿到相对根记录、`.agent` 初始化与「不生成 Web 占位入口」这三条既有保证,并且与 Cocos 分支保持对称。 +- 影响范围:`apps/ai-game-creator-shell/template-library/v1/godot-*`(新增模板源)、`apps/ai-game-creator-shell/src-tauri/src/template_library.rs`、`.../src/project/manifest.rs`(`apply_godot_project_display_name`)、`.../src/project/manifest/import_tests.rs`、`docs/【模板规范】AGC模板包组织指南-2026-09-21.md`、`docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md`。 +- 验证方式:本机 Godot `4.7.2.stable` 对四个模板逐一做「主场景实例化 + 3 帧 + GDScript `--check-only`」,平台跳跃模板另做真实物理试玩(落地 y=627.99、1 秒右移 320px、跳上平台 y=515.93、三枚金币全收集);Rust 定向回归 `import_tests::rewrites_only_the_godot_display_name_line`、`import_tests::keeps_a_godot_project_without_a_display_name_line_untouched`、`template_library::tests::godot_template_creates_native_project_with_relative_root_and_display_name` 与既有 Cocos/Web 建项回归;发布走 `--only godot-*` 定向合并。 + +## 2026-09-21 Godot 工作区发现放宽与内置插件行去掉手动启动 + +- 背景:2026-08-10 “发现与歧义决策”把「一层多命中」和「`project.godot` 必须是普通文件」两条定为失败关闭。这两种布局都会让整个工作区被判成「没有 Godot 工程」,而 `PluginHost::list` 只按发现结果过滤,结果是 Godot 内置插件在设置→扩展 里直接消失,用户看不到任何可诊断入口。 +- 决策(发现):根目录命中仍然优先;根未命中时一层直接子目录多命中改为按目录名排序取第一个,`godotProjectRoot` 记录该相对目录名,结果确定且可复现。`project.godot` 允许是符号链接 / Windows reparse point / 硬链接,判据改为「链接目标解析后是文件」,目录与悬空链接仍不算命中。工作区根本身是链接、候选子目录是链接、二层及更深不递归这三条边界不变。 +- 决策(界面):设置→扩展 的内置插件行去掉手动「启动 / 停止」按钮。启动本来就由项目切换时的 `startAvailableAgcEditorPlugins` 自动完成,手动按钮只是第二个可绕过入口;停止改走该行的启用/禁用开关,`set_agc_plugin_enabled(false)` 会停止插件并断开编辑器连接。导入扩展行的启动按钮保留,因为导入扩展没有自动启动路径。 +- 原因:Godot 插件列表是按项目过滤的,发现失败等于功能静默消失;把不确定性收敛成一个确定的排序选择,比让用户面对空列表更好。链接放宽只作用于只读的工程标记文件,受管描述文件、运行缓存与项目目录的链接拒绝规则不动。 +- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs`、`.../src/commands.rs`(错误文案)、`.../src/project/manifest/import_tests.rs`、`.../src/tests/project.rs`、`apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` §3.8、`docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 +- 验证方式:`cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- project::manifest::import_tests::` 与 `-- tests::project::`;前端 `agc:typecheck` 与 `runtime-settings` / `pluginHost` 套件。 ## 2026-09-21 模板正文目录门禁:CLI 打包与后台上传同一份段名单 @@ -8259,7 +8276,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-08-10 AGC 打开现有 Godot 项目 - 项目双根决策(2026-08-14 更新):项目组只保留通用“打开项目 / 新建项目”,不再提供独立 Godot 入口。用户选择目录始终是工作区根,也是 `.agent`、Session、Runner、沙箱、通用文件工具和外围资料的唯一授权根;实际 Godot 根由根目录或一层直接子目录中的普通文件 `project.godot` 唯一确定,并以工作区相对 `godotProjectRoot` 记录,根目录使用 `.`。不得把 Runtime 根切换成 Godot 子目录,也不得复制工程或建立第二套工作区。 -- 发现与歧义决策:根目录命中优先;根未命中时只检查一层直接子目录,唯一命中才通过,多个命中在任何 `.agent` 写入前失败关闭。候选目录与工程文件拒绝符号链接和 Windows reparse point,二层及更深不递归。未来只有 Godot 专属命令显式使用经过校验的相对 Godot cwd。 +- 发现与歧义决策:根目录命中优先;根未命中时只检查一层直接子目录,唯一命中才通过,多个命中在任何 `.agent` 写入前失败关闭。候选目录与工程文件拒绝符号链接和 Windows reparse point,二层及更深不递归。未来只有 Godot 专属命令显式使用经过校验的相对 Godot cwd。(2026-09-21 部分取代:多命中改为按目录名排序取第一个,`project.godot` 允许链接并按目标判定;候选子目录与更深层的边界不变,见本文件「2026-09-21 Godot 工作区发现放宽与内置插件行去掉手动启动」。) - 元数据决策:首次导入只在工作区根创建并保留 `.agent/manifest.json`、`.agent/agent.db`、`.agent/logs/` 与 `.agent/runtime/`;不得创建默认 Web 原型的 `game/`、`assets/`、`memory/`、`exports/`。已有有效 `.agent` 项目继续复用身份;缺失或错误的可推导 `godotProjectRoot` 只在 Godot 打开边界按唯一文件布局校准,歧义时不改写。 - Windows 锁文件决策:提升权限进程新建 `.agent/.manifest.json.lock` 时,Windows 可能把 owner 设为 `Administrators`。仅在固定锁路径已取得不共享独占句柄并确认是普通、非 reparse、单链接文件后,才初始化为当前 `TokenUser`;随后再次复核句柄并执行原有 owner/DACL 校验,不放宽既有异常对象的安全规则。 - 运行决策:Godot 项目提交给 Project Supervisor 时使用 `standard` Run Profile,避免触发 Web 专用 `game/index.html`、HTTP preview 与自主 Web 完成门。Godot 编辑器启动和内嵌运行预览不在本切片范围。 diff --git a/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md b/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md index b33697696..5e3dbf114 100644 --- a/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md +++ b/docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md @@ -3,7 +3,9 @@ > 文档状态:`current` > 规范关系:承接 AGC 通用插件宿主与编辑器适配主规范 -更新时间:`2026-09-20` +更新时间:`2026-09-21` + +2026-09-21 更新:解除工作区发现上的两条限制——一层子目录多命中时按目录名排序取第一个(不再整体失败关闭),`project.godot` 允许是符号链接 / Windows reparse point / 硬链接(按链接目标判定);同时设置→扩展 的内置插件行去掉手动「启动 / 停止」,启动统一由前端自动完成、停止改走该行启用开关。 ## 目标与边界 @@ -15,7 +17,7 @@ 将 Godot 编辑器操控接入现有 AGC PluginHost、EditorAdapter、Runner、内置插件开关、权限审计和 Agent 工具链。Windows x64 的 Godot 4.7 及以上标准编辑器是首个实现目标,实机验收使用 4.7.2;其他平台和 .NET 编辑器不得从该结果推断支持。 -初版工程路径支持 Windows 本地盘符目录;UNC/网络共享路径在准备描述文件前明确拒绝。链接/reparse point 继续按同一文件边界失败关闭。 +初版工程路径支持 Windows 本地盘符目录;UNC/网络共享路径在准备描述文件前明确拒绝。受管描述文件、缓存与运行缓存继续按同一文件边界对链接/reparse point 失败关闭;唯一例外是工作区发现读取的 `project.godot`,它允许是链接并按目标判定。 DLL 原件随 AGC 安装包放在插件资源目录中;Godot Windows 加载器会在被加载文件旁生成 `~DLL`,因此宿主在 AGC 私有配置目录的运行缓存中按编辑器实例和构建身份准备临时加载副本。AGC 向项目新增一个可扫描的受管 `.gdextension` 描述文件,通过绝对路径引用该实例的加载副本;Godot 可自动生成其同名 `.uid` 伴生文件。DLL 不复制进工程。重新聚焦 Godot 后,由官方文件扫描完成首次加载;不需要用户打开或运行脚本,不创建 EditorPlugin addon,不修改 project.godot 或业务场景文件。编译工具链只属于开发与打包环境,不要求终端用户安装编译器。 @@ -24,9 +26,9 @@ DLL 原件随 AGC 安装包放在插件资源目录中;Godot Windows 加载器 ## 入口与归属 - 插件 id 为 `agc-godot-editor`,适配器为 `godot-editor`;命令 `godot.editor.execute`、连接能力 `godot.editor.connection`,DirectProject 工具为 `agc_godot_execute`。复用已有扩展列表和启用开关,不建立平行插件管理页面。 -- 项目发现沿用现有 Godot 工作区合同:工作区根保持用户选定目录;实际 Godot 根由普通 project.godot 在根或唯一一层子目录中确定。准备描述文件和读取 Godot 缓存只作用于实际 Godot 根,通用文件工具/Runtime 的工作区根不改变。 +- 项目发现沿用现有 Godot 工作区合同:工作区根保持用户选定目录;实际 Godot 根由根目录或一层直接子目录中的 project.godot 确定,一层命中多个时按目录名排序取第一个(确定性,不再报歧义),`project.godot` 本身是链接时按链接目标判定。准备描述文件和读取 Godot 缓存只作用于实际 Godot 根,通用文件工具/Runtime 的工作区根不改变。 - 平台、内置开关、项目及目标身份必须在执行入口重新检查。插件只能处理宿主传入的当前受控项目,模型不能覆盖项目路径、DLL 路径、端口、令牌或目标实例。 -- Godot 的插件列表、启动和 Agent 工具目录继续按当前 Godot 项目过滤;Cocos/Unity 沿用各自不按工程类型过滤的合同。前端统一消费宿主列表自动启动三种编辑器插件,不在界面重复推断项目类型。Godot 的项目切换仍撤销旧插件上下文并停止旧实例。 +- Godot 的插件列表、启动和 Agent 工具目录继续按当前 Godot 项目过滤;Cocos/Unity 沿用各自不按工程类型过滤的合同。前端统一消费宿主列表自动启动三种编辑器插件,不在界面重复推断项目类型;设置→扩展 的内置插件行只保留启用/禁用与重载,不再提供手动启动/停止入口。Godot 的项目切换仍撤销旧插件上下文并停止旧实例。 - 插件启动先完成项目事件订阅并接收当前受控项目快照,再注册命令和连接能力;命令可见时必须已经具备执行上下文。订阅期间收到的新项目事件优先于迟到的初始快照。 - 只连接已打开且唯一匹配真实工程路径的 Godot Editor;校验 PID、进程启动身份、Godot 版本、握手中的工程路径与会话代次。多个候选、非编辑器、路径不符或已退出的进程均拒绝,不启动或关闭用户编辑器。 - GUI、DirectProject 和 Agent Runtime 的原生操作统一由长寿命 Runner 持有。项目切换使连接失效,迟到回执不能改变新项目状态。 diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md index c320b6b0f..90aa75885 100644 --- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md +++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md @@ -86,9 +86,11 @@ templates/ - 只释放本任务已明确获取且 owner 标识仍一致的锁。获取结果不明时不猜测删除。正文对象不可变,其写入失败可安全释放本任务的锁;清单 PUT 已发起后若遇到断连、超时或服务端 5xx 等不确定结果,必须保留锁并报错,防止旧在途请求晚于下一发布者写入。清单收到确定成功或确定拒绝响应后才进入正常解锁路径;不自动重发清单写入,也不凭一次 GET 猜测在途 PUT 已结束。遗留锁须在确认原请求及进程已终止或完成后由运维处理;释放失败必须报告,不伪装为发布成功。 - `--dry-run` 仅构造和读取合并计划,不读取凭据、不获取锁、不 PUT/DELETE。发布不删除历史对象,也不提供随发布清理的选项;旧客户端缓存和未完成下载可能仍引用旧键。 - 同一 ID、同一 `templateVersion` 的 ZIP 大小或摘要改变时,在上传正文前拒绝,要求更新模板版本;只把相同 ZIP 迁移到新键可保留版本。客户端现有 `/` 缓存行为不变。 -- 当前模板:`blank-web`(空白网页)、`blank-2d-canvas`(空白二维画布)、`blank-3d-scene`(空白三维场景)、`phaser-2d-starter`(Phaser 2D 起步工程)、`threejs-3d-starter`(Three.js 3D 起步工程),以及 Cocos Creator 3.8.8 的 `cocos-empty-2d`、`cocos-empty-3d`、`cocos-empty-3d-hq`、`cocos-hello-world`。 +- 当前模板:`blank-web`(空白网页)、`blank-2d-canvas`(空白二维画布)、`blank-3d-scene`(空白三维场景)、`phaser-2d-starter`(Phaser 2D 起步工程)、`threejs-3d-starter`(Three.js 3D 起步工程)、Cocos Creator 3.8.8 的 `cocos-empty-2d`、`cocos-empty-3d`、`cocos-empty-3d-hq`、`cocos-hello-world`,以及 Godot 4.7 的 `godot-empty-2d`、`godot-empty-3d`、`godot-hello-world`、`godot-platformer-2d`。 - Cocos 内容来自 Creator 3.8.8 随附的 `resources/templates/{empty-2d,empty,empty-quality,hello-3d-world}`,保留官方资源、`.meta`、设置和模板预设;补齐 `package.json.creator.version`,空模板以 `assets/.gitkeep` 保证资源目录进入 Git 和 ZIP。`entry` 为 `package.json`,不打包编辑器生成的缓存或用户项目数据。 - Cocos 建项在复制后按实际 `package.json.creator.version + assets/` 识别,复用既有 Cocos 导入流程,写入 `cocosProjectRoot: "."`;每个新项目重建 `package.json.uuid` 并写入所选项目名。只创建 `.agent` 管理目录,不生成 Web 占位入口;模板源与本机安装缓存不被改写。 +- Godot 内容为仓库内手写的 Godot 4.7 工程(`project.godot` + `scenes/` + `scripts/`,GL Compatibility 渲染,只用内置 `ui_*` 输入动作,不依赖外部贴图),`entry` 为 `project.godot`,不打包 `.godot/` 编辑器缓存与导出产物。 +- Godot 建项在复制后按 `project.godot` 识别,复用既有 Godot 导入流程,写入 `godotProjectRoot: "."`,并把工程显示名改写成用户选择的项目名(只改 `[application]` 段的 `config/name` 一行,其余字节逐字保留);不生成 `game/` 占位入口与 `assets/`、`memory/`、`exports/` 并行目录,也不改写模板源与本机安装缓存。 - 客户端可用 `AGC_TEMPLATE_LIBRARY_BASE_URL` 覆盖库地址;只接受 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com`(拒绝其他主机、路径、http)。 ## 客户端实现 @@ -99,7 +101,7 @@ templates/ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fetch_game_template_library` | 读 `templates/index.json`(≤4 MiB),校验后缓存到 `/templates/index.json`;网络失败时回退本机缓存并在 `source` 标 `cache`。远端已经答话但正文不是合法 UTF-8 或不符合 schema、以及缓存自己损坏时,一律失败关闭,不用缓存掩盖远端错误 | | `download_game_template` | 取清单里对应条目,流式下载 zip(≤512 MiB),校验字节数与 SHA-256,解压到 `/templates/installed///`,最后写 `installed.json` 作为安装完成的唯一标记 | -| `create_automatic_local_game_project_from_template` | 需要时先安装模板,然后在自动工作区根目录下按既有自动工作区规则建目录:先复制模板文件;Cocos 项目更新自身身份后走既有 Cocos 导入,其余沿现有 `init_local_game_project_at` 初始化。根目录默认是 `/projects/`,用户可选 `projectsRoot` 覆盖(必须来自本机目录选择器并通过私有路径门禁),见 [`【实施计划】AGC项目创建目录可选-2026-09-17.md`](../project-memory/plans/【实施计划】AGC项目创建目录可选-2026-09-17.md) | +| `create_automatic_local_game_project_from_template` | 需要时先安装模板,然后在自动工作区根目录下按既有自动工作区规则建目录:先复制模板文件;Cocos 项目更新自身身份后走既有 Cocos 导入,Godot 项目改写工程显示名后走既有 Godot 导入,其余沿现有 `init_local_game_project_at` 初始化。根目录默认是 `/projects/`,用户可选 `projectsRoot` 覆盖(必须来自本机目录选择器并通过私有路径门禁),见 [`【实施计划】AGC项目创建目录可选-2026-09-17.md`](../project-memory/plans/【实施计划】AGC项目创建目录可选-2026-09-17.md) | 安全与健壮性: @@ -135,6 +137,8 @@ Cocos 回归分别覆盖仓库模板和线上真实 ZIP 的安装、连续建项 `2026-09-21` 模板正文目录门禁:CLI 打包(`readProjectFiles`)与后台上传(`validate_import_archive`)统一拒绝正文含 `.agent` / `.git` / `.svn` / `node_modules` 段(任意层级)与根目录 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode` 的模板,两处用同一份段名单与同一句文案。同名目录段只在根目录受限:正文内 `game/dist/**` 与 `.gitignore` 仍合法(`.gitignore` 不等于 `.git`),Cocos 模板的 `.creator` / `.gitignore` 不受影响。证据:Node 发布回归 28 项通过(新增 1 项,13 条拒绝用例 + 2 条放行断言);Rust `admin_templates` 8 项回归通过(新增 `template_import_archive_rejects_identity_and_build_directories`);仓库现有 9 个模板源无一条命中门禁,本地重打包的 9 个 ZIP 摘要与线上 `index.json` 的 `zipSha256` 逐条一致(匿名读清单 200、9 个模板、0 个下架条目),即门禁未改变任何已发布字节。未执行真实 OSS 写入。 +`2026-09-21` Godot 模板入库:新增 `godot-empty-2d` / `godot-empty-3d` / `godot-hello-world` / `godot-platformer-2d` 四个仓库内手写模板,并给建项补 Godot 分流。模板内容用本机 Godot `4.7.2.stable` 逐项验证:四个模板的主场景都能实例化并跑满 3 帧、全部 GDScript 过 `--check-only`(同一条命令对故意写错的脚本报告 Parse Error,证明检查有效);平台跳跃模板另做真机物理试玩——角色落地 y=627.99(地面顶 656 − 半高 28)、按住右 1 秒位移 320px、连按跳跃后落在平台上 y=515.93(平台顶 544 − 28)、三枚金币全部收集后 HUD 变为「已收集 3 / 3 —— 全部完成!Esc 重来」。Rust 侧 4 项定向回归通过:`template_library::tests::godot_template_creates_native_project_with_relative_root_and_display_name`(Godot 模板建项得到 `godotProjectRoot: "."`、无 `game/` 占位入口、`config/name` 改写成所选名称且其余行逐字保留)、`import_tests::rewrites_only_the_godot_display_name_line`、`import_tests::keeps_a_godot_project_without_a_display_name_line_untouched`,以及既有 Cocos/Web 建项回归未受影响。发布保持 `--only` 定向:只新增四个 Godot 模板对象并重写清单,其余 9 个模板的键与版本不变;未在客户端「模板库」里实际建一次项目。 + ## 本地压测假数据注入(feature 控制) 模板库的数据源在 Rust 侧(清单校验、安装状态、下载与建项目都在这里),TS 只消费快照做渲染,所以假数据注入也放在 Rust 侧,走与真实完全一致的链路。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 671f17ad6..30b18702f 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1,5 +1,16 @@ # AI 游戏创作智能体 App 实施计划 +## 2026-09-21 Godot 工作区发现放宽与内置插件行去掉手动启动 + +本节覆盖下文“打开项目自动识别 Godot”中的旧口径:判定从「唯一命中」放宽为「确定性命中」,`project.godot` 从「必须是普通文件」放宽为「按链接目标判定」。 + +| 要求 | 必须成立的行为 | 完成证据 | +| --- | --- | --- | +| 确定性多命中 | 根目录未命中时,一层直接子目录中多个 `project.godot` 命中按目录名排序取第一个;不再报「多个 Godot 项目」,也不再要求用户改选具体工程 | `cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- project::manifest::import_tests::` 与 `-- tests::project::` | +| 链接标记 | `project.godot` 是符号链接 / Windows reparse point / 硬链接时按链接目标判定,目标解析为普通文件即命中;目录与悬空链接仍不算命中 | `import_tests::accepts_symbolic_link_project_marker`(unix)、`import_tests::accepts_windows_hard_link_project_marker`、`import_tests::accepts_windows_reparse_project_marker` | +| 保持不变的边界 | 工作区根本身是链接、候选子目录是链接、二层及更深目录不递归,这三条既有边界不动 | `import_tests::ignores_symbolic_link_child_candidate_without_writing_agent_metadata`、`import_tests::ignores_windows_reparse_child_candidate_without_writing_agent_metadata`、`import_tests::ignores_godot_projects_below_the_first_child_level` | +| 内置插件行 | 设置→扩展 的内置插件行不再渲染手动「启动 / 停止」按钮;启动由项目切换时的前端自动启动承担,停止走该行启用开关(禁用即停止并断开编辑器连接);导入扩展行的启动按钮保留 | `apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx`、`tests/pluginHost.test.ts` | + ## 2026-09-20 DirectProject 七项效率闭环(补齐合同) 本节补齐并覆盖下节中仅靠 Skill 要求预检、收尾、批读和原生命令预算的部分。完整目标仍为:自动预检、宿主验收与收尾、分层验证、统一执行/返修预算、稳定测试基线、请求耗时与批量读取、所有工具并行。已有代码及测试不等于全部目标已完成;按下表逐项验收。 diff --git a/docs/【模板规范】AGC模板包组织指南-2026-09-21.md b/docs/【模板规范】AGC模板包组织指南-2026-09-21.md index 933e30080..f92bd0183 100644 --- a/docs/【模板规范】AGC模板包组织指南-2026-09-21.md +++ b/docs/【模板规范】AGC模板包组织指南-2026-09-21.md @@ -39,6 +39,16 @@ assets/… # 可选 - 建项时会在复制模板文件之后补齐 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`,并创建 `.agent/` 清单;这些目录不用在模板里手工占位。 - 模板没有 `game/index.html` 时,建项会写入一份默认入口占位页;所以 html 类模板应当自带 `game/index.html`,并把 `entry` 填成 `game/index.html`。根目录的 `index.html` 不会被当成游戏入口(建项仍会补默认 `game/index.html`),不要用这种结构。 +### Godot 项目(`runtime=godot`) + +- 根目录直接放 Godot 工程:`project.godot`(此时 `entry` 用 `project.godot`)、`scenes/`、`scripts/`、`icon.svg`、`.gitignore` 等,`.import` / `.uid` 之类由引擎生成的伴生文件不必手工准备。 +- `project.godot` 要能独立打开:`config_version=5`,且 `[application]` 段的 `run/main_scene` 指向工程内真实场景。建项会把 `config/name` 改写成用户选择的项目名(只改这一行),模板里的名字只是初始值。 +- 目标版本是 Godot 4.7:`config/features=PackedStringArray("4.7", "GL Compatibility")` 配套 `renderer/rendering_method="gl_compatibility"` 兼容面最大,2D 与轻量 3D 模板都用这一套。 +- 建项按 `project.godot` 识别 Godot 模板,写入 `godotProjectRoot: "."`,不生成 `game/` 占位入口与 `assets/`、`memory/`、`exports/` 并行目录。 +- 不要打包 `.godot/` 编辑器与导入缓存、导出产物(`export/`、`build/`);模板正文保持「克隆下来直接就能打开」的源状态。 +- 输入优先只用 Godot 内置的 `ui_*` 动作;需要自定义动作时把 `[input]` 段写进 `project.godot`,不要让模板依赖开发者本机的输入映射。 +- 建议不引入外部贴图与音频,用 `Polygon2D`、`ColorRect`、`BoxMesh` 一类节点搭出可见骨架,避免模板源里出现难以复核的二进制素材。 + ### Cocos Creator 项目(`runtime=cocos`) - 根目录直接放 Creator 工程:`package.json`(此时 `entry` 用 `package.json`)、`assets/`、`settings/`、`profiles/`、`.creator/`、`tsconfig.json`、`.gitignore` 等官方结构**原样保留**,`.meta` 与导入设置必须一起带上,否则导入后资源关系会丢。 @@ -94,7 +104,7 @@ assets/… # 可选 ## 发布前自检 -1. 核对 ZIP 结构:`unzip -l your-template.zip`,确认没有外层目录、`entry` 存在、没有 `.agent/`、`.git/`、`node_modules/`、`dist/`。 +1. 核对 ZIP 结构:`unzip -l your-template.zip`,确认没有外层目录、`entry` 存在、没有 `.agent/`、`.git/`、`node_modules/`、`dist/`(Godot 模板另需确认没有 `.godot/`)。 2. 核对体积与条目数(见上表)。 3. CLI 路径:`node scripts/agc-template-library-publish.mjs --source --dry-run` 查看合并计划与摘要,确认后再去掉 `--dry-run`。 4. 后台路径:上传页逐行核对 ID / 名称 / 版本 / 运行时 / entry,确认封面已按 ID 匹配。