# ZGN 插件 API 开发文档（开发者版）

本文档面向会写代码的插件开发者。

- 插件系统宿主：`ZhiGenNiao.dll`
- 当前 ABI：`ZGN_PLUGIN_ABI_VERSION = 12`

## 1. 系统概览

当前插件系统支持两种插件形态：

- `DLL` 插件：你自己编译成 `.dll` 后放进插件目录。
- `C` 源码插件：把 `.c` 直接放进插件目录，宿主会自动编译成临时 DLL 后再加载。

两种插件都通过菜单里的 `插件 / Plugin` 页面统一管理：

- 刷新
- 加载
- 取消加载

默认目录：

```text
\resource\plugins
```

对于源码插件，自动编译产物放在：

```text
\resource\plugins\_compiled
```

## 2. 60 秒快速开始

最快的开发路径是直接写一个 `.c` 源码插件：

1. 在 `\resource\plugins` 新建一个 `.c` 文件。
2. 文件头写元信息。
3. `#include "zgn_plugin_api.h"`。
4. 实现 5 个必需导出函数。
5. 进游戏后在 `插件 / Plugin` 页面点 `刷新`。
6. 选中该插件并点击 `加载`。

最小可运行示例：

```c
/*
@zgn_name HelloPlugin
@zgn_author YourName
@zgn_version 1.0.0
@zgn_description 最小可运行插件
@zgn_mode overlay
*/

#include "zgn_plugin_api.h"

static const ZgnHostApi* g_host = 0;

ZGN_PLUGIN_EXPORT uint32_t ZGN_PLUGIN_CALL ZgnPlugin_GetAbiVersion(void)
{
    return ZGN_PLUGIN_ABI_VERSION;
}

ZGN_PLUGIN_EXPORT const ZgnPluginInfo* ZGN_PLUGIN_CALL ZgnPlugin_GetInfo(void)
{
    static const ZgnPluginInfo info = {
        "HelloPlugin",
        "YourName",
        "1.0.0",
        "最小可运行插件",
        ZGN_PLUGIN_ABI_VERSION
    };
    return &info;
}

ZGN_PLUGIN_EXPORT int32_t ZGN_PLUGIN_CALL ZgnPlugin_OnLoad(const ZgnHostApi* host_api)
{
    g_host = host_api;
    if (g_host && g_host->log)
    {
        g_host->log("HelloPlugin", "插件已加载");
    }
    if (g_host && g_host->show_hint_utf8)
    {
        g_host->show_hint_utf8("HelloPlugin 已加载");
    }
    return 1;
}

ZGN_PLUGIN_EXPORT void ZGN_PLUGIN_CALL ZgnPlugin_OnUpdate(void)
{
}

ZGN_PLUGIN_EXPORT void ZGN_PLUGIN_CALL ZgnPlugin_OnUnload(void)
{
    if (g_host && g_host->log)
    {
        g_host->log("HelloPlugin", "插件已卸载");
    }
    g_host = 0;
}
```

## 3. 插件目录、产物与日志

### 3.1 目录

- Debug 插件目录：`\resource\plugins`

### 3.2 源码插件自动编译产物

源码插件 `.c` 会被编译到 `_compiled` 目录，输出通常包含：

- 编译后的临时 `.dll`
- `.obj`
- `.lib`
- `.exp`
- `.build.log`

其中最重要的是：

- 编译日志：`resource\plugins\_compiled\*.build.log`

### 3.3 运行期日志

插件宿主自己的运行日志：

- Debug：`\Log\plugin_loader.log`

你通过 `host_api->log()` 写出的插件日志也会进入宿主日志体系，适合做行为追踪和排错。

## 4. 源码插件头部元信息

推荐格式：

```c
/*
@zgn_name 原生弹窗插件
@zgn_author ZGN
@zgn_version 1.2.0
@zgn_description 按 B 打开控制窗口，测试原生弹窗
@zgn_mode overlay
*/
```

### 4.1 当前宿主实际解析的字段

当前宿主会从源码前 80 行内读取这些字段：

- `@zgn_name`
- `@zgn_author`
- `@zgn_version`
- `@zgn_description`
- `@zgn_http_auth`

说明：

- 没写 `@zgn_name` 时，插件列表会退回显示文件名去掉扩展名后的名称。
- `.c` 插件在未加载前，插件页优先显示这里的头部元信息。
- 插件加载后，最终信息仍以 `ZgnPluginInfo` 为准。
- `@zgn_http_auth` 只用于“源码插件开放本地 HTTP / TCP 监听端口”的机器授权，不会显示在插件列表里。

### 4.2 关于 `@zgn_mode`

`@zgn_mode` 目前是推荐写法，但当前宿主并不会实际解析它。

也就是说：

- 你可以继续写，方便人看。
- 不要依赖它控制真实行为。
- 真正决定窗口显示方式的是你是否导出 `ZgnPlugin_OnMenu` 或 `ZgnPlugin_OnOverlay`，以及你在代码里怎么写。

## 5. ABI、导出函数与校验规则

### 5.1 当前 ABI

```c
#define ZGN_PLUGIN_ABI_VERSION 12u
```

宿主加载时会检查：

- `ZgnPlugin_GetAbiVersion()`
- `ZgnPluginInfo.min_host_abi_version`

当前宿主 ABI 是 `12`，并继续兼容 `ABI 1 / ABI 2 / ABI 3 / ABI 4 / ABI 5 / ABI 6 / ABI 7 / ABI 8 / ABI 9 / ABI 10 / ABI 11` 插件。

说明：

- 新插件建议按当前头文件编译，也就是 `ABI 12`。
- 旧的 `ABI 1 / ABI 2 / ABI 3 / ABI 4 / ABI 5 / ABI 6 / ABI 7 / ABI 8 / ABI 9 / ABI 10 / ABI 11` 插件仍可继续加载。
- 如果插件声明的 ABI 高于当前宿主，插件会被拒绝加载。
- 头文件里仍保留了少量“已撤销 API”的兼容槽位，只是为了维持 ABI 结构布局稳定；宿主不会再为这些槽位提供实现。

### 5.2 必需导出

下面 5 个函数是硬要求，函数名必须完全一致：

```c
uint32_t ZgnPlugin_GetAbiVersion(void);
const ZgnPluginInfo* ZgnPlugin_GetInfo(void);
int32_t ZgnPlugin_OnLoad(const ZgnHostApi* host_api);
void ZgnPlugin_OnUpdate(void);
void ZgnPlugin_OnUnload(void);
```

### 5.3 可选导出

```c
void ZgnPlugin_OnMenu(void);
void ZgnPlugin_OnOverlay(void);
```

### 5.4 每个函数的语义

- `ZgnPlugin_GetAbiVersion`：返回插件 ABI 版本，通常直接返回 `ZGN_PLUGIN_ABI_VERSION`。
- `ZgnPlugin_GetInfo`：返回一个静态 `ZgnPluginInfo`。
- `ZgnPlugin_OnLoad`：插件加载时调用，返回 `1` 表示成功，返回 `0` 表示拒绝加载。
- `ZgnPlugin_OnUpdate`：每帧在游戏线程调用。
- `ZgnPlugin_OnUnload`：插件卸载时调用。
- `ZgnPlugin_OnMenu`：菜单打开时在 ImGui UI 线程调用。
- `ZgnPlugin_OnOverlay`：独立悬浮窗口渲染时在 ImGui UI 线程调用。

### 5.5 `ZgnPluginInfo`

结构定义：

```c
typedef struct ZgnPluginInfo
{
    const char* name;
    const char* author;
    const char* version;
    const char* description;
    uint32_t min_host_abi_version;
} ZgnPluginInfo;
```

推荐规则：

- `name`：尽量稳定，不要频繁改。
- `author`：建议写你自己的名字或组织名。
- `version`：建议用 `1.0.0` 这种格式。
- `description`：给插件页展示，尽量一句话说清用途。
- `min_host_abi_version`：一般写 `ZGN_PLUGIN_ABI_VERSION`。

## 6. 生命周期与线程模型

这是最重要的部分之一。

### 6.1 游戏线程

下面这些回调在游戏线程执行：

- `OnLoad`
- `OnUpdate`
- `OnUnload`

适合做的事情：

- 访问宿主提供的世界对象
- 读取或修改坐标
- 处理游戏逻辑
- 做轻量状态机

### 6.2 ImGui UI 线程

下面这些回调在 ImGui UI 线程执行：

- `OnMenu`
- `OnOverlay`

适合做的事情：

- 画窗口
- 画文本
- 画按钮
- 画输入框

不适合做的事情：

- 直接访问 managed 对象
- 直接碰 `GameAssembly / il2cpp` 对象
- 做高风险 SDK 直连

### 6.3 一个很重要的例外

虽然 `OnMenu / OnOverlay` 本身是 UI 线程，但下面这些原生弹窗接口已经由宿主排队回游戏线程执行，所以可以直接在按钮回调里调用：

- `show_hint_utf8`
- `show_sdk_code_dialog`
- `show_ok_cancel_hint_utf8`
- `show_custom_ok_cancel_hint_utf8`
- `show_custom_ok_cancel_hint_event_utf8`
- `show_plugin_confirm_utf8`
- `show_joypad_confirm_utf8`
- `quit_game`
- `logout_to_login`
- `open_url_in_default_browser_utf8`

### 6.4 Fault / 熔断行为

如果插件在以下任意阶段抛出异常：

- `OnLoad`
- `OnUpdate`
- `OnUnload`
- `OnMenu`
- `OnOverlay`

宿主会把该插件标记为 `Faulted`，而不是让整个插件系统一起崩掉。插件页会显示错误状态，并提示重启游戏后再试。

### 6.5 工作线程与本地网络端口

插件如果要开本地开发端口，例如：

- `127.0.0.1:25565`
- `127.0.0.1:8080`

必须自己起工作线程处理，不能把监听、`accept()`、`recv()`、HTTP 解析这类阻塞逻辑放在：

- `OnLoad`
- `OnUpdate`
- `OnMenu`
- `OnOverlay`

推荐模式：

1. `OnLoad` 里初始化状态并启动工作线程。
2. 工作线程里完成：
   - `WSAStartup`
   - `socket / bind / listen`
   - 请求循环
3. `OnUnload` 里设置退出标记，关闭 socket，等待线程结束，再做 `WSACleanup`。

强烈建议：

- 默认只绑定 `127.0.0.1`，不要默认绑 `0.0.0.0`
- 默认只做开发调试用途，不要直接暴露公网
- 不要在工作线程里直接碰 `unitysdk` 托管对象
- 如果工作线程需要影响游戏状态，请把结果先写到你自己的共享状态，再由 `OnUpdate` 在游戏线程里消费

从 `ABI 12` 开始，源码插件如果看起来会启动本地网络监听，宿主会在加载前做一次静态扫描。扫描到 `winsock2.h / ws2tcpip.h / WSAStartup / socket / bind / listen / accept / HTTP/1.x` 等明显痕迹时，源码头部必须提供有效的 `@zgn_http_auth`，否则插件会被拒绝加载。

拒绝加载时日志会写英文提示：

```text
HTTP port authorization is required. Your machine code: ZGN-MC-.... Please send your machine code to main@neikos496.xyz to request HTTP port permission.
```

授权 key 格式：

```c
/*
@zgn_name MyHttpPlugin
@zgn_author You
@zgn_version 1.0.0
@zgn_description Local HTTP server demo
@zgn_http_auth ZGN00-...
*/
```

授权 key 由项目根目录的 `generate_http_port_auth.py` 生成。流程是：用户把日志里的机器码发给 `main@neikos496.xyz`，你在本机运行脚本粘贴机器码，脚本输出 `ZGN00-...` 授权 key，再把 key 发回给用户。

### 6.6 端口调用的安全加密要求

如果你的插件要开放本地端口，并允许其他程序通过端口调用插件逻辑，那么默认规则应视为：

- 禁止明文 HTTP
- 禁止只靠“固定 token 明文传输”
- 禁止默认监听 `0.0.0.0`

最低要求：

1. 只监听 `127.0.0.1`
2. 通信内容必须走加密通道
3. 请求必须带身份校验
4. 必须有重放保护

推荐实现方式，按优先级排序：

1. `TLS / HTTPS`
   - 优先使用 Windows 自带 `SChannel`
   - 适合“其他程序像正常客户端一样发 HTTPS 请求”
2. 自定义 TCP + 消息级加密
   - 使用 Windows CNG（`bcrypt`）做 `AES-GCM`
   - 每条消息使用随机 `nonce`
   - 再配合时间戳 / 递增计数器做重放保护

补充说明：

- 只有“共享密钥 + 明文 HTTP Header”不算安全加密通道。
- 只有 Base64、XOR、简单混淆都不算加密。
- 即使只监听 `127.0.0.1`，如果本机有其他进程，也仍然可能被本地抓包或复用接口，所以不能把“仅本地”当成“可明文”。
- 如果只是内部开发联调，最少也应做到：`127.0.0.1 + AES-GCM + 时间戳/nonce + 身份校验`。

推荐开发约束：

- 插件启动时生成或读取本地密钥材料，不要把密钥硬编码成公开常量
- 响应里不要直接返回高风险原始对象地址
- 对外暴露的端口能力尽量做成白名单动作，而不是任意执行
- 每个外部请求都先写入插件自己的审计日志，再决定是否执行
- 开端口前必须调用 `ZgnAuthorizeHttpPortUtf8(...)`；如果返回 `0`，不要继续 `bind / listen`

授权机制说明：

- 机器码来自宿主读取本机稳定标识后做哈希，不直接暴露原始设备信息。
- 授权 key 绑定机器码，使用 `ZGN00` 魔术头、16 轮 ROX 混淆和 `AES-128-CBC` 包装。
- 这能阻止普通复制 key、漏写 key、随便打开端口等情况；如果恶意 DLL 完全绕过 Host API 直接调 WinSock，仍需要未来再加全局 WinSock Hook 才能做到硬拦截。

## 7. `ZgnHostApi` 总览

在 `OnLoad` 中，宿主会把 `const ZgnHostApi*` 传给插件。

你应该把它保存到全局静态指针里，例如：

```c
static const ZgnHostApi* g_host = 0;
```

结构里包含：

- 宿主信息：`abi_version`、`host_name`
- 日志与路径
- 字符串与对象辅助
- 世界对象访问
- 玩家状态查询
- 大世界敌人只读查询
- 已加载宝箱基础信息只读查询
- HTTP 端口授权
- 玩家身份信息
- 系统时间与宿主动作
- 位置读写
- 原生弹窗
- ImGui UI 封装
- 原始 `GameAssembly / il2cpp` 桥接

### 7.1 返回值约定

大多数 Host API 都遵循下面的规则：

- 返回 `1`：成功
- 返回 `0`：失败
- 返回指针：`NULL` 表示失败或对象不存在

调用建议：

- 先判空函数指针
- 再检查返回值
- 不要默认宿主一定提供所有高级能力

## 8. `ZgnHostApi` 详细参考

以下签名均来自 `zgn_plugin_api.h`。

### 8.1 宿主信息

```c
uint32_t abi_version;
const char* host_name;
```

- `abi_version`：当前宿主 ABI 版本。
- `host_name`：当前宿主名，当前实现为 `ZhiGenNiao`。

### 8.2 日志与路径

```c
void (*log)(const char* plugin_name, const char* message);
const char* (*get_resource_root)(void);
const char* (*get_log_directory)(void);
const char* (*get_plugin_directory)(void);
```

说明：

- `log`：写一条插件日志。
- `get_resource_root`：资源根目录。
- `get_log_directory`：日志目录。
- `get_plugin_directory`：当前插件目录。

推荐：

- 每个按钮点击、关键状态切换、加载失败路径都打日志。
- 复杂插件启动时把目录信息打印一次，排错会很方便。

### 8.3 字符串与对象辅助

```c
void* (*create_system_string_utf8)(const char* utf8);
int32_t (*get_unity_object_name_utf8)(void* object, char* buffer, uint32_t buffer_size);
```

说明：

- `create_system_string_utf8`：把 UTF-8 文本转换成 `System.String*`，高级插件直连 SDK 时可用。
- `get_unity_object_name_utf8`：读取 Unity 对象名，成功返回 `1`。

建议：

- 纯 C 插件优先用 Host API 封装，不要急着自己操作 `System.String*`。
- 如果只是要调宿主原生弹窗，通常不需要自己创建 `System.String`。

### 8.4 常用世界对象

```c
void* (*get_main_camera)(void);
void* (*get_ui_camera)(void);
void* (*get_local_player)(void);
int32_t (*is_interactive_world_ready)(void);
```

说明：

- `get_main_camera`：主相机，失败返回 `NULL`。
- `get_ui_camera`：UI 相机，失败返回 `NULL`。
- `get_local_player`：本地玩家对象，失败返回 `NULL`。
- `is_interactive_world_ready`：当前是否处于可交互世界，成功状态返回 `1`。

建议：

- 访问玩家和坐标前先判断 `is_interactive_world_ready()`。
- 插件刚进游戏或切场景时，本地玩家可能暂时为空。

### 8.5 玩家身份信息

```c
int32_t (*get_local_player_uid)(uint32_t* out_uid);
int32_t (*get_local_player_nickname_utf8)(char* buffer, uint32_t buffer_size);
```

说明：

- `get_local_player_uid`：读取当前登录玩家的 UID 数字，成功时写入 `out_uid` 并返回 `1`。
- `get_local_player_nickname_utf8`：读取当前登录玩家昵称，按 UTF-8 写入 `buffer`，成功返回 `1`。
- 宿主当前底层走的是 `RPG::Client::PlayerData::get_UserID()` 和 `RPG::Client::PlayerData::get_NickName()`。

调用建议：

- `out_uid` 不能为 `NULL`。
- `buffer` 不能为空，`buffer_size` 必须大于 `0`。
- 这两个接口不依赖 `is_interactive_world_ready()`，因为它们读取的是玩家资料，不是场景内实体坐标。
- 但如果还没进到玩家数据已同步的阶段，接口仍可能返回 `0`。

示例：

```c
uint32_t uid = 0;
char nickname[128] = {0};

if (g_host)
{
    if (ZgnGetLocalPlayerUid(g_host, &uid) &&
        ZgnGetLocalPlayerNicknameUtf8(g_host, nickname, sizeof(nickname)))
    {
        char message[256];
        _snprintf_s(message, sizeof(message), _TRUNCATE, "uid=%u nickname=%s", uid, nickname);
        g_host->log("PlayerInfoDemo", message);
    }
}
```

### 8.6 玩家状态

如果你只是想判断“当前玩家现在处于什么阶段”，推荐优先用这组 Host API，而不是插件自己到处拼 SDK。

签名：

```c
typedef struct ZgnPlayerStateInfo
{
    int32_t has_local_player;
    int32_t is_interactive_world_ready;
    int32_t is_in_battle;
    int32_t has_battle_info;
    int32_t game_mode_type;
    uint32_t current_battle_id;
    int32_t is_in_performance;
    int32_t is_performance_replaying;
} ZgnPlayerStateInfo;

int32_t (*get_player_state_info)(ZgnPlayerStateInfo* out_info);
```

字段说明：

- `has_local_player`：当前是否已经拿到本地玩家实体
- `is_interactive_world_ready`：当前是否处于可交互大世界
- `is_in_battle`：`BattleModule::get_IsInBattle()`
- `has_battle_info`：`BattleModule::HasBattleInfo()`
- `game_mode_type`：`AdventureStatic::GetCurrentGameModeType()` 返回的枚举整数
- `current_battle_id`：当前战斗 ID；不在战斗时通常是 `0`
- `is_in_performance`：当前是否处于演出 / performance
- `is_performance_replaying`：当前是否处于 performance replay

返回值约定：

- 成功返回 `1`
- 失败返回 `0`

适合用途：

- 按钮点击前先判断当前能不能执行
- 区分“在大世界”还是“已经进战斗”
- 区分“可交互状态”还是“正在演出 / 回放”

头文件辅助函数：

```c
int32_t ZgnGetPlayerStateInfo(
    const ZgnHostApi* host_api,
    ZgnPlayerStateInfo* out_info);
```

示例：

```c
ZgnPlayerStateInfo state = {};
if (g_host && ZgnGetPlayerStateInfo(g_host, &state))
{
    if (g_host->log)
    {
        char message[256];
        snprintf(
            message,
            sizeof(message),
            "player_state local=%d world=%d battle=%d battle_info=%d mode=%d battle_id=%u performance=%d replay=%d",
            state.has_local_player,
            state.is_interactive_world_ready,
            state.is_in_battle,
            state.has_battle_info,
            state.game_mode_type,
            state.current_battle_id,
            state.is_in_performance,
            state.is_performance_replaying);
        g_host->log("StateDemo", message);
    }
}
```

建议：

- 这是只读状态接口，不会触发任何游戏行为
- 插件如果只需要判断“能不能在当前时机执行”，优先用这组接口
- `game_mode_type` 当前直接透出 SDK 枚举整数；如果你的插件要展示给用户看，可以在插件侧自己做一个文本映射表

### 8.7 位置相关

```c
int32_t (*get_local_player_position)(ZgnVector3* out_position);
int32_t (*set_local_player_position)(ZgnVector3 position);
int32_t (*get_transform_position)(void* transform, ZgnVector3* out_position);
int32_t (*set_transform_position)(void* transform, ZgnVector3 position);
```

说明：

- `get_local_player_position`：读本地玩家坐标。
- `set_local_player_position`：改本地玩家坐标。
- `get_transform_position`：读任意 `Transform` 坐标。
- `set_transform_position`：改任意 `Transform` 坐标。

使用建议：

- `out_position` 不能为 `NULL`。
- 改坐标前最好先判断世界是否 ready。
- 高频写坐标要谨慎，尽量做成按键触发或节流。

### 8.8 动作触发（普通攻击 / 秘技）

```c
int32_t (*trigger_local_player_attack)(void);
int32_t (*trigger_local_player_maze_skill)(void);
```

说明：

- `trigger_local_player_attack`：请求当前本地玩家触发一次普通攻击输入/动作链。
- `trigger_local_player_maze_skill`：请求当前本地玩家触发一次秘技/迷宫技能输入/动作链。
- 这两个接口对应底层 SDK `AdventureCharacterController::TriggerAttack()` 和 `AdventureCharacterController::TriggerMazeSkill()`。
- 底层 SDK 这两个原始调用的返回类型都是 `void`，所以 Host API 返回的不是“技能是否释放成功”，而是宿主分配的 `request_id`。

返回值约定：

- `> 0`：请求已经被宿主接受并排队，会在游戏线程执行。
- `0`：请求未能排队，本次调用没有被宿主接受。
- 不要把返回值当成角色是否真的出手或技能是否真的放出来；真实结果仍取决于场景状态、角色状态和输入限制。

调用建议：

- 建议先判断 `is_interactive_world_ready()`，只有在可交互世界里再触发。
- 插件侧优先调用 Host API，不要自己跨线程直连底层 `TriggerAttack()` / `TriggerMazeSkill()`。
- 角色正在切场景、播剧情、不可控、被禁输入时，即使排队成功，也可能看不到动作效果。
- 排错时可以查看 `Log\sdk_trigger_debug.log`，宿主会记录 `request_id`、排队阶段和实际调用阶段。

示例：

```c
if (g_host && g_host->is_interactive_world_ready &&
    g_host->is_interactive_world_ready())
{
    if (g_host->trigger_local_player_attack)
    {
        const int32_t attack_request_id = g_host->trigger_local_player_attack();
        if (g_host->log)
        {
            g_host->log("ActionDemo",
                attack_request_id > 0
                    ? "trigger_local_player_attack queued"
                    : "trigger_local_player_attack queue failed");
        }
    }

    if (g_host->trigger_local_player_maze_skill)
    {
        const int32_t maze_request_id = g_host->trigger_local_player_maze_skill();
        if (g_host->log)
        {
            g_host->log("ActionDemo",
                maze_request_id > 0
                    ? "trigger_local_player_maze_skill queued"
                    : "trigger_local_player_maze_skill queue failed");
        }
    }
}
```

### 8.9 附近可交互按钮（已验证成功链路）
这组接口只包装 `SDK.md` 里已经实测成功的这条链路：

- `ShowTriggerUIButton(1581)` 出现时缓存当前 `ButtonListTrigger_RuntimeButton`
- 插件读取当前可见交互按钮的 `hostRuntimeID / 文本 / 名称 / 图标 / enable / priority`
- 插件再按 `hostRuntimeID` 调用 `TriggerUIButtonConfirm`

当前宿主**只开放这条稳定链路**，不把 `ButtonParam` / `TalkTrigger` 那些实验 fallback 包进 Host API。也就是说，这组接口面向的是“屏幕上已经出现 F 提示 / 交互按钮”的场景。

签名：
```c
typedef struct ZgnInteractionButtonInfo
{
    uint32_t host_runtime_id;
    int32_t enable;
    int32_t show_priority;
} ZgnInteractionButtonInfo;

int32_t (*can_local_player_interact_option)(void);
int32_t (*get_visible_interaction_button_count)(void);
int32_t (*get_visible_interaction_button_info)(int32_t index, ZgnInteractionButtonInfo* out_info);
int32_t (*get_visible_interaction_button_text_utf8)(uint32_t host_runtime_id, char* buffer, uint32_t buffer_size);
int32_t (*get_visible_interaction_button_name_utf8)(uint32_t host_runtime_id, char* buffer, uint32_t buffer_size);
int32_t (*get_visible_interaction_button_icon_utf8)(uint32_t host_runtime_id, char* buffer, uint32_t buffer_size);
int32_t (*trigger_visible_interaction_button)(uint32_t host_runtime_id);
```

字段说明：
- `can_local_player_interact_option`：读 `AdventureStatic::CheckLocalPlayerCanInteractOption()` 的当前结果。它表示“当前本地玩家是否处于可交互状态”，但**不保证一定已经拿到 RuntimeButton**。
- `get_visible_interaction_button_count`：当前实现只会返回 `0` 或 `1`。因为宿主目前只缓存“当前可见的那个 RuntimeButton”。
- `host_runtime_id`：当前这次交互按钮对应的唯一运行时 ID，也是后续触发时要传回去的 ID。
- `enable`：按钮当前是否可触发。
- `show_priority`：按钮显示优先级，直接透出底层 `ButtonShowPriority` 整数值。
- `get_visible_interaction_button_text_utf8`：读取按钮显示文本，优先对应 `get_ButtonText()` 转出的文本。
- `get_visible_interaction_button_name_utf8`：读取按钮内部名字，对应 `get_ButtonName()`。
- `get_visible_interaction_button_icon_utf8`：读取按钮图标标识，对应 `get_ButtonIcon()`。
- `trigger_visible_interaction_button`：只会尝试触发“当前缓存且仍可读、`enable=true`、`host_runtime_id` 匹配”的那个按钮。

返回值约定：
- `can_local_player_interact_option`：可交互返回 `1`，否则返回 `0`
- `get_visible_interaction_button_count`：当前可见 RuntimeButton 数量，当前只会是 `0` 或 `1`
- 其余接口：成功返回 `1`，失败返回 `0`

当前实现边界：
- 这组接口不是“全地图扫描附近所有交互点”，而是“读取当前宿主已经捕获到的可见交互按钮”。
- 只有在游戏已经实际弹出那种 `ShowTriggerUIButton(1581)` 风格的交互提示时，这组接口才会有值。
- 有些交互只走 `ShowTriggerUI(1575)` 或别的链路，这类当前**不纳入 Host API**。
- `host_runtime_id` 只在当前缓存仍然有效时可用；按钮消失、隐藏、过期后，查询会重新回到 `0` / 失败。
- 这组接口建议只在 `OnLoad / OnUpdate / OnUnload` 里调用，不建议在 `OnMenu / OnOverlay` 里直接读。

头文件还提供了这些辅助函数：
```c
int32_t ZgnCanLocalPlayerInteractOption(
    const ZgnHostApi* host_api);

int32_t ZgnGetVisibleInteractionButtonCount(
    const ZgnHostApi* host_api);

int32_t ZgnGetVisibleInteractionButtonInfo(
    const ZgnHostApi* host_api,
    int32_t index,
    ZgnInteractionButtonInfo* out_info);

int32_t ZgnGetVisibleInteractionButtonTextUtf8(
    const ZgnHostApi* host_api,
    uint32_t host_runtime_id,
    char* buffer,
    uint32_t buffer_size);

int32_t ZgnGetVisibleInteractionButtonNameUtf8(
    const ZgnHostApi* host_api,
    uint32_t host_runtime_id,
    char* buffer,
    uint32_t buffer_size);

int32_t ZgnGetVisibleInteractionButtonIconUtf8(
    const ZgnHostApi* host_api,
    uint32_t host_runtime_id,
    char* buffer,
    uint32_t buffer_size);

int32_t ZgnTriggerVisibleInteractionButton(
    const ZgnHostApi* host_api,
    uint32_t host_runtime_id);
```

示例：
```c
ZgnInteractionButtonInfo button = {};
char text[256] = {0};
char name[256] = {0};

if (g_host &&
    ZgnCanLocalPlayerInteractOption(g_host) &&
    ZgnGetVisibleInteractionButtonCount(g_host) > 0 &&
    ZgnGetVisibleInteractionButtonInfo(g_host, 0, &button))
{
    ZgnGetVisibleInteractionButtonTextUtf8(
        g_host,
        button.host_runtime_id,
        text,
        (uint32_t)sizeof(text));

    ZgnGetVisibleInteractionButtonNameUtf8(
        g_host,
        button.host_runtime_id,
        name,
        (uint32_t)sizeof(name));

    if (g_host->log)
    {
        char message[512];
        snprintf(
            message,
            sizeof(message),
            "interaction id=%u enable=%d priority=%d text=%s name=%s",
            button.host_runtime_id,
            button.enable,
            button.show_priority,
            text[0] ? text : "(empty)",
            name[0] ? name : "(empty)");
        g_host->log("InteractionDemo", message);
    }

    if (button.enable)
    {
        ZgnTriggerVisibleInteractionButton(g_host, button.host_runtime_id);
    }
}
```

### 8.10 大世界敌人只读查询

这组接口是把 `SDK.md` 里“大世界敌人 / NpcMonster / MapNpcDef”那条读取链，包装成了宿主可直接调用的只读 Host API。

当前目标是：

- 读取当前已经加载进场景的大世界怪物
- 拿某一只怪物的基础状态、实时坐标和地图配置坐标
- 拿离本地玩家最近的一只怪物
- 读取 `MapNpcDef::UniqueName`

签名：

```c
typedef struct ZgnWorldMonsterInfo
{
    void* entity;
    void* npc_component;
    void* map_npc_def;
    uint32_t runtime_id;
    uint32_t server_entity_id;
    uint32_t group_id;
    uint32_t group_entity_id;
    uint32_t npc_id;
    uint32_t stage_id;
    int32_t stage_type;
    int32_t is_loaded;
    int32_t is_on_stage;
    int32_t is_visible;
    int32_t is_monster;
    int32_t is_danger_monster;
    int32_t alive_state;
    int32_t status;
    int32_t map_npc_type;
    int32_t purpose_type;
    int32_t camp_id;
    uint32_t mapping_info_id;
    uint32_t nearest_teleport_mapping_info_id;
    float alert_value;
    float alert_value_normalized;
    float warning_percent;
    float combat_power;
    ZgnVector3 position;
    ZgnVector3 config_position;
} ZgnWorldMonsterInfo;

int32_t (*get_loaded_world_monster_count)(void);
int32_t (*get_loaded_world_monster_info)(int32_t index, ZgnWorldMonsterInfo* out_info);
int32_t (*get_nearest_world_monster_info)(ZgnWorldMonsterInfo* out_info);
int32_t (*get_world_monster_unique_name_utf8)(void* map_npc_def, char* buffer, uint32_t buffer_size);
```

字段说明：

- `entity`：对应的 `RPG::GameCore::GameEntity*`
- `npc_component`：对应的 `RPG::GameCore::NPCComponent*`
- `map_npc_def`：对应的 `RPG::Client::MapNpcDef*`
- `runtime_id / server_entity_id / group_id / group_entity_id`：实体基础 ID
- `npc_id`：优先取 `MapNpcDef::get_NPCID()`，拿不到时 fallback 到 `NPCComponent::GetNPCID()`
- `stage_id`：优先取 `NPCComponent::get_StageID()`，为 0 时 fallback 到 `AdventureStatic::GetStageIDFromNPC(...)`
- `stage_type`：对应 `AdventureStatic::GetNpcMonsterStageType(...)`
- `is_loaded / is_on_stage / is_visible`：当前实体状态
- `is_monster / is_danger_monster`：是否怪物 / 是否危险怪
- `alive_state / status`：底层枚举值，直接按整数透出
- `map_npc_type / purpose_type / camp_id`：地图定义层枚举值，直接按整数透出
- `mapping_info_id / nearest_teleport_mapping_info_id`：地图定位相关 ID
- `alert_value / alert_value_normalized / warning_percent / combat_power`：警戒值、警戒百分比和战力估算
- `position`：当前场景里的实时坐标，优先取实体 `UnityGO->transform.position`
- `config_position`：地图配置坐标，来自 `MapNpcDef::get_Position()`

返回值约定：

- `get_loaded_world_monster_count`：返回当前已加载怪物数量，失败返回 `0`
- 其他 3 个接口：成功返回 `1`，失败返回 `0`

当前实现边界：

- 只统计“当前已经加载到场景里的大世界怪物”，不会把纯地图配置里还没实例化的怪物算进去
- 这是只读接口，不会改怪物 AI、位置、状态
- `position` 和 `config_position` 不一定相同
- `get_world_monster_unique_name_utf8` 需要传 `out_info.map_npc_def`

线程要求：

- 这组接口只能在游戏线程回调里用，也就是 `OnLoad / OnUpdate / OnUnload`
- 不建议在 `OnMenu / OnOverlay` 里直接调用，因为它们会触碰游戏内 managed 对象

头文件还提供了 4 个辅助函数：

```c
int32_t ZgnGetLoadedWorldMonsterCount(
    const ZgnHostApi* host_api);

int32_t ZgnGetLoadedWorldMonsterInfo(
    const ZgnHostApi* host_api,
    int32_t index,
    ZgnWorldMonsterInfo* out_info);

int32_t ZgnGetNearestWorldMonsterInfo(
    const ZgnHostApi* host_api,
    ZgnWorldMonsterInfo* out_info);

int32_t ZgnGetWorldMonsterUniqueNameUtf8(
    const ZgnHostApi* host_api,
    void* map_npc_def,
    char* buffer,
    uint32_t buffer_size);
```

示例：

```c
ZgnWorldMonsterInfo nearest = {};
char unique_name[256] = {0};

if (g_host && ZgnGetNearestWorldMonsterInfo(g_host, &nearest))
{
    if (nearest.map_npc_def)
    {
        ZgnGetWorldMonsterUniqueNameUtf8(
            g_host,
            nearest.map_npc_def,
            unique_name,
            (uint32_t)sizeof(unique_name));
    }

    if (g_host->log)
    {
        char message[512];
        snprintf(
            message,
            sizeof(message),
            "nearest monster runtime=%u npc=%u stage=%u danger=%d pos=(%.2f, %.2f, %.2f) unique=%s",
            nearest.runtime_id,
            nearest.npc_id,
            nearest.stage_id,
            nearest.is_danger_monster,
            nearest.position.x,
            nearest.position.y,
            nearest.position.z,
            unique_name[0] ? unique_name : "(empty)");
        g_host->log("WorldMonsterDemo", message);
    }
}
```

批量遍历示例：

```c
const int32_t count = ZgnGetLoadedWorldMonsterCount(g_host);
for (int32_t i = 0; i < count; ++i)
{
    ZgnWorldMonsterInfo info = {};
    if (!ZgnGetLoadedWorldMonsterInfo(g_host, i, &info))
    {
        continue;
    }

    /* 在这里处理每一只已加载怪物 */
}
```

### 8.11 已加载宝箱基础信息只读查询

这组接口是把 `SDK.md` 里“已加载宝箱 Prop 信息查找”那条读取链，包装成了宿主可直接调用的只读 Host API。

当前目标很克制，只做 get：

- 获取当前已经加载进场景的宝箱数量
- 获取指定下标宝箱的名字、ID、坐标和基础状态
- 获取离本地玩家最近的一个已加载宝箱
- 不触发找宝箱、不模拟交互、不开箱、不修改状态

签名：

```c
typedef struct ZgnTreasureChestInfo
{
    void* entity;
    void* prop_component;
    void* map_prop_def;
    uint32_t runtime_id;
    uint32_t server_entity_id;
    uint32_t group_id;
    uint32_t group_entity_id;
    uint32_t prop_server_entity_id;
    uint32_t interaction_runtime_id;
    uint32_t map_group_id;
    uint32_t map_instance_id;
    uint32_t map_server_entity_id;
    uint32_t prop_id;
    uint32_t chest_id;
    uint32_t mapping_info_id;
    int32_t prop_type;
    int32_t prop_state;
    int32_t is_loaded;
    int32_t is_on_stage;
    int32_t is_visible;
    int32_t is_opened;
    ZgnVector3 position;
    ZgnVector3 config_position;
    char name[128];
    char prop_name[128];
    char map_name[128];
    char icon_path[128];
} ZgnTreasureChestInfo;

int32_t (*get_loaded_treasure_chest_count)(void);
int32_t (*get_loaded_treasure_chest_info)(int32_t index, ZgnTreasureChestInfo* out_info);
int32_t (*get_nearest_treasure_chest_info)(ZgnTreasureChestInfo* out_info);
int32_t (*get_treasure_chest_text_name_utf8)(void* prop_component, char* buffer, uint32_t buffer_size);
```

字段说明：

- `entity`：对应的 `RPG::GameCore::GameEntity*`
- `prop_component`：对应的 `RPG::GameCore::PropComponent*`
- `map_prop_def`：对应的 `RPG::Client::MapPropDef*`
- `runtime_id / server_entity_id / group_id / group_entity_id`：实体基础 ID
- `prop_server_entity_id / interaction_runtime_id`：Prop 和交互层相关 ID
- `map_group_id / map_instance_id / map_server_entity_id`：地图配置层 ID
- `prop_id / chest_id / mapping_info_id`：宝箱和地图定位相关 ID
- `prop_type / prop_state`：底层枚举值，直接按整数透出
- `is_loaded / is_on_stage / is_visible`：当前实体状态
- `is_opened`：宿主尽量根据 `PropState::ChestUsed`、当前地图已开启列表和 `FindChestModule` 判断，成功时为 `1`
- `position`：当前场景里的实时坐标，优先取实体 `UnityGO->transform.position`
- `config_position`：地图配置坐标，来自 `MapPropDef::get_Position()`
- `name`：优先使用 `PropComponent::get_PropTextIDName()` 通过 `TextmapStatic::GetText(...)` 解析出的当前语言显示名，中文环境下通常就是中文名；拿不到时回退到 Prop 名字，再回退到地图配置名字
- `prop_name / map_name / icon_path`：Prop 名、地图配置名和图标路径

返回值约定：

- `get_loaded_treasure_chest_count`：返回当前已加载宝箱数量，失败返回 `0`
- `get_loaded_treasure_chest_info`：成功返回 `1`，失败返回 `0`
- `get_nearest_treasure_chest_info`：成功返回 `1`，失败返回 `0`
- `get_treasure_chest_text_name_utf8`：传入 `out_info.prop_component`，把 `PropTextIDName` 解析成 UTF-8 当前语言文本，成功返回 `1`

当前实现边界：

- 只统计“当前已经加载到场景里的宝箱 Prop”，不会扫描全地图离线配置
- 离宝箱太远、宝箱所在 group 未加载时，接口可能查不到它
- 这是只读接口，不调用 `TriggerFindChest`、不调用交互按钮、不打开宝箱
- `is_opened` 是宿主基于当前可读状态做的保守判断，实际奖励状态仍以游戏服务器为准

线程要求：

- 这组接口只能在游戏线程回调里用，也就是 `OnLoad / OnUpdate / OnUnload`
- 不建议在 `OnMenu / OnOverlay` 里直接调用，因为它们会触碰游戏内 managed 对象

头文件还提供了 3 个辅助函数：

```c
int32_t ZgnGetLoadedTreasureChestCount(
    const ZgnHostApi* host_api);

int32_t ZgnGetLoadedTreasureChestInfo(
    const ZgnHostApi* host_api,
    int32_t index,
    ZgnTreasureChestInfo* out_info);

int32_t ZgnGetNearestTreasureChestInfo(
    const ZgnHostApi* host_api,
    ZgnTreasureChestInfo* out_info);

int32_t ZgnGetTreasureChestTextNameUtf8(
    const ZgnHostApi* host_api,
    void* prop_component,
    char* buffer,
    uint32_t buffer_size);
```

最近宝箱示例：

```c
ZgnTreasureChestInfo chest = {};

if (g_host && ZgnGetNearestTreasureChestInfo(g_host, &chest))
{
    char text_name[128] = {0};
    ZgnGetTreasureChestTextNameUtf8(
        g_host,
        chest.prop_component,
        text_name,
        (uint32_t)sizeof(text_name));

    char message[512];
    snprintf(
        message,
        sizeof(message),
        "nearest chest name=%s text_name=%s chest_id=%u prop_id=%u map=(%u,%u) opened=%d pos=(%.2f, %.2f, %.2f)",
        chest.name[0] ? chest.name : "(empty)",
        text_name[0] ? text_name : "(empty)",
        chest.chest_id,
        chest.prop_id,
        chest.map_group_id,
        chest.map_instance_id,
        chest.is_opened,
        chest.position.x,
        chest.position.y,
        chest.position.z);

    if (g_host->log)
    {
        g_host->log("TreasureChestDemo", message);
    }
}
```

批量遍历示例：

```c
const int32_t count = ZgnGetLoadedTreasureChestCount(g_host);
for (int32_t i = 0; i < count; ++i)
{
    ZgnTreasureChestInfo chest = {};
    if (!ZgnGetLoadedTreasureChestInfo(g_host, i, &chest))
    {
        continue;
    }

    /* 在这里读取每一个已加载宝箱的名字、ID、坐标和基础状态 */
}
```

### 8.12 原生提示、系统时间与宿主动作

```c
int32_t (*show_hint_utf8)(const char* message);
int32_t (*show_sdk_code_dialog)(int32_t retcode);
int32_t (*show_ok_cancel_hint_utf8)(const char* message);
int32_t (*show_custom_ok_cancel_hint_utf8)(const char* content, const char* title, const char* ok_title, const char* cancel_title);
int32_t (*show_custom_ok_cancel_hint_event_utf8)(const char* content, const char* title, const char* ok_title, const char* cancel_title);
int32_t (*consume_confirm_dialog_event)(int32_t* out_request_id, int32_t* out_result);
int32_t (*show_plugin_confirm_utf8)(const char* content, const char* ok_title, const char* cancel_title);
int32_t (*show_joypad_confirm_utf8)(const char* content, const char* accept_title, const char* cancel_title);
int32_t (*get_system_datetime_utf8)(char* buffer, uint32_t buffer_size);
int32_t (*quit_game)(void);
int32_t (*logout_to_login)(void);
int32_t (*open_url_in_default_browser_utf8)(const char* url);
```

说明：

- `show_hint_utf8`：显示游戏原生提示窗。
- `show_sdk_code_dialog`：显示 SDK Code 对话框。
- `show_ok_cancel_hint_utf8`：显示 RPG 风格确认/取消框。
- `show_custom_ok_cancel_hint_utf8`：显示 RPG 风格确认/取消框，并允许自定义标题、确认按钮文字、取消按钮文字。
- `show_custom_ok_cancel_hint_event_utf8`：显示同样的 RPG 风格确认/取消框，但会返回一个 `request_id`，后续可以单独拿到确认/取消事件。
- `consume_confirm_dialog_event`：消费一个已经完成的确认框事件，拿到对应 `request_id` 和结果。
- `show_plugin_confirm_utf8`：显示 MiHoYo SDK Confirm。
- `show_joypad_confirm_utf8`：显示 Joypad Confirm。
- `get_system_datetime_utf8`：读取当前系统本地时间，格式是 `YYYY-MM-DD HH:MM:SS`。
- `quit_game`：直接退出游戏。
- `logout_to_login`：退出登录并返回登录页面。
- `open_url_in_default_browser_utf8`：用系统当前默认浏览器打开指定网址。

当前实现补充：

- `show_plugin_confirm_utf8` 会先尝试走 `MiHoYo::SDK::Confirm::ShowPluginUI(...)`。
- 如果插件式 UI 不可用，宿主会自动 fallback 到普通 `Confirm::Show(...)`。
- `show_custom_ok_cancel_hint_utf8` 底层仍走 RPG 原生 ConfirmDialog，只是宿主会在弹出后改写标题和两个按钮文字。
- `show_custom_ok_cancel_hint_event_utf8` 底层同样走 RPG 原生 ConfirmDialog，但宿主会跟踪这次弹窗的 `_ClickType`，因此可以区分：
- `confirm`
- `cancel`
- `close`
- `background`
- 如果弹窗创建失败或运行期异常，事件结果会是 `faulted`，不会让插件一直卡在等待状态。
- `get_system_datetime_utf8` 读取的是宿主机器当前本地时间，不依赖游戏服务器时间。
- `quit_game` 优先走 `RPGApplication::QuitApp(false)`，如果应用实例不可用，会 fallback 到 `UnityEngine::Application::Quit()`。
- `logout_to_login` 当前会优先尝试 `SwitchRole()` 这类退登入口；只要命中任意退登链路，宿主就会在约 `0.5` 秒后自动退出游戏。
- `open_url_in_default_browser_utf8` 当前走 `UnityEngine::Application::OpenURL(...)`，桌面平台下会交给系统默认浏览器处理。
- 曾经短暂提供过原生输入框弹窗 API，但由于当前宿主环境无法稳定拿到 `MiHoYo::SDK::ConsoleDelegate::StaticGet_ShowTextEntry()` delegate，这组 API 已从正式开发文档中撤销，不再推荐使用。

推荐调用方式：

头文件已经提供了一个更顺手的辅助函数，推荐优先用它：

```c
int32_t ZgnShowCustomOkCancelHintUtf8(
    const ZgnHostApi* host_api,
    const char* title,
    const char* content,
    const char* cancel_title,
    const char* confirm_title);

int32_t ZgnShowCustomOkCancelHintEventUtf8(
    const ZgnHostApi* host_api,
    const char* title,
    const char* content,
    const char* cancel_title,
    const char* confirm_title);

int32_t ZgnConsumeConfirmDialogEvent(
    const ZgnHostApi* host_api,
    int32_t* out_request_id,
    int32_t* out_result);

int32_t ZgnGetSystemDateTimeUtf8(
    const ZgnHostApi* host_api,
    char* buffer,
    uint32_t buffer_size);

int32_t ZgnQuitGame(
    const ZgnHostApi* host_api);

int32_t ZgnLogoutToLogin(
    const ZgnHostApi* host_api);

int32_t ZgnOpenUrlInDefaultBrowserUtf8(
    const ZgnHostApi* host_api,
    const char* url);
```

这个辅助函数的参数顺序是：

- `title`
- `content`
- `cancel`
- `confirm`

事件结果常量：

```c
ZGN_CONFIRM_DIALOG_RESULT_CONFIRM
ZGN_CONFIRM_DIALOG_RESULT_CANCEL
ZGN_CONFIRM_DIALOG_RESULT_CLOSE
ZGN_CONFIRM_DIALOG_RESULT_BACKGROUND
ZGN_CONFIRM_DIALOG_RESULT_FAULTED
```

调用建议：

- 在 `OnMenu` 或 `OnOverlay` 里可以直接调用这些接口。
- 传空字符串会失败。
- 普通确认框 `content` 里换行请用 `\n`。
- 当前宿主会为普通确认框这类文本开启 rich text。
- `content` 当前支持的推荐格式包括：
- 颜色：`<color=#FFFFFF>白色文字</color>`
- 粗体：`<b>粗体</b>`
- 斜体：`<i>斜体</i>`
- 下划线：`<u>下划线</u>`
- 删除线：`<s>删除线</s>`
- 上面这些格式可以混用，也可以和 `\n` 一起使用。
- 标题或按钮文字为空时，宿主会保留默认文案。
- 这些富文本标签主要建议放在 `content` 里使用。
- 如果你只想“弹一个框”，继续用 `show_custom_ok_cancel_hint_utf8` 或 `ZgnShowCustomOkCancelHintUtf8(...)` 就行。
- 如果你还想单独处理“点了确认”还是“点了取消”，请改用 `show_custom_ok_cancel_hint_event_utf8` 或 `ZgnShowCustomOkCancelHintEventUtf8(...)`，然后在 `OnUpdate` 里轮询 `consume_confirm_dialog_event`。
- `quit_game`、`logout_to_login`、`open_url_in_default_browser_utf8` 当前也会由宿主排队回游戏线程执行，所以可以从 `OnMenu / OnOverlay` 的按钮回调里直接调用。
- `open_url_in_default_browser_utf8` 建议传完整网址，例如 `https://example.com`。

示例：

```c
if (g_host)
{
    ZgnShowCustomOkCancelHintUtf8(
        g_host,
        "我是标题",
        "第一行内容\n<b>我是粗体</b> <i>我是斜体</i>\n<u>我是下划线</u> <s>我是删除线</s>\n<color=#FFFFFF>我是白色</color>",
        "我是取消",
        "我是确认");
}
```

系统时间、退出游戏、退出登录与打开网址示例：

```c
if (g_host)
{
    char datetime[64] = { 0 };
    if (ZgnGetSystemDateTimeUtf8(g_host, datetime, (uint32_t)sizeof(datetime)))
    {
        g_host->show_hint_utf8(datetime);
    }

    ZgnOpenUrlInDefaultBrowserUtf8(g_host, "https://www.mihoyo.com");

    /* 下面两个接口一旦调用就会立即开始执行，请谨慎放到真正需要的按钮里。 */
    /* ZgnLogoutToLogin(g_host); */
    /* ZgnQuitGame(g_host); */
}
```

带结果事件的示例：

```c
static int32_t g_confirm_request_id = 0;

if (g_host && g_confirm_request_id == 0)
{
    g_confirm_request_id = ZgnShowCustomOkCancelHintEventUtf8(
        g_host,
        "我是标题",
        "第一行内容\n<color=#FFFFFF>我是白色</color>\n第三行内容",
        "我是取消",
        "我是确认");
}

if (g_host)
{
    int32_t request_id = 0;
    int32_t result = 0;
    while (ZgnConsumeConfirmDialogEvent(g_host, &request_id, &result))
    {
        if (request_id != g_confirm_request_id)
        {
            continue;
        }

        if (result == ZGN_CONFIRM_DIALOG_RESULT_CONFIRM)
        {
            g_host->show_hint_utf8("你点了确认");
        }
        else if (result == ZGN_CONFIRM_DIALOG_RESULT_CANCEL)
        {
            g_host->show_hint_utf8("你点了取消");
        }

        g_confirm_request_id = 0;
    }
}
```

### 8.13 HTTP 端口授权

```c
int32_t (*get_http_auth_machine_code_utf8)(char* buffer, uint32_t buffer_size);
int32_t (*authorize_http_port_utf8)(const char* plugin_name, const char* auth_key, const char* bind_address, uint16_t port);
```

辅助函数：

```c
int32_t ZgnGetHttpAuthMachineCodeUtf8(
    const ZgnHostApi* host_api,
    char* buffer,
    uint32_t buffer_size);

int32_t ZgnAuthorizeHttpPortUtf8(
    const ZgnHostApi* host_api,
    const char* plugin_name,
    const char* auth_key,
    const char* bind_address,
    uint16_t port);
```

用途：

- `get_http_auth_machine_code_utf8`：读取当前机器码，通常只用于调试或展示授权错误。
- `authorize_http_port_utf8`：在插件准备开放本地 HTTP / TCP 监听端口前做授权校验。
- 校验成功返回 `1`，失败返回 `0`。
- 失败时宿主会写英文日志，日志里会包含当前机器码和授权邮箱。

源码插件如果包含明显网络监听代码，建议在文件头部同时写入授权 key：

```c
/*
@zgn_name MyHttpPlugin
@zgn_author You
@zgn_version 1.0.0
@zgn_description Local HTTP server demo
@zgn_http_auth ZGN00-...
*/
```

运行时也建议在真正 `bind / listen` 前主动调用授权接口：

```c
#define MY_HTTP_AUTH_KEY "ZGN00-..."

static int32_t StartHttpServer(void)
{
    if (!ZgnAuthorizeHttpPortUtf8(g_host, "MyHttpPlugin", MY_HTTP_AUTH_KEY, "127.0.0.1", 25565))
    {
        return 0;
    }

    /* 授权通过后，再启动工作线程并执行 WSAStartup / socket / bind / listen。 */
    return 1;
}
```

授权生成流程：

- 用户加载 HTTP 插件但没有授权时，日志会出现类似：

```text
HTTP port authorization is required. Your machine code: ZGN-MC-.... Please send your machine code to main@neikos496.xyz to request HTTP port permission.
```

- 开发者在项目根目录运行 `generate_http_port_auth.py`。
- 粘贴用户发来的机器码。
- 脚本会输出 `ZGN00-...` 授权 key 和一行可直接复制到源码头部的 `@zgn_http_auth ZGN00-...`。

当前授权格式：

- 魔术头：`ZGN00`
- 用途绑定：`HTTP_PORT`
- 机器绑定：当前机器码
- 包装方式：自定义 16 轮 ROX 混淆 + `AES-128-CBC`

安全说明：

- 这套授权会阻止普通源码插件在没有授权 key 时开放本地监听端口。
- 源码插件会在加载前做静态扫描；扫描到明显网络监听代码但没有有效 `@zgn_http_auth` 时会直接拒绝加载。
- 运行期也应主动调用 `ZgnAuthorizeHttpPortUtf8(...)`，不要把端口开放逻辑写在授权之前。
- 如果你允许其他程序通过端口调用插件，还必须自行实现 TLS / HTTPS 或消息级加密认证；HTTP 端口授权只负责“允许这个插件在这台机器上开端口”，不等于业务请求已经安全。
- 恶意手写 DLL 如果完全绕过 Host API 并直接调用 WinSock，仍需要未来做全局 WinSock hook 才能硬拦截；当前重点保护的是源码插件和按规范调用 API 的插件。

### 8.14 插件 UI（ImGui 封装）

```c
int32_t (*ui_begin_window_utf8)(const char* title, int32_t* open, float width, float height);
void (*ui_end_window)(void);
void (*ui_text_utf8)(const char* text);
int32_t (*ui_button_utf8)(const char* label, float width, float height);
void (*ui_same_line)(void);
int32_t (*ui_input_text_utf8)(const char* label, char* buffer, uint32_t buffer_size, float width, float height);
```

#### `ui_begin_window_utf8`

- `title` 不能为空。
- `open` 可为 `NULL`；如果不为 `NULL`，会同步窗口开关状态。
- `width <= 0` 时默认首开宽度回退到 `420`。
- `height <= 0` 时默认首开高度回退到 `280`。
- 返回 `1` 表示当前可以绘制窗口内容。
- 返回 `0` 通常表示窗口折叠，但窗口仍然已经 Begin 了。

非常重要：

- 只要调用了 `ui_begin_window_utf8`，就必须调用一次 `ui_end_window()`。
- 即使 `ui_begin_window_utf8` 返回 `0`，也仍然要 `ui_end_window()`。

推荐模式：

```c
if (g_host->ui_begin_window_utf8("MyWindow", &g_open, 600.0f, 400.0f))
{
    g_host->ui_text_utf8("hello");
}
g_host->ui_end_window();
```

#### `ui_text_utf8`

- 文本自动换行。
- 传 `NULL` 也不会崩，等价于空字符串。

#### `ui_button_utf8`

- 返回 `1` 表示这一帧按钮被点击。
- `width <= 0` 或 `height <= 0` 时，对应方向使用 ImGui 默认尺寸。

#### `ui_same_line`

- 让下一个控件继续画在同一行。

#### `ui_input_text_utf8`

- `buffer` 不能为空。
- `buffer_size` 必须大于 `0`。
- 返回 `1` 表示内容发生变化。
- `height > 0` 时使用多行输入框。
- `height <= 0` 时使用单行输入框。
- `width <= 0` 时默认铺满当前可用区域宽度。

### 8.15 安全验证

```c
int32_t (*validate_object)(void* object, const char* namespaze, const char* class_name, const char* source);
```

用途：

- 校验一个对象是否真的是你以为的类型。
- 成功返回 `1`，失败返回 `0`。

参数说明：

- `object`：待检查对象。
- `namespaze`：命名空间字符串，可以为空。
- `class_name`：类名字符串，可以为空。
- `source`：调试来源描述，会进日志。

适用场景：

- 你从其他 API 拿到一个 `void*` 后，想做保守校验。
- 你在高级插件里直连 `unitysdk`，想先确认对象类型。

### 8.16 原始桥接

```c
uintptr_t game_assembly_base;
uintptr_t il2cpp_module_base;
void* (*il2cpp_class_from_type_definition_index)(uint32_t type_definition_index);
```

说明：

- `game_assembly_base`：`GameAssembly.dll` 基址。
- `il2cpp_module_base`：`hIl2Cpp` 基址。
- `il2cpp_class_from_type_definition_index`：通过 type definition index 取 `Il2CppClass*`。

这部分属于高级接口。

建议：

- 只有在 Host API 明显不够用时再碰。
- 纯 C 插件的第一版尽量不要依赖这些字段。

## 9. 源码插件自动编译细节

当前宿主会按下面顺序为 `.c` 插件寻找编译器：

- 先找 `resource\plugin_sdk\zig\zig.exe`
- 如果系统 `PATH` 里已经有 `zig.exe`，会直接复用
- 如果没找到可用 Zig，再回退到 Visual Studio Build Tools
- 如果上面都没有，宿主会尝试自动联网下载 Zig 到 `resource\plugin_sdk\zig`
- 如果自动下载失败，`resource\plugins\_compiled\*.build.log` 会写出官方下载地址、目标放置目录和预期的 `zig.exe` 路径

实际编译参数的关键点如下：

- Zig 路线默认走：`zig cc -shared -target x86_64-windows-gnu`
- VS 路线会通过 `vcvars64.bat` 初始化环境
- 强制按 `C` 编译：`/TC`
- 强制 UTF-8：`/utf-8`
- 定义 `ZGN_PLUGIN_EXPORTS`
- 默认包含这些目录：

```text
项目根目录
项目根目录\include\global
当前插件目录
```

- 默认链接：
- `user32.lib`
- `ws2_32.lib`
- `bcrypt.lib`
- `crypt32.lib`
- `secur32.lib`

这意味着：

- 源码插件可以直接 `#include "zgn_plugin_api.h"`。
- 也可以在高级玩法里包含 `include\global\unitysdk` 下的头。
- 如果你 `#include <windows.h>`，像 `GetAsyncKeyState` 这类常用 Win32 API 默认也能用。
- 如果你 `#include <winsock2.h>` / `#include <ws2tcpip.h>`，源码插件现在也可以直接写本地 TCP / HTTP 监听服务。
- 如果你要做 TLS / HTTPS 或消息级加密，源码插件现在也可以直接使用：
- `SChannel`：依赖 `secur32.lib`
- `CryptoAPI`：依赖 `crypt32.lib`
- `Windows CNG`：依赖 `bcrypt.lib`
- 因为是 `/TC`，所以不要在 `.c` 插件里写 C++ 语法。
- 默认发布包不会内置 Zig；只有在目标机器第一次需要编译源码插件时，才会尝试自动下载。

如果你准备在插件里起本地 HTTP 服务，请额外遵守：

- 监听逻辑必须放在工作线程，不要放在主线程回调里
- `OnUnload` 时一定要停线程并关闭监听 socket
- 推荐先只做 `127.0.0.1` 本地开发端口
- 如果允许其他程序通过端口调用插件，必须走安全加密通道，不要暴露明文 HTTP

## 10. 什么时候该用纯 C，什么时候该用 C++

### 10.1 适合纯 C 插件的场景

- 做简单状态机
- 读写玩家坐标
- 写独立悬浮窗
- 写开关按钮和输入框
- 调宿主原生弹窗
- 做轻量级快捷键工具

### 10.2 适合 DLL + C++ 插件的场景

- 你要大量直连 `unitysdk`
- 你要写复杂对象包装
- 你需要更丰富的工程结构
- 你已经能自己控制编译链和依赖

建议是：

- 第一版先用纯 C + Host API 打通功能。
- 只有 Host API 不够时，再升级到 C++ 和高级 SDK 直连。

## 11. 实战示例

### 11.1 一个简单的 Overlay 窗口

```c
#include "zgn_plugin_api.h"
#include <windows.h>

static const ZgnHostApi* g_host = 0;
static int32_t g_open = 0;
static int g_b_was_down = 0;

ZGN_PLUGIN_EXPORT uint32_t ZGN_PLUGIN_CALL ZgnPlugin_GetAbiVersion(void)
{
    return ZGN_PLUGIN_ABI_VERSION;
}

ZGN_PLUGIN_EXPORT const ZgnPluginInfo* ZGN_PLUGIN_CALL ZgnPlugin_GetInfo(void)
{
    static const ZgnPluginInfo info = {
        "OverlayDemo",
        "Demo",
        "1.0.0",
        "按 B 打开窗口",
        ZGN_PLUGIN_ABI_VERSION
    };
    return &info;
}

ZGN_PLUGIN_EXPORT int32_t ZGN_PLUGIN_CALL ZgnPlugin_OnLoad(const ZgnHostApi* host_api)
{
    g_host = host_api;
    return 1;
}

ZGN_PLUGIN_EXPORT void ZGN_PLUGIN_CALL ZgnPlugin_OnUpdate(void)
{
    const int down = (GetAsyncKeyState('B') & 0x8000) != 0;
    if (down && !g_b_was_down)
    {
        g_open = !g_open;
    }
    g_b_was_down = down;
}

ZGN_PLUGIN_EXPORT void ZGN_PLUGIN_CALL ZgnPlugin_OnOverlay(void)
{
    if (!g_host || !g_open || !g_host->ui_begin_window_utf8 || !g_host->ui_end_window)
    {
        return;
    }

    if (g_host->ui_begin_window_utf8("OverlayDemo", &g_open, 520.0f, 320.0f))
    {
        g_host->ui_text_utf8("这是一个最简单的悬浮窗");
        if (g_host->ui_button_utf8 && g_host->ui_button_utf8("弹提示", 120.0f, 32.0f))
        {
            if (g_host->show_hint_utf8)
            {
                g_host->show_hint_utf8("按钮已点击");
            }
        }
    }
    g_host->ui_end_window();
}

ZGN_PLUGIN_EXPORT void ZGN_PLUGIN_CALL ZgnPlugin_OnUnload(void)
{
    g_open = 0;
    g_host = 0;
}
```

### 11.2 示例插件与构建脚本

仓库里有参考目录：

- 示例源码：`<PROJECT_ROOT>\plugin_sdk\sample_plugin\zgn_sample_plugin.c`
- 示例构建脚本：`<PROJECT_ROOT>\plugin_sdk\sample_plugin\build_debug.bat`

## 12. 调试与排错

### 12.1 源码插件编译失败

先看：

- `resource\plugins\_compiled\*.build.log`

常见原因：

- 写了 C++ 语法
- 头文件路径不对
- 字符串引号没闭合
- 漏了某个导出函数
- 忘了包含 `zgn_plugin_api.h`

### 12.2 插件能编译，但加载失败

先看：

- `x64\Debug\Log\plugin_loader.log`

重点检查：

- 是否缺少必需导出
- ABI 是否匹配
- `OnLoad` 是否返回了 `0`
- `OnLoad` 是否抛异常

### 12.3 插件运行中突然 Faulted

这通常说明：

- `OnUpdate`
- `OnMenu`
- `OnOverlay`
- `OnUnload`

中的某个回调抛异常了。

处理方式：

- 看 `plugin_loader.log`
- 看你自己通过 `host_api->log()` 打的日志
- 先缩小最近一次新增逻辑

### 12.4 原生弹窗没出现

建议顺序：

1. 确认对应函数指针不为空。
2. 确认传入文本不是空字符串。
3. 看 `plugin_loader.log`。
4. 对 `show_plugin_confirm_utf8`，现在宿主会记录插件式 Confirm 是否成功，以及是否启用了 fallback。

### 12.5 世界对象为空

这通常发生在：

- 切场景
- 刚进游戏
- 非交互阶段

先判断：

```c
if (g_host->is_interactive_world_ready && g_host->is_interactive_world_ready())
{
    /* 再继续读玩家/坐标 */
}
```

## 13. 最佳实践

- 第一版插件优先只用 `ZgnHostApi`。
- `OnUpdate` 保持轻量，不要做大扫描。
- `OnMenu / OnOverlay` 只做纯 UI。
- 访问高风险对象前先判空，必要时用 `validate_object`。
- 用静态全局变量保存状态，不要把状态散落在很多地方。
- 每个关键动作都写日志，尤其是加载、按钮点击、失败分支。
- 对所有函数指针做判空，不要假设新接口在旧宿主一定存在。

## 14. 高级扩展说明

如果你准备直连 `unitysdk`：

- 仓库里已经提供 `include\global\unitysdk`
- 源码插件编译时也已经默认加了该 include 路径
- 但你要自己承担更多版本耦合和线程风险

建议把高级扩展分两层：

- 安全层：优先通过 `ZgnHostApi` 暴露的稳定能力完成
- 高级层：确实不够时，再通过 `unitysdk` 做精确补充

如果你只是想快速做插件，而不是研究底层对象模型，请停在 Host API 这一层。
