Skip to content

SCE 编辑器 C# 插件开发指南 ​

插件系统采用属性驱动、零配置设计,一个类加几个 Attribute 即可构成完整插件。

核心特性:

  • [EditorPlugin] + [MCPTool] + [MenuItem] 声明式定义
  • [MCPTool] 方法自动暴露为 MCP 工具,AI 可直接调用
  • [MenuItem] 方法自动注册到编辑器标题菜单栏
  • 通过 IPluginContext 访问受控的服务子集

前提条件:.NET 9.0 SDK + PluginSDK.dll

快速开始 ​

插件是引用 PluginSDK.dll 的 C# 类库。编译后将 DLL 放到编辑器 Plugins/ 目录,启动编辑器自动加载。

最小插件模板 ​

csharp
using SCE.PluginSDK;

[EditorPlugin("MyPlugin", "插件功能描述")]
public class MyPlugin : IEditorPlugin
{
    private IPluginContext _ctx = null!;

    public void Initialize(IPluginContext context) => _ctx = context;
    public void Shutdown() { }

    [MCPTool("my_tool_name", "工具功能描述")]
    public string MyTool(
        [Param("input", "输入参数说明")] string input)
    {
        return $"Result: {input}";
    }
}

项目文件模板 ​

xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Platforms>x64</Platforms>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="PluginSDK">
      <HintPath>path\to\PluginSDK.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

Attribute 参考 ​

[EditorPlugin(name, description)] -- 插件声明(必需,标记在类上) ​

必须同时实现 IEditorPlugin 接口。

属性类型默认值说明
Namestring必填插件显示名称
Descriptionstring必填插件功能描述
Versionstring"1.0"版本号
Authorstring""作者名

[MCPTool(name, description)] -- MCP 工具注册(标记在方法上) ​

工具名自动添加 plugin_ 前缀(如 "my_tool" -> "plugin_my_tool")。

返回类型行为
string直接作为文本返回
object / 任意类序列化为 JSON
Task<string> / Task<object>异步版本
void / Task返回 "OK"

[Param(description)] 或 [Param(name, description)] -- 参数描述 ​

属性类型默认值说明
Namestring?C# 参数名覆盖参数名称
Descriptionstring必填参数说明,对 AI 可见
Requiredbooltrue设为 false 表示可选

支持类型:string, int, long, float, double, bool, 数组, 对象。

属性类型默认值说明
Pathstring必填菜单路径,如 "插件/数据工具/批量重命名"
Shortcutstring?null快捷键,如 "Ctrl+Shift+R"

[EditorCommand(commandId)] -- 编辑器命令(规划中) ​

IPluginContext 服务接口 ​

csharp
public interface IPluginContext
{
    IDataAccess Data;        // 游戏数据表操作
    ITriggerAccess Triggers; // 触发器系统
    IPluginLogger Log;       // 日志输出
    IEditorAccess Editor;    // 编辑器状态
}

IDataAccess ​

csharp
IReadOnlyList<string> GetTableNames();
IReadOnlyList<int> GetEntryIds(string tableName);
object? GetFieldValue(string tableName, int entryId, string fieldName);
bool SetFieldValue(string tableName, int entryId, string fieldName, object? value);
bool TableExists(string tableName);

ITriggerAccess ​

csharp
IReadOnlyList<string> GetFileNames(string? env = null);
IReadOnlyList<string> Search(string query, string? nodeType = null, string? env = null);
bool IsAvailable { get; }

IEditorAccess ​

csharp
bool IsMapLoaded { get; }
string? ProjectPath { get; }

IPluginLogger ​

csharp
void Info(string message);
void Warn(string message);
void Error(string message);
void Debug(string message);

运行机制 ​

  1. 编辑器加载地图时初始化插件系统
  2. 扫描 Plugins/ 目录中所有 DLL,查找 [EditorPlugin] 类
  3. 调用每个插件的 Initialize(context)
  4. 反射 [MCPTool] 方法,生成 JSON Schema 注册到 MCP 工具列表
  5. 反射 [MenuItem] 方法,注册到编辑器菜单栏
  6. AI 可通过 MCP 调用 plugin_xxx 工具

最佳实践 ​

  • 一个 DLL 一个插件
  • 工具命名用 snake_case(自动加 plugin_ 前缀)
  • 在返回值中包含错误信息,不要抛异常
  • 用 _ctx.Log 输出日志,禁止 Console.WriteLine
  • I/O 密集型操作用 Task<string> 返回类型
  • 测试:DLL 放入 Plugins/,以 --mcp-stdio 或 --mcp-port=8765 启动,用 tools/list 验证

完整示例 ​

csharp
using SCE.PluginSDK;

[EditorPlugin("DataBatch", "游戏数据表批量操作工具")]
public class DataBatchPlugin : IEditorPlugin
{
    private IPluginContext _ctx = null!;

    public void Initialize(IPluginContext context) => _ctx = context;
    public void Shutdown() { }

    [MCPTool("batch_set_field", "批量设置数据表中所有条目的某个字段值")]
    public object BatchSetField(
        [Param("table", "数据表名")] string table,
        [Param("field", "要设置的字段名")] string field,
        [Param("value", "要设置的值")] string value)
    {
        if (!_ctx.Data.TableExists(table))
            return new { error = $"数据表 '{table}' 不存在" };

        var ids = _ctx.Data.GetEntryIds(table);
        int updated = 0;
        foreach (var id in ids)
        {
            if (_ctx.Data.SetFieldValue(table, id, field, value))
                updated++;
        }
        return new { success = true, updated, total = ids.Count };
    }

    [MCPTool("batch_find_entries", "查找字段值匹配的条目")]
    public object FindEntries(
        [Param("table", "数据表名")] string table,
        [Param("field", "要检查的字段名")] string field,
        [Param("value", "要匹配的值")] string value)
    {
        if (!_ctx.Data.TableExists(table))
            return new { error = $"数据表 '{table}' 不存在" };

        var ids = _ctx.Data.GetEntryIds(table);
        var matches = new List<int>();
        foreach (var id in ids)
        {
            var fieldValue = _ctx.Data.GetFieldValue(table, id, field);
            if (fieldValue?.ToString() == value)
                matches.Add(id);
        }
        return new { matches, count = matches.Count };
    }

    [MenuItem("插件/数据批量工具/显示统计")]
    public void ShowStats()
    {
        var tables = _ctx.Data.GetTableNames();
        _ctx.Log.Info($"当前共 {tables.Count} 张数据表");
    }
}