5822b64d7c
接入 Godot 原生桥、受控执行、Runner 回执与编辑器操作指南 保留主分支 Cocos 和 Unity 跨工程能力及外置提示词结构 解决插件生命周期、工具目录、前端启动和文档合并冲突
559 lines
20 KiB
Rust
559 lines
20 KiB
Rust
//! 内置插件登记表与可用开关。
|
|
//!
|
|
//! 内置插件随 `plugins/` 工作区随包分发,用户不能卸载,只能控制是否可用。
|
|
//! 开关状态持久化在 AppData `extensions/builtin-plugins.json`,同时被两处消费:
|
|
//! 插件宿主(禁用后不能启动,状态显示为 disabled)和 Agent 工具目录(禁用后
|
|
//! 不出现在工具列表与 Agent 上下文里)。
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Cocos Creator 编辑器插件的插件 id,与 `plugins/agc-cocos-editor/plugin.json` 一致。
|
|
pub(crate) const AGC_COCOS_EDITOR_PLUGIN_ID: &str = "agc-cocos-editor";
|
|
|
|
/// 该插件在 Agent 侧对应的 Runtime 工具名。
|
|
pub(crate) const AGC_COCOS_EDITOR_TOOL_NAME: &str = "cocos.editor.execute";
|
|
pub(crate) const AGC_UNITY_EDITOR_PLUGIN_ID: &str = "agc-unity-editor";
|
|
pub(crate) const AGC_UNITY_EDITOR_TOOL_NAME: &str = "unity.editor.execute";
|
|
pub(crate) const AGC_GODOT_EDITOR_PLUGIN_ID: &str = "agc-godot-editor";
|
|
pub(crate) const AGC_GODOT_EDITOR_TOOL_NAME: &str = "godot.editor.execute";
|
|
|
|
const STATE_FILE_NAME: &str = "builtin-plugins.json";
|
|
const STATE_SCHEMA_VERSION: &str = "agc.builtin-plugins.v1";
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) enum BuiltinPlugin {
|
|
CocosEditor,
|
|
UnityEditor,
|
|
GodotEditor,
|
|
}
|
|
|
|
impl BuiltinPlugin {
|
|
pub(crate) fn id(self) -> &'static str {
|
|
match self {
|
|
Self::CocosEditor => AGC_COCOS_EDITOR_PLUGIN_ID,
|
|
Self::UnityEditor => AGC_UNITY_EDITOR_PLUGIN_ID,
|
|
Self::GodotEditor => AGC_GODOT_EDITOR_PLUGIN_ID,
|
|
}
|
|
}
|
|
|
|
/// 未持久化任何开关时的默认状态。
|
|
fn default_enabled(self) -> bool {
|
|
match self {
|
|
Self::CocosEditor | Self::UnityEditor | Self::GodotEditor => true,
|
|
}
|
|
}
|
|
|
|
/// 该插件是否向 Agent 暴露 Runtime 工具。
|
|
fn exposes_agent_tools(self) -> bool {
|
|
match self {
|
|
Self::CocosEditor | Self::UnityEditor | Self::GodotEditor => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) const BUILTIN_PLUGINS: &[BuiltinPlugin] = &[
|
|
BuiltinPlugin::CocosEditor,
|
|
BuiltinPlugin::UnityEditor,
|
|
BuiltinPlugin::GodotEditor,
|
|
];
|
|
|
|
pub(crate) fn builtin_plugin(id: &str) -> Option<BuiltinPlugin> {
|
|
BUILTIN_PLUGINS
|
|
.iter()
|
|
.copied()
|
|
.find(|plugin| plugin.id() == id.trim())
|
|
}
|
|
|
|
pub(crate) fn is_builtin(id: &str) -> bool {
|
|
builtin_plugin(id).is_some()
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct BuiltinPluginState {
|
|
path: Option<PathBuf>,
|
|
enabled: BTreeMap<String, bool>,
|
|
/// 开关文件不可读或格式不受支持时,内置插件全部按不可用处理。
|
|
fail_closed: bool,
|
|
}
|
|
|
|
static STATE: OnceLock<Mutex<BuiltinPluginState>> = OnceLock::new();
|
|
|
|
fn state() -> &'static Mutex<BuiltinPluginState> {
|
|
STATE.get_or_init(|| Mutex::new(BuiltinPluginState::default()))
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct BuiltinPluginStateFile {
|
|
#[serde(default)]
|
|
schema_version: Option<String>,
|
|
#[serde(default)]
|
|
enabled: BTreeMap<String, bool>,
|
|
}
|
|
|
|
/// 读取 AppData 里的开关状态;文件缺失按默认状态处理,坏文件失败关闭。
|
|
pub(crate) fn initialize(config_dir: &Path) -> Result<(), String> {
|
|
let root = config_dir.join("extensions");
|
|
let path = root.join(STATE_FILE_NAME);
|
|
if let Err(error) = fs::create_dir_all(&root) {
|
|
mark_fail_closed(Some(path));
|
|
return Err(format!("准备内置插件目录失败:{error}"));
|
|
}
|
|
let mut guard = state()
|
|
.lock()
|
|
.map_err(|_| "内置插件状态锁已损坏".to_string())?;
|
|
guard.path = Some(path.clone());
|
|
reload_state(&mut guard, &path)
|
|
}
|
|
|
|
fn read_state_file(path: &Path) -> Result<BuiltinPluginStateFile, String> {
|
|
match fs::read(path) {
|
|
Ok(bytes) => match serde_json::from_slice::<BuiltinPluginStateFile>(&bytes) {
|
|
Ok(file) if file.schema_version.as_deref() == Some(STATE_SCHEMA_VERSION) => Ok(file),
|
|
Ok(_) => Err(format!(
|
|
"内置插件开关文件版本不受支持,需要 {STATE_SCHEMA_VERSION}"
|
|
)),
|
|
Err(error) => Err(format!("内置插件开关文件无效:{error}")),
|
|
},
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
Ok(BuiltinPluginStateFile::default())
|
|
}
|
|
Err(error) => Err(format!("读取内置插件开关失败:{error}")),
|
|
}
|
|
}
|
|
|
|
fn reload_state(guard: &mut BuiltinPluginState, path: &Path) -> Result<(), String> {
|
|
let loaded = match read_state_file(path) {
|
|
Ok(loaded) => loaded,
|
|
Err(error) => {
|
|
guard.fail_closed = true;
|
|
guard.enabled = BUILTIN_PLUGINS
|
|
.iter()
|
|
.map(|plugin| (plugin.id().to_string(), false))
|
|
.collect();
|
|
return Err(error);
|
|
}
|
|
};
|
|
guard.fail_closed = false;
|
|
// 只接受登记表里的 id,避免坏文件把未知对象带进运行时。
|
|
guard.enabled = loaded
|
|
.enabled
|
|
.into_iter()
|
|
.filter(|(id, _)| is_builtin(id))
|
|
.collect();
|
|
Ok(())
|
|
}
|
|
|
|
fn mark_fail_closed(path: Option<PathBuf>) {
|
|
if let Ok(mut guard) = state().lock() {
|
|
guard.path = path;
|
|
guard.fail_closed = true;
|
|
guard.enabled = BUILTIN_PLUGINS
|
|
.iter()
|
|
.map(|plugin| (plugin.id().to_string(), false))
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_enabled(id: &str) -> bool {
|
|
let Some(plugin) = builtin_plugin(id) else {
|
|
return false;
|
|
};
|
|
// Runner/CLI 已绑定配置根,但不会执行 GUI setup;不推断隔离子进程的 AppData。
|
|
let config_dir = crate::game_creator_runtime_config_dir_lock()
|
|
.lock()
|
|
.ok()
|
|
.and_then(|path| path.clone());
|
|
state()
|
|
.lock()
|
|
.map(|mut guard| {
|
|
let Some(path) = guard
|
|
.path
|
|
.clone()
|
|
.or_else(|| config_dir.map(|root| root.join("extensions").join(STATE_FILE_NAME)))
|
|
else {
|
|
return false;
|
|
};
|
|
// 每次查询读取持久化权威,使运行中的其它进程立即感知开关变化。
|
|
let _ = reload_state(&mut guard, &path);
|
|
if guard.fail_closed {
|
|
return false;
|
|
}
|
|
guard
|
|
.enabled
|
|
.get(plugin.id())
|
|
.copied()
|
|
.unwrap_or_else(|| plugin.default_enabled())
|
|
})
|
|
// 状态锁损坏时同样 fail-closed,避免异常状态重新放开内置能力。
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// 内置插件的用户开关;`None` 表示该 id 不是内置插件,由来源自己决定启用状态。
|
|
pub(crate) fn toggle_state(id: &str) -> Option<bool> {
|
|
builtin_plugin(id).map(|_| is_enabled(id))
|
|
}
|
|
|
|
pub(crate) fn set_enabled(id: &str, enabled: bool) -> Result<bool, String> {
|
|
let Some(plugin) = builtin_plugin(id) else {
|
|
return Err(format!("{id} 不是内置插件,不能使用内置插件开关"));
|
|
};
|
|
let mut guard = state()
|
|
.lock()
|
|
.map_err(|_| "内置插件状态锁已损坏".to_string())?;
|
|
let path = guard
|
|
.path
|
|
.clone()
|
|
.ok_or_else(|| "内置插件开关尚未初始化".to_string())?;
|
|
// 保留其它进程刚写入的开关;损坏文件按全部禁用起步,允许用户显式修复。
|
|
let _ = reload_state(&mut guard, &path);
|
|
let previous = guard.enabled.get(plugin.id()).copied();
|
|
guard.enabled.insert(plugin.id().to_string(), enabled);
|
|
if let Err(error) = persist(&guard) {
|
|
match previous {
|
|
Some(value) => {
|
|
guard.enabled.insert(plugin.id().to_string(), value);
|
|
}
|
|
None => {
|
|
guard.enabled.remove(plugin.id());
|
|
}
|
|
}
|
|
return Err(error);
|
|
}
|
|
guard.fail_closed = false;
|
|
Ok(enabled)
|
|
}
|
|
|
|
fn persist(guard: &BuiltinPluginState) -> Result<(), String> {
|
|
let path = guard
|
|
.path
|
|
.clone()
|
|
.ok_or_else(|| "内置插件开关尚未初始化".to_string())?;
|
|
let file = BuiltinPluginStateFile {
|
|
schema_version: Some(STATE_SCHEMA_VERSION.to_string()),
|
|
enabled: guard.enabled.clone(),
|
|
};
|
|
let bytes = serde_json::to_vec_pretty(&file)
|
|
.map_err(|error| format!("序列化内置插件开关失败:{error}"))?;
|
|
let temporary = path.with_extension("json.tmp");
|
|
fs::write(&temporary, bytes).map_err(|error| format!("写入内置插件开关失败:{error}"))?;
|
|
fs::rename(&temporary, &path).map_err(|error| format!("提交内置插件开关失败:{error}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Agent 工具面是否可用:编译期 feature 打开且用户没有禁用该内置插件。
|
|
pub(crate) fn agent_tool_available(plugin: BuiltinPlugin) -> bool {
|
|
plugin.exposes_agent_tools()
|
|
&& match plugin {
|
|
BuiltinPlugin::CocosEditor => cfg!(all(windows, feature = "cocos-editor-execute")),
|
|
BuiltinPlugin::UnityEditor => {
|
|
cfg!(all(
|
|
windows,
|
|
target_arch = "x86_64",
|
|
feature = "unity-editor-execute"
|
|
)) && unity_editor_bridge::is_supported_platform()
|
|
}
|
|
BuiltinPlugin::GodotEditor => {
|
|
cfg!(all(
|
|
windows,
|
|
target_arch = "x86_64",
|
|
feature = "godot-editor-execute"
|
|
)) && godot_editor_bridge::is_supported_platform()
|
|
}
|
|
}
|
|
&& is_enabled(plugin.id())
|
|
}
|
|
|
|
pub(crate) fn cocos_editor_agent_tool_available() -> bool {
|
|
agent_tool_available(BuiltinPlugin::CocosEditor)
|
|
}
|
|
|
|
pub(crate) fn available_agent_tools() -> Vec<&'static str> {
|
|
let mut available = Vec::new();
|
|
if cocos_editor_agent_tool_available() {
|
|
let mut tools = vec![AGC_COCOS_EDITOR_TOOL_NAME];
|
|
tools.extend(
|
|
cocos_editor_bridge::cocos_operation_catalog()
|
|
.iter()
|
|
.filter_map(|tool| tool["name"].as_str()),
|
|
);
|
|
available.extend(tools);
|
|
}
|
|
if unity_editor_agent_tool_available() {
|
|
available.push(AGC_UNITY_EDITOR_TOOL_NAME);
|
|
}
|
|
if godot_editor_agent_tool_available() {
|
|
available.push(AGC_GODOT_EDITOR_TOOL_NAME);
|
|
}
|
|
available
|
|
}
|
|
|
|
/// DirectProject 项目工具目录:Cocos/Unity 不按工程类型过滤,Godot 保持受控项目合同。
|
|
pub(crate) fn available_agent_tools_for_project(root: &Path) -> Vec<&'static str> {
|
|
available_agent_tools()
|
|
.into_iter()
|
|
.filter(|tool| {
|
|
*tool != AGC_GODOT_EDITOR_TOOL_NAME
|
|
|| godot_editor_agent_tool_available_for_project(root)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn unity_editor_agent_tool_available() -> bool {
|
|
agent_tool_available(BuiltinPlugin::UnityEditor)
|
|
}
|
|
|
|
pub(crate) fn godot_editor_agent_tool_available() -> bool {
|
|
agent_tool_available(BuiltinPlugin::GodotEditor)
|
|
}
|
|
|
|
pub(crate) fn godot_editor_agent_tool_available_for_project(root: &Path) -> bool {
|
|
godot_editor_agent_tool_available()
|
|
&& crate::project::discover_local_godot_project_root(root)
|
|
.ok()
|
|
.flatten()
|
|
.is_some()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) use tests::test_lock;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::tempdir;
|
|
|
|
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
|
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
LOCK.get_or_init(|| Mutex::new(()))
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner())
|
|
}
|
|
|
|
#[test]
|
|
fn godot_tools_follow_real_subproject_and_independent_toggle() {
|
|
let _guard = test_lock();
|
|
let config = tempdir().unwrap();
|
|
initialize(config.path()).unwrap();
|
|
let project = tempdir().unwrap();
|
|
fs::create_dir(project.path().join("game")).unwrap();
|
|
fs::write(
|
|
project.path().join("game/project.godot"),
|
|
"config_version=5\n",
|
|
)
|
|
.unwrap();
|
|
let supported = cfg!(all(
|
|
windows,
|
|
target_arch = "x86_64",
|
|
feature = "godot-editor-execute"
|
|
));
|
|
assert_eq!(
|
|
available_agent_tools_for_project(project.path()).contains(&AGC_GODOT_EDITOR_TOOL_NAME),
|
|
supported
|
|
);
|
|
set_enabled(AGC_GODOT_EDITOR_PLUGIN_ID, false).unwrap();
|
|
assert!(!available_agent_tools_for_project(project.path())
|
|
.contains(&AGC_GODOT_EDITOR_TOOL_NAME));
|
|
assert!(is_enabled(AGC_UNITY_EDITOR_PLUGIN_ID));
|
|
set_enabled(AGC_GODOT_EDITOR_PLUGIN_ID, true).unwrap();
|
|
fs::create_dir(project.path().join("other")).unwrap();
|
|
fs::write(
|
|
project.path().join("other/project.godot"),
|
|
"config_version=5\n",
|
|
)
|
|
.unwrap();
|
|
assert!(!godot_editor_agent_tool_available_for_project(
|
|
project.path()
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn unity_tool_visibility_requires_platform_and_independent_toggle() {
|
|
let _guard = test_lock();
|
|
let config = tempdir().unwrap();
|
|
initialize(config.path()).unwrap();
|
|
let supported = cfg!(all(
|
|
windows,
|
|
target_arch = "x86_64",
|
|
feature = "unity-editor-execute"
|
|
));
|
|
assert_eq!(
|
|
available_agent_tools().contains(&AGC_UNITY_EDITOR_TOOL_NAME),
|
|
supported
|
|
);
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).unwrap();
|
|
assert_eq!(unity_editor_agent_tool_available(), supported);
|
|
set_enabled(AGC_UNITY_EDITOR_PLUGIN_ID, false).unwrap();
|
|
assert!(!unity_editor_agent_tool_available());
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).unwrap();
|
|
assert!(!unity_editor_agent_tool_available());
|
|
}
|
|
|
|
#[test]
|
|
fn builtin_plugins_default_to_enabled_and_reject_unknown_ids() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().expect("temp config");
|
|
initialize(directory.path()).expect("initialize");
|
|
assert!(is_builtin(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
assert!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
assert_eq!(toggle_state(AGC_COCOS_EDITOR_PLUGIN_ID), Some(true));
|
|
assert_eq!(toggle_state("imported-plugin"), None);
|
|
assert!(!is_enabled("imported-plugin"));
|
|
assert!(set_enabled("imported-plugin", false).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_state_round_trips_through_appdata_file() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().expect("temp config");
|
|
initialize(directory.path()).expect("initialize");
|
|
assert_eq!(
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).expect("disable"),
|
|
false
|
|
);
|
|
let path = directory.path().join("extensions").join(STATE_FILE_NAME);
|
|
let written = fs::read_to_string(&path).expect("state file");
|
|
assert!(written.contains(STATE_SCHEMA_VERSION));
|
|
assert!(written.contains(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
|
|
// 重新初始化模拟下次启动读取持久化结果。
|
|
initialize(directory.path()).expect("re-initialize");
|
|
assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("enable");
|
|
assert!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_state_file_fails_closed() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().expect("temp config");
|
|
let root = directory.path().join("extensions");
|
|
fs::create_dir_all(&root).expect("extensions dir");
|
|
fs::write(root.join(STATE_FILE_NAME), "{ not json").expect("write corrupt state");
|
|
assert!(initialize(directory.path()).is_err());
|
|
assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
assert_eq!(toggle_state(AGC_COCOS_EDITOR_PLUGIN_ID), Some(false));
|
|
}
|
|
|
|
#[test]
|
|
fn enabling_after_corruption_recovers_without_reinitializing() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().unwrap();
|
|
fs::create_dir_all(directory.path().join("extensions")).unwrap();
|
|
fs::write(
|
|
directory.path().join("extensions/builtin-plugins.json"),
|
|
"{",
|
|
)
|
|
.unwrap();
|
|
assert!(initialize(directory.path()).is_err());
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).unwrap();
|
|
assert!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
}
|
|
|
|
#[test]
|
|
fn availability_reloads_changes_written_by_another_process() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().unwrap();
|
|
initialize(directory.path()).unwrap();
|
|
let path = directory.path().join("extensions/builtin-plugins.json");
|
|
for enabled in [false, true, false] {
|
|
fs::write(
|
|
&path,
|
|
serde_json::json!({
|
|
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
"enabled": {AGC_COCOS_EDITOR_PLUGIN_ID: enabled}
|
|
})
|
|
.to_string(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID), enabled);
|
|
}
|
|
fs::write(&path, "{").unwrap();
|
|
assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
}
|
|
|
|
#[test]
|
|
fn runner_process_reads_disabled_state_without_gui_setup() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().unwrap();
|
|
initialize(directory.path()).unwrap();
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).unwrap();
|
|
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
|
.args([
|
|
"--exact",
|
|
"builtin_plugins::tests::runner_process_probe",
|
|
"--nocapture",
|
|
])
|
|
.env("AGC_BUILTIN_TEST_CONFIG_DIR", directory.path())
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stdout)
|
|
);
|
|
assert!(String::from_utf8_lossy(&result.stdout).contains("1 passed"));
|
|
}
|
|
|
|
#[test]
|
|
fn runner_process_probe() {
|
|
let Some(config_dir) = std::env::var_os("AGC_BUILTIN_TEST_CONFIG_DIR") else {
|
|
return;
|
|
};
|
|
// Runner/CLI 只绑定配置根目录,不进入 Tauri GUI setup。
|
|
crate::set_game_creator_runtime_config_dir(PathBuf::from(config_dir));
|
|
assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_schema_fails_closed() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().expect("temp config");
|
|
let root = directory.path().join("extensions");
|
|
fs::create_dir_all(&root).expect("extensions dir");
|
|
fs::write(
|
|
root.join(STATE_FILE_NAME),
|
|
serde_json::json!({"schemaVersion": "agc.builtin-plugins.v0", "enabled": {}})
|
|
.to_string(),
|
|
)
|
|
.expect("write unsupported state");
|
|
assert!(initialize(directory.path()).is_err());
|
|
assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
|
|
}
|
|
|
|
#[test]
|
|
fn agent_tool_visibility_follows_the_toggle() {
|
|
let _guard = test_lock();
|
|
let directory = tempdir().expect("temp config");
|
|
initialize(directory.path()).expect("initialize");
|
|
let tool_visible_when_enabled = cfg!(all(windows, feature = "cocos-editor-execute"));
|
|
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("enable");
|
|
assert_eq!(
|
|
cocos_editor_agent_tool_available(),
|
|
tool_visible_when_enabled
|
|
);
|
|
assert_eq!(
|
|
crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME),
|
|
tool_visible_when_enabled
|
|
);
|
|
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).expect("disable");
|
|
assert!(!cocos_editor_agent_tool_available());
|
|
assert!(
|
|
!crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME)
|
|
);
|
|
|
|
set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("re-enable");
|
|
assert_eq!(
|
|
crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME),
|
|
tool_visible_when_enabled
|
|
);
|
|
}
|
|
}
|