Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs
T
lhk229 ec3a187dd7
Project CI / AI game creator shell Rust crates (push) Successful in 1m11s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m34s
Project CI / Backend tests (push) Successful in 4m46s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m31s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m41s
Project CI / Frontend tests (push) Successful in 2m19s
Project CI / Native shell tests (push) Successful in 7m5s
Project CI / AI game creator shell web tests (push) Successful in 1m54s
Project CI / Repository checks (push) Successful in 2m36s
处理编译大量warning问题 (#503)
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/503
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-24 13:01:31 +08:00

286 lines
11 KiB
Rust

//! 已链接的编辑器适配器目录。
//!
//! 编辑器专属实现随 `plugins/` 工作区里的插件包分发;其中 native 适配器模块
//! 目前由宿主在编译期链接(Cargo path 依赖),再按插件 manifest 的 `adapter`
//! 字段注册到通用插件宿主。宿主只认适配器 id,不包含目标编辑器知识。
#[cfg(all(windows, feature = "cocos-editor-execute"))]
use std::path::PathBuf;
#[cfg(all(windows, feature = "cocos-editor-execute"))]
use tauri::Manager;
use crate::plugin_host::PluginHost;
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
use editor_adapter_api::{EditorAdapter, EditorConnectionInfo};
#[cfg(any(
test,
all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
)
))]
use serde_json::json;
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
use serde_json::Value;
use std::path::Path;
mod execution;
pub(crate) use execution::*;
/// GUI 只转发已有 Runner RPC;每个引擎的连接和回执均归同一个 owner。
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
struct RunnerManagedEditorAdapter(ManagedEditor);
#[cfg(all(
windows,
target_arch = "x86_64",
any(feature = "unity-editor-execute", feature = "godot-editor-execute")
))]
impl EditorAdapter for RunnerManagedEditorAdapter {
fn id(&self) -> &'static str {
self.0.adapter()
}
fn detect(&self, project_path: &Path) -> Result<EditorConnectionInfo, String> {
serde_json::from_value(managed_editor_rpc(
self.0,
"detect",
json!({"projectPath":project_path.to_string_lossy()}),
)?)
.map_err(|_| "编辑器探测回执格式无效".to_string())
}
fn connect(
&mut self,
pid: u32,
project_path: &Path,
_version: &str,
) -> Result<EditorConnectionInfo, String> {
serde_json::from_value(managed_editor_rpc(
self.0,
"connect",
json!({"processId":pid,"projectPath":project_path.to_string_lossy()}),
)?)
.map_err(|_| "编辑器连接回执格式无效".to_string())
}
fn disconnect(&mut self) {
let _ = disconnect_managed_editor_connection(self.0);
}
fn translate_rpc(&self, method: &str, params: Value) -> Result<Value, String> {
match self.0 {
ManagedEditor::Unity => unity_editor_bridge::UnityEditorAdapter::new(Vec::new())
.translate_rpc(method, params),
ManagedEditor::Godot => godot_editor_bridge::GodotEditorAdapter::new(Vec::new())
.translate_rpc(method, params),
}
}
fn rpc(&self, method: &str, params: Value) -> Result<Value, String> {
managed_editor_rpc(self.0, method, params)
}
}
/// 随包 payload 相对资源根目录的位置,与 `tauri.windows.conf.json` 的资源映射保持一致。
#[allow(dead_code)]
pub(crate) const COCOS_BRIDGE_PAYLOAD_RELATIVE: &str =
"plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll";
#[allow(dead_code)]
pub(crate) const UNITY_ATTACH_HELPER_RELATIVE: &str =
"plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe";
/// Runner 没有 GUI setup,仍只从随包位置或编译期工作区取受控 helper。
pub(crate) fn configure_unity_helper_for_runtime() -> Result<(), String> {
let mut candidates = Vec::new();
if let Ok(executable) = std::env::current_exe() {
if let Some(directory) = executable.parent() {
candidates.push(directory.join(UNITY_ATTACH_HELPER_RELATIVE));
}
}
#[cfg(debug_assertions)]
candidates.push(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(3)
.ok_or("插件工作区目录不可用")?
.join(UNITY_ATTACH_HELPER_RELATIVE),
);
unity_editor_bridge::configure_helper_candidates(candidates)
}
pub(crate) const GODOT_BRIDGE_PAYLOAD_RELATIVE: &str =
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll";
/// 安装包与开发构建使用同一插件资源布局,不将 DLL 复制进 Godot 工程。
pub(crate) fn configure_godot_payload_for_runtime(config_dir: &Path) -> Result<(), String> {
godot_editor_bridge::configure_runtime_cache_dir(config_dir.join("godot-editor-runtime"))?;
let mut candidates = Vec::new();
if let Ok(executable) = std::env::current_exe() {
if let Some(directory) = executable.parent() {
candidates.push(directory.join(GODOT_BRIDGE_PAYLOAD_RELATIVE));
}
}
#[cfg(debug_assertions)]
candidates.push(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(3)
.ok_or("插件工作区目录不可用")?
.join(GODOT_BRIDGE_PAYLOAD_RELATIVE),
);
godot_editor_bridge::configure_payload_candidates(candidates)
}
pub(crate) fn register_linked_editor_adapters(
app: &tauri::AppHandle,
host: &PluginHost,
) -> Result<(), String> {
#[cfg(all(windows, feature = "cocos-editor-execute"))]
{
let adapter =
cocos_editor_bridge::CocosEditorAdapter::new(cocos_bridge_payload_candidates(app));
host.register_editor_adapter(Box::new(adapter))?;
}
#[cfg(not(all(windows, feature = "cocos-editor-execute")))]
{
let _ = (app, host);
}
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
{
host.register_editor_adapter(Box::new(RunnerManagedEditorAdapter(ManagedEditor::Unity)))?;
}
#[cfg(all(windows, target_arch = "x86_64", feature = "godot-editor-execute"))]
{
host.register_editor_adapter(Box::new(RunnerManagedEditorAdapter(ManagedEditor::Godot)))?;
}
Ok(())
}
#[cfg(all(windows, feature = "cocos-editor-execute"))]
fn cocos_bridge_payload_candidates(app: &tauri::AppHandle) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Ok(resource_dir) = app.path().resource_dir() {
candidates.push(resource_dir.join(COCOS_BRIDGE_PAYLOAD_RELATIVE));
}
// 开发构建还要能直接从 plugins/ 工作区读取尚未打包的 payload。
#[cfg(debug_assertions)]
{
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
candidates.push(
manifest_dir
.join("../../../plugins/agc-cocos-editor/native/payload")
.join("cocos-editor-bridge.dll"),
);
}
candidates
}
#[cfg(test)]
mod unity_receipt_tests {
use super::*;
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
#[test]
fn unity_delivery_ack_distinguishes_busy_disabled_and_known_pre_dispatch_failure() {
let _guard = crate::builtin_plugins::test_lock();
let config = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
for directory in ["Assets", "Packages", "ProjectSettings"] {
std::fs::create_dir(project.path().join(directory)).unwrap();
}
std::fs::write(
project.path().join("ProjectSettings/ProjectVersion.txt"),
"m_EditorVersion: 6000.0.1f1",
)
.unwrap();
let previous_config = crate::game_creator_runtime_config_dir_lock()
.lock()
.unwrap()
.clone();
*crate::game_creator_runtime_config_dir_lock()
.lock()
.unwrap() = Some(config.path().to_path_buf());
crate::builtin_plugins::initialize(config.path()).unwrap();
let params = json!({"projectPath":project.path().to_string_lossy(),"code":"return 2;"});
{
let _busy = unity_pending_delivery().lock().unwrap();
let result = managed_editor_rpc_owned(
ManagedEditor::Unity,
"execute",
params.clone(),
Some("busy-request"),
)
.unwrap();
assert_eq!(result["status"], "failed");
assert_eq!(result["dispatched"], false);
assert!(!unity_delivery_requires_ack("busy-request"));
}
crate::builtin_plugins::set_enabled(
crate::builtin_plugins::AGC_UNITY_EDITOR_PLUGIN_ID,
false,
)
.unwrap();
assert!(managed_editor_rpc_owned(
ManagedEditor::Unity,
"execute",
params.clone(),
Some("disabled-request")
)
.is_err());
assert!(!unity_delivery_requires_ack("disabled-request"));
crate::builtin_plugins::set_enabled(
crate::builtin_plugins::AGC_UNITY_EDITOR_PLUGIN_ID,
true,
)
.unwrap();
// 空代码在 native 发送前失败,但本次 Runner delivery 已建 fence,仍须确认回执。
let result = managed_editor_rpc_owned(
ManagedEditor::Unity,
"execute",
json!({"projectPath":project.path().to_string_lossy(),"code":""}),
Some("known-failure"),
)
.unwrap();
assert_eq!(result["status"], "failed");
assert_eq!(result["dispatched"], false);
assert_eq!(result["ackRequired"], true);
assert!(unity_delivery_requires_ack("known-failure"));
assert!(acknowledge_editor_delivery(ManagedEditor::Unity, "wrong-id").is_err());
assert!(editor_execution_fence_path(ManagedEditor::Unity, config.path()).exists());
acknowledge_editor_delivery(ManagedEditor::Unity, "known-failure").unwrap();
assert!(!editor_execution_fence_path(ManagedEditor::Unity, config.path()).exists());
*crate::game_creator_runtime_config_dir_lock()
.lock()
.unwrap() = previous_config;
}
#[test]
fn unity_ack_requires_complete_consistent_execution_receipt() {
assert!(editor_execute_receipt_is_valid(
&json!({"status":"completed","ok":true,"dispatched":true,"retryAllowed":false,"result":null})
));
assert!(editor_execute_receipt_is_valid(
&json!({"status":"failed","ok":false,"dispatched":false,"retryAllowed":false,"error":{"code":"missing-helper","message":"no helper"}})
));
for value in [
json!({"status":"completed","ok":true}),
json!({"status":"completed","ok":false,"dispatched":true,"retryAllowed":false,"result":1}),
json!({"status":"failed","ok":false,"dispatched":true,"retryAllowed":false}),
json!({"status":"needs-reconciliation","ok":false,"dispatched":false,"retryAllowed":false,"error":"lost"}),
] {
assert!(!editor_execute_receipt_is_valid(&value));
}
}
}