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
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>
286 lines
11 KiB
Rust
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));
|
|
}
|
|
}
|
|
}
|