Skip to content

Plugin Development Guide ZH

Lafko edited this page Apr 9, 2026 · 14 revisions

← Home


插件开发指南

1. 快速入门

先决条件

  • MSVC v143 (Visual Studio 2022)
  • C++20 (/std:c++20)
  • x64 Release 构建
  • 运行时库: /MD (Multi-threaded DLL) — 必须与宿主匹配

项目设置

  1. 在 Visual Studio 中创建一个新的 C++ DLL 项目
  2. 将包含路径设置为指向 POEFixer 源目录(用于 SDK 和 ImGui 头文件)
  3. 将 ImGui 源文件添加到项目中: imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp
  4. 在插件源代码中包含 Plugin SDK 头文件:
    #include "plugin_sdk/PluginAPI.h"
    #include "plugin_sdk/PluginContext.h"
    #include "imgui/imgui.h"
    或使用 ExamplePlugin 的便捷头文件:
    #include "sdk/PluginHelpers.h"  // Includes all SDK headers + MemoryReader + utilities
  5. 在项目的预处理器定义中定义 PLUGIN_EXPORTS_CRT_SECURE_NO_WARNINGS

文件夹结构

Plugins/
  YourPlugin/
    YourPlugin.dll      <-- DLL 名称必须与文件夹名称匹配
    config/
      settings.txt      <-- 可选的设置文件
    data/
      ...               <-- 可选的数据目录(数据库、缓存等)

ExamplePlugin 项目结构

ExamplePlugin 展示了推荐的项目布局:

Plugins/ExamplePlugin/
  ExamplePlugin.cpp        <-- 主插件入口点 + 工厂导出
  sdk/
    PluginHelpers.h        <-- MemoryReader、WideToNarrow、实体/稀有度辅助函数
  examples/
    ExampleBuffs.h         <-- 带过滤和进度条的增益列表
    ExampleEntities.h      <-- 带监视机制、组件树、JSON 导出的实体调试列表
    ExampleInventory.h     <-- ServerData、背包选择器、格子网格、带稀有度的物品词缀
    ExampleMemory.h        <-- Hex 查看器、Read<T> 演示、模式扫描器
    ExampleUiExplorer.h    <-- 带搜索、导航、高亮的完整 UI 元素浏览器

KillCount 插件结构

包含 SQLite3、图标图集和覆盖层渲染的更完整插件示例:

Plugins/KillCount/
  KillCount.cpp            <-- 主插件入口点、IPlugin 生命周期、设置 UI
  KillCount.h              <-- 插件类声明
  KillTracker.cpp/h        <-- 击杀/宝箱/死亡计数引擎
  OverlayRenderer.cpp/h    <-- 带拖拽重定位模式的 ImGui 覆盖层
  IconAtlas.cpp/h           <-- 精灵图纹理加载(D3D11 + stb_image)
  Database.cpp/h           <-- 持久化统计的 SQLite3 封装
  DisplaySettings.h        <-- 设置结构体
  sdk/
    PluginHelpers.h        <-- 从 ExamplePlugin 复制
  lib/
    sqlite3.c/h            <-- SQLite3 合并文件(作为 C 编译)
    sqlite3-vcpkg-config.h <-- 静态链接的本地覆盖

命名约定

DLL 文件名必须与文件夹名称完全匹配:

  • 文件夹: Plugins/MyPlugin/ → DLL: MyPlugin.dll
  • 宿主扫描 Plugins/ 中的每个子文件夹,查找 <FolderName>.dll

2. 插件生命周期

Load DLL (LoadLibrary)
  → CreatePlugin()           -- 工厂: 实例化 IPlugin
  → SetContext(ctx)           -- 接收宿主服务
  → SetPluginDirectory(dir)   -- 接收文件夹路径
  → GetSDKVersion()           -- 兼容性检查
  → GetName()                 -- UI 显示名称
  → [if enabled] OnEnable()  -- 初始化资源
  ↓
  Main Loop (every frame):
    → DrawUI()                -- 渲染覆盖层(仅在启用时)
    → DrawSettings()          -- 在 Plugins 选项卡中渲染设置
    → WantsOverlay()          -- 宿主检查插件是否需要覆盖层模式
  ↓
  Periodically / on shutdown:
    → SaveSettings()          -- 持久化设置
  ↓
  → OnDisable()               -- 清理资源
  → DestroyPlugin(plugin)     -- 工厂: 删除 IPlugin
  → FreeLibrary               -- 卸载 DLL

线程

  • 所有 Draw* 方法在主/渲染线程上调用
  • GetSnapshot() 和其他 PluginContext 函数是线程安全
  • 不要创建调用 ImGui 的线程 — ImGui 不是线程安全的

3. IPlugin 接口参考

每个插件必须实现 IPlugin 接口(定义在 plugin_sdk/PluginAPI.h 中):

void SetPluginDirectory(const char* dir)

  • 调用时机: 创建后立即调用一次
  • 参数: 相对路径,如 "Plugins/YourPlugin"
  • 用途: 保存此路径以加载设置/资源

void SetContext(PluginContext* context)

  • 调用时机: SetPluginDirectory 之后调用一次
  • 参数: 宿主的 PluginContext 指针(在插件生命周期内有效)
  • 用途: 保存此指针 — 它是您访问所有游戏数据的入口
  • 重要: 在此处调用 ImGui::SetCurrentContext(ctx->ImGuiContext)

void OnEnable(bool isGameOpened)

  • 调用时机: 当用户启用插件时,或如果之前已启用则在启动时
  • 参数: 如果游戏进程当前已连接则为 true
  • 用途: 加载设置、分配资源、初始化状态

void OnDisable()

  • 调用时机: 当用户禁用插件时
  • 用途: 释放资源、停止后台工作

void DrawUI()

  • 调用时机: 每帧,仅在插件启用时
  • 用途: 使用 ImGui 渲染覆盖层
  • 注意: 使用唯一的窗口 ID(如 "MyWindow##MyPlugin")以避免冲突

void DrawSettings()

  • 调用时机: 每帧,在 Plugins 设置选项卡中(仅在启用时)
  • 用途: 使用 ImGui 渲染插件配置

void SaveSettings()

  • 调用时机: 定期调用,以及在应用程序关闭时
  • 用途: 将设置保存到磁盘(例如 Plugins/YourPlugin/config/settings.txt

const char* GetName()

  • 返回值: Plugins 选项卡中显示的名称(例如 "My Plugin"

int GetSDKVersion()

  • 返回值: PLUGIN_SDK_VERSION(当前为 4)
  • 用途: 宿主检查兼容性 — 必须匹配

bool WantsOverlay() (SDK v2)

  • 返回值: 如果插件想在覆盖层模式(游戏上方的透明覆盖层)中渲染则为 true
  • 默认值: false — 插件仅在普通设置窗口中渲染
  • 用途: 当任何插件返回 true 时,即使没有内置功能需要,宿主也会进入覆盖层模式

工厂导出

您的 DLL 必须导出这两个 C 函数:

extern "C" PLUGIN_API IPlugin* CreatePlugin() {
    return new MyPlugin();
}

extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
    delete plugin;
}

4. PluginContext API 参考

PluginContext 结构体(定义在 plugin_sdk/PluginContext.h 中)提供用于访问游戏数据的函数指针。所有类型位于 PluginSDK 命名空间中。

游戏数据访问

GetSnapshot()shared_ptr<const PluginGameSnapshot>

返回游戏状态的完整快照。每帧更新一次。包含:

字段 类型 说明
CurrentState GameStateTypes 当前游戏状态
CurrentAreaName string 区域名称(例如 "The Riverways")
CurrentAreaHash string 唯一区域实例哈希
CurrentAreaLevel uint8_t 当前区域的怪物等级
IsTown bool 在城镇中为 true
IsHideout bool 在藏身处中为 true
IsPaused bool 游戏暂停时为 true
IsSkillTreeVisible bool 技能树面板打开时为 true
WorldToGridConvertor float 世界→网格转换系数
Player RadarEntity 本地玩家实体数据
Entities vector<RadarEntity> 所有附近实体
LargeMap / MiniMap MapData 地图覆盖层数据
Vitals PlayerVitals 玩家 HP/ES/MP + 增益
ScreenWidth / ScreenHeight int 游戏窗口尺寸
ProcessId DWORD 游戏进程 ID
GameWindow HWND 游戏窗口句柄
GameWindowForeground bool 游戏在前台时为 true
IsAttached bool 已连接到游戏进程时为 true
IsWindowValid bool 游戏窗口有效时为 true
LastUpdateTime uint64_t 最后数据更新的时间戳
AreaChangeCounter uint64_t 区域变更时递增
Inventories vector<InventoryInfo> 玩家背包内容
CurrencyTotals map<string,int> 按路径统计的货币数量
InventoryGrid InventoryGridInfo 背包 UI 网格信息
WorldToScreenMatrix XMFLOAT4X4 3D→2D 投影矩阵

重要: 实体过滤 死亡实体(EntityState == Useless 的实体)在插件接收快照之前就已被过滤掉。这意味着您永远不会观察到从存活到死亡的 HP 转变。如果需要检测击杀,请使用基于消失的检测 — 按区域追踪实体 ID,当实体在 InnerCircleOuterCircle 近接范围内从实体列表中消失时计为击杀。详见第 8 节: 常用方法

GetPlayerVitals()PlayerVitals

玩家生命值的便捷快捷方式。

GetCurrentState()GameStateTypes

返回当前游戏状态枚举。

IsAttached()bool

游戏进程已连接且可读取时为 true。

IsInGame()bool

当前在游戏中时为 true(不是加载中,不在登录界面)。

IsGameForeground()bool

游戏窗口是前台窗口时为 true。

GetProcessId()DWORD

返回游戏进程 ID。

物品数据访问

ReadExtendedItemMods(entityAddress)ExtendedItemModInfo

读取物品实体的所有词缀。

ReadItemRarity(entityAddress)int

返回: 0=普通, 1=魔法, 2=稀有, 3=传奇

ReadItemStackCount(entityAddress)int

返回货币/可堆叠物品的堆叠数量。

ReadItemName(entityAddress)string

返回物品的基础类型名称。

ReadItemPath(entityAddress)string

返回物品的元数据路径。

ReadItemBaseTypeName(entityAddress)string

返回物品的基础类型名称(例如 "Divine Orb", "Chaos Orb")。与返回元数据路径的 ReadItemName 不同,此函数从 BaseItemTypeData.BaseTypeName 读取实际基础类型名称。

ReadItemUniqueName(entityAddress)string

返回物品的传奇名称(来自 Words.dat,例如 "Headhunter", "Brimstone Call")。非传奇物品返回空字符串。

覆盖层模式 (SDK v2)

IsOverlayMode()bool

当宿主当前处于覆盖层模式(游戏窗口上方的透明覆盖层)时返回 true。用此来调整渲染 — 例如在游戏覆盖层上绘制与在设置窗口中绘制之间切换。

UI 状态 (SDK v4)

IsMenuVisible()bool

当宿主设置菜单可见时(覆盖层可交互)返回 true。当菜单隐藏时,覆盖层窗口是点击穿透的(WS_EX_TRANSPARENT),因此 ImGui 窗口无法接收鼠标输入。

用于实现可拖拽覆盖层模式:

  • 菜单可见: 显示拖拽手柄,允许交互(选项卡、按钮)
  • 菜单隐藏: 移除拖拽手柄,添加 ImGuiWindowFlags_NoInputs 使窗口不可交互

完整实现请参见第 6 节: 可拖拽覆盖层模式

内存读取 (SDK v2)

直接访问游戏进程内存。所有读取操作都是安全的(失败时返回 0/空值)。

GetBaseAddress()uintptr_t

返回游戏可执行模块的基地址。未连接时返回 0。

GetModuleSize()uintptr_t

返回游戏模块的字节大小。未连接时返回 0。

ReadProcessMemory(address, buffer, size)bool

从游戏进程读取原始字节块。buffer 必须至少分配了 size 字节。成功时返回 true。

// Example: Read a 4-byte integer from game memory
uint32_t value = 0;
m_Context->ReadProcessMemory(address, &value, sizeof(value));

// Example: Read a struct
MyStruct data{};
m_Context->ReadProcessMemory(structAddress, &data, sizeof(data));

ReadString(address)string

从游戏内存读取 null 终止的 ASCII 字符串(最多 128 个字符)。

ReadUnicodeString(address)wstring

从游戏内存读取 null 终止的 Unicode(宽)字符串(最多 128 个 wchar)。

GetPatternAddress(patternName)uintptr_t

通过名称获取已解析的模式扫描地址。未找到时返回 0。

标准模式:

名称 说明
"Game States" GameStates 向量根节点
"File Root" 文件注册表
"AreaChangeCounter" 区域转换计数器
"Terrain Rotator Helper" 旋转数据
"Terrain Rotation Selector" 旋转选择器
"GameCullSize" 屏幕裁剪值

世界到屏幕投影 (SDK v2)

WorldToScreen(worldX, worldY, worldZ, outX, outY)bool

将世界空间位置转换为屏幕坐标。如果位置在屏幕上可见则返回 true。

float screenX, screenY;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &screenX, &screenY)) {
    ImGui::GetBackgroundDrawList()->AddText(ImVec2(screenX, screenY), IM_COL32_WHITE, "Label");
}

背包 (SDK v2)

RequestInventoryScan(inventoryId)

请求宿主扫描背包。传入 -1 扫描所有背包,或传入特定背包 ID。快照中的背包数据在扫描完成后(下一帧)填充。

注意: 背包数据不会自动刷新 — 您必须调用此函数来触发扫描。如果需要持续的背包数据,请定期调用(例如每 2 秒)。

地形数据 (SDK v2)

GetWalkableGrid(outWidth, outHeight)const uint8_t*

返回可行走网格数据的指针。网格是 2D 数组,其中 0 = 不可行走,非零 = 可行走。如果数据不可用则返回 nullptr。

GetTerrainHeight(gridX, gridY)float

返回网格位置的地形高度。超出范围或数据不可用时返回 0。

原生容器读取 (SDK v3)

这些函数直接从游戏内存读取 C++ 标准库容器,映射宿主的 Core::Process 方法。

ReadStdVector(containerAddress, elementSize, outCount)void*

从游戏内存读取 StdVector(24 字节结构体: {First, Last, End})。返回 malloc 分配的元素缓冲区。调用者必须 free() 返回的指针。失败时返回 nullptr

// Example: Read a vector of uint32_t
int count = 0;
void* data = m_Context->ReadStdVector(vectorAddr, sizeof(uint32_t), &count);
if (data && count > 0) {
    uint32_t* values = static_cast<uint32_t*>(data);
    for (int i = 0; i < count; i++) { /* values[i] */ }
    free(data);
}

ReadStdList(containerAddress, elementSize, outCount)void*

从游戏内存读取 StdList(16 字节结构体: {Head, Size})。遍历链表并返回连续缓冲区。调用者必须 free()

ReadStdBucket(containerAddress, elementSize, outCount)void*

从游戏内存读取 StdBucket(读取内嵌的 StdVector)。调用者必须 free()

ReadStdMap(containerAddress, keySize, valueSize, callback, userData)int

遍历 StdMap(16 字节结构体: {Head, Size})并为每个键值对调用 callback。返回访问的节点数。

// Example: Read a map<uint32_t, float>
struct MapResult { std::vector<std::pair<uint32_t, float>> entries; };
MapResult result;
m_Context->ReadStdMap(mapAddr, sizeof(uint32_t), sizeof(float),
    [](const void* key, const void* value, void* userData) {
        auto* r = static_cast<MapResult*>(userData);
        uint32_t k; float v;
        memcpy(&k, key, sizeof(k));
        memcpy(&v, value, sizeof(v));
        r->entries.push_back({k, v});
    }, &result);

ReadStdWString(containerAddress)wstring

从游戏内存读取 StdWString(带内联/堆缓冲区的 32 字节结构体)。

GetInventoryName(inventoryId)const char*

返回背包 ID 的人类可读名称(例如 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1")。

调试数据访问 (SDK v4)

SDK v4 提供对宿主调试数据的直接访问 — 实体组件、背包详情和 UI 元素树 — 对应内置的 Debug 选项卡。

实体调试列表

GetEntityDebugList()vector<DebugEntityInfo>

返回包含调试元数据(Id、Address、Path、Type、SubType、State、Rarity、Zone)的所有实体列表。对应 Debug→Entity List 选项卡。

WatchEntity(entityId)

开始监视实体的组件。宿主的工作线程将每帧读取此实体的完整组件数据。

UnwatchEntity(entityId)

停止监视实体的组件。当用户折叠实体树节点时调用此函数以释放资源。

GetWatchedEntityData(entityId)DebugEntityComponents

返回被监视实体的完整组件数据。包含所有 8 个已识别组件(Life、Render、Positioned、Targetable、Animated、Stats、Actor、Buffs)的子结构体,以及所有组件地址的列表。

// Example: Watch entity on expand, read components
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
    if (ImGui::TreeNode(e.Path.c_str())) {
        m_Context->WatchEntity(e.Id);
        auto data = m_Context->GetWatchedEntityData(e.Id);
        if (data.HasLife) {
            ImGui::Text("HP: %d / %d  ES: %d / %d",
                data.Life.Health.Current, data.Life.Health.Total,
                data.Life.EnergyShield.Current, data.Life.EnergyShield.Total);
        }
        ImGui::TreePop();
    } else {
        m_Context->UnwatchEntity(e.Id);
    }
}

背包调试

GetServerDataAddress()uintptr_t

返回 ServerData 组件的基地址。

GetPlayerInventoryList()vector<pair<int, uintptr_t>>

返回所有玩家背包 ID 及其地址(来自 ServerData)。

WatchInventory(inventoryId)

开始监视背包以进行详细调试检查。宿主读取槽位占用情况、物品详情和词缀。

GetWatchedInventoryData()DebugInventoryData

返回当前被监视背包的完整数据: 网格尺寸、槽位占用情况、带稀有度和词缀的物品。

// Example: Inventory inspector
auto invList = m_Context->GetPlayerInventoryList();
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
for (auto& item : inv.Items) {
    ImGui::Text("[%s] Rarity=%d Mods=%d", item.Path.c_str(), item.Rarity,
        (int)(item.ImplicitMods.size() + item.ExplicitMods.size()));
}

UI 元素树

GetGameUiRootAddress()uintptr_t

返回游戏 UI 根元素地址(用于游戏内 UI 树导航)。

GetUiRootAddress()uintptr_t

返回顶层 UI 根地址。

GetGameCullValue()int

返回当前 GameCullSize 值,用于 UI 缩放计算。结合屏幕尺寸,可以准确计算 UI 元素的位置/大小。

// Example: UI scale calculation (matching host logic)
int cullValue = m_Context->GetGameCullValue();
auto snapshot = m_Context->GetSnapshot();
// Scale for index 1 (width): screenWidth / (cullValue / baseWidth)
// Scale for index 2 (height): screenHeight / (cullValue / baseHeight)

PluginHelpers.h — 便捷封装 (SDK v3)

sdk/PluginHelpers.h 头文件(包含在 ExamplePlugin 中)提供了类型安全的 MemoryReader 类,封装了原始的 PluginContext 函数:

PluginSDK::MemoryReader mem(m_Context);

// Read a single struct
auto data = mem.Read<MyStruct>(address);

// Read an array
auto arr = mem.ReadArray<uint32_t>(address, count);

// Read native containers — returns std::vector<T>
auto vec = mem.ReadStdVector<uint32_t>(vectorAddr);
auto list = mem.ReadStdList<MyNode>(listAddr);
auto bucket = mem.ReadStdBucket<MyEntry>(bucketAddr);
auto map = mem.ReadStdMap<uint32_t, float>(mapAddr);
auto wstr = mem.ReadStdWString(wstringAddr);

MemoryReader 还提供了 ReadString()ReadUnicodeString()GetBaseAddress()GetModuleSize()GetPatternAddress() 的便捷封装。

PluginHelpers.h 中的其他工具:

  • WideToNarrow(wstring) — 安全的 wstring→string 转换(ASCII 有损)
  • GetEntityTypeName(type) — 枚举到显示名称(包括 ExpeditionMarker/ExpeditionRemnant)
  • GetNearbyZoneName(zone) — 区域到显示名称
  • GetRarityName(rarity) / GetRarityColor(rarity) — 稀有度显示辅助函数

宿主服务

Log(level, message)

写入宿主的日志系统。级别: "Debug", "Info", "Warning", "Error"

ImGuiContext (void*)

宿主的 ImGui 上下文。在 SetContext() 中调用 ImGui::SetCurrentContext()

D3DDevice (void*)

宿主的 ID3D11Device*。强制转换后用于加载纹理。


5. 数据结构参考

所有类型位于 PluginSDK 命名空间中。插件通常添加 using namespace PluginSDK;

RadarEntity

snapshot->Entities 中可用的每实体数据:

字段 类型 说明
Id uint32_t 唯一实体 ID
Address uintptr_t 内存地址(用于物品 API 调用)
EntityDetailsAddress uintptr_t 实体详情结构体地址
RenderComponentAddress uintptr_t Render 组件地址(快捷方式)
IsValid bool 实体有效性标志
entityType EntityTypes 实体类别
entitySubtype EntitySubtypes 实体子类别
entityState EntityStates 实体状态
Rarity int 0=普通, 1=魔法, 2=稀有, 3=传奇
Reaction uint8_t 0=敌对, 1=中立, 2=友好
GridPositionX/Y float 地形网格上的位置
TerrainHeight float 实体位置的地形高度
WorldX/Y/Z float 世界空间位置
ModelBoundsZ float 模型高度
Path wstring 实体元数据路径
PlayerName wstring 玩家名称(如果是玩家实体)
TgtPath string 目标路径(窄字符串)
CurrentHP/MaxHP int 实体生命值
CurrentES/MaxES int 实体能量护盾
IsSleeping bool 远距离实体标志
IsChestOpened bool 宝箱打开状态
Zone NearbyZone 与玩家的距离
ComponentCache EntityComponentCache 组件地址

重要: 死亡实体(EntityState::Useless)在快照到达插件之前就被移除。您永远不会看到怪物的 HP 降到零 — 它只是从列表中消失。使用 Zone 字段区分击杀(实体从 InnerCircle/OuterCircle 消失)和超出范围的实体(实体在 Far 区域)。

Buff

活跃的增益/减益:

字段 类型 说明
Name string 内部增益名称(例如 "flask_effect_life"
TimeLeft float 剩余秒数
Charges short 层数
TotalTime float 总持续时间

MapData

小地图/大地图状态:

字段 类型 说明
CenterX/Y float 地图中心
SizeX/Y float 地图尺寸
ShiftX/Y float 当前平移偏移
DefaultShiftX/Y float 默认偏移值
Zoom float 缩放级别
Scale float 地图缩放因子
IsVisible bool 地图当前是否显示

InventoryInfo / InventoryItemInfo

字段 类型 说明
Id int 背包 ID
TotalBoxesX/Y int 网格尺寸
Ptr uintptr_t 背包内存地址
Items vector<InventoryItemInfo> 背包中的物品

物品字段: Address, Name(元数据路径), Path(与 Name 相同), BaseTypeName(基础类型名称,例如 "Divine Orb"), UniqueName(传奇名称,来自 Words.dat,例如 "Headhunter",非传奇物品为空), SlotX/Y, Width/Height, StackCount, IsCurrency

ExtendedItemModInfo

ReadExtendedItemMods() 返回:

字段 类型 说明
ImplicitMods vector<ItemModData> 隐性词缀
ExplicitMods vector<ItemModData> 显性词缀
EnchantMods vector<ItemModData> 附魔词缀
HellscapeMods vector<ItemModData> Hellscape 词缀
CrucibleMods vector<ItemModData> Crucible 词缀
Rarity int 0=普通, 1=魔法, 2=稀有, 3=传奇

ItemModData

字段 类型 说明
Key string 词缀统计键
Values vector<float> 词缀掷骰值

EntityComponentCache

每实体缓存的组件地址(通过 RadarEntity.ComponentCache 可用):

字段 类型 存在检查方法
RenderAddr uintptr_t HasRender()
PositionedAddr uintptr_t HasPositioned()
ChestAddr uintptr_t HasChest()
PlayerAddr uintptr_t HasPlayer()
ShrineAddr uintptr_t HasShrine()
LifeAddr uintptr_t HasLife()
TargetableAddr uintptr_t HasTargetable()
OMPAddr uintptr_t HasOMP()
NPCAddr uintptr_t HasNPC()
TriggerableBlockageAddr uintptr_t HasTriggerableBlockage()
DiesAfterTimeAddr uintptr_t HasDiesAfterTime()
BuffsAddr uintptr_t HasBuffs()
WorldItemAddr uintptr_t HasWorldItem()
AreaTransitionAddr uintptr_t HasAreaTransition()
MinimapIconAddr uintptr_t HasMinimapIcon()
StatsAddr uintptr_t HasStats()

DebugEntityInfo (SDK v4)

来自 GetEntityDebugList() 的实体元数据:

字段 类型 说明
Id uint32_t 实体 ID
Address uintptr_t 内存地址
Path string 元数据路径
EntityType int 实体类型(强制转换为 EntityTypes
EntitySubType int 实体子类型(强制转换为 EntitySubtypes
EntityState int 实体状态(强制转换为 EntityStates
Rarity int 0=普通, 1=魔法, 2=稀有, 3=传奇
Zone NearbyZone 与玩家的距离
ComponentAddresses vector<pair<string,uintptr_t>> 所有组件名称→地址对

DebugEntityComponents (SDK v4)

来自 GetWatchedEntityData() 的完整组件数据:

字段 类型 说明
EntityId uint32_t 此数据对应的实体
Valid bool 数据是否成功读取
HasLife / Life bool / DebugLifeComp Life 组件 — Life.Health, Life.EnergyShield, Life.Mana(每个都是 DebugVital: .Current, .Total, .Regeneration, .ReservedFlat, .ReservedPercent
HasRender / Render bool / DebugRenderComp 位置(WorldX/Y/Z、GridX/Y)、TerrainHeight、ModelBounds(X/Y/Z)
HasPositioned / Positioned bool / DebugPositionedComp Reaction 值、IsFriendly 标志
HasTargetable / Targetable bool / DebugTargetableComp IsTargetable、IsHighlightable、IsTargettedByPlayer、HiddenFromPlayer、MeetsQuestState、MeetsItemRequirements
HasAnimated / Animated bool / DebugAnimatedComp Animation Path (string)、Id (uint32)
HasStats / Stats bool / DebugStatsComp CurrentWeaponIndex、IsShapeshifted、StatsItems/StatsBuff(统计 ID→值对的向量)
HasActor / Actor bool / DebugActorComp AnimationId、AnimationName、ActiveSkills(DebugActiveSkill 向量)、DeployedCounts[256]
HasBuffs / Buffs bool / vector<DebugBuff> 活跃增益: Name、TotalTime、TimeLeft、Charges、FlaskSlot、Effectiveness、SourceEntityId

DebugInventoryData (SDK v4)

来自 GetWatchedInventoryData() 的背包详情:

字段 类型 说明
InventoryId int 背包 ID(无则为 -1)
Address uintptr_t 背包地址
TotalBoxesX/Y int 网格尺寸
ServerRequestCounter int 服务器同步计数器
GridScreenX / GridScreenY float 网格 UI 屏幕位置
CellSize float 网格单元格大小(像素)
GridValid bool 网格 UI 数据是否有效
SlotOccupied vector<bool> 每槽位占用情况
Items vector<DebugInventoryItem> 带路径、稀有度和词缀的物品

DebugInventoryItem (SDK v4)

字段 类型 说明
Address uintptr_t 物品实体地址
Path string 物品元数据路径
BaseTypeName string 基础类型名称(例如 "Divine Orb")
UniqueName string 传奇名称(来自 Words.dat,非传奇物品为空)
SlotX / SlotY int 背包中的网格位置
Rarity int 0=普通, 1=魔法, 2=稀有, 3=传奇
ItemLevel int 物品等级
RequiredLevel int 需要的角色等级
IsIdentified bool 物品是否已鉴定
IsCorrupted bool 物品是否已腐化
CraftedModCount int 制作词缀数量
ImplicitMods vector<DebugModInfo> 隐性词缀
ExplicitMods vector<DebugModInfo> 显性词缀
EnchantMods vector<DebugModInfo> 附魔词缀
HellscapeMods vector<DebugModInfo> Hellscape 词缀

DebugActiveSkill (SDK v4)

字段 类型 说明
Name string 技能名称
UseStage int 当前使用阶段
CastType int 施放类型
TotalUses int 总使用次数
TotalCooldownTimeInMs int 冷却时间(毫秒)
CanBeUsed bool 技能是否当前可用

DebugBuff (SDK v4)

字段 类型 说明
Name string 内部增益名称
TotalTime float 总持续时间
TimeLeft float 剩余秒数
Charges short 层数
FlaskSlot short 药剂栏位索引
Effectiveness short 增益效能
SourceEntityId uint32_t 施加此增益的实体

DebugModInfo (SDK v4)

字段 类型 说明
Name string 词缀显示名称
StatKey string 统计键标识符
AffixName string 词缀名称
GenerationType int 1=Prefix, 2=Suffix, 3=Implicit
Value0 float 第一个值(无则为 NaN)
Value1 float 第二个值(无则为 NaN)

枚举

EntityTypes: Unidentified(0), Chest(1), NPC(2), Player(3), Shrine(4), Monster(5), DeliriumBomb(6), DeliriumSpawner(7), OtherImportantObjects(8), Item(9), Renderable(10), AreaTransition(11), ExpeditionMarker(12), ExpeditionRemnant(13)

EntitySubtypes: _Unidentified(0), _None(1), PlayerSelf(2), PlayerOther(3), ChestWithMagicRarity(4), ChestWithRareRarity(5), ExpeditionChest(6), BreachChest(7), Strongbox(8), SpecialNPC(9), POIMonster(10), PinnacleBoss(11), WorldItem(12), InventoryItem(13)

EntityStates: None(0), Useless(1), PlayerLeader(2), MonsterFriendly(3), PinnacleBossHidden(4)

NearbyZone: None(0), InnerCircle(1, ~60 格单位), OuterCircle(2, ~120 格单位), Far(3)

GameStateTypes: AreaLoadingState(0), ChangePasswordState(1), CreditsState(2), EscapeState(3), InGameState(4), PreGameState(5), LoginState(6), WaitingState(7), CreateCharacterState(8), SelectCharacterState(9), DeleteCharacterState(10), LoadingState(11), GameNotLoaded(12)


6. 插件中的 ImGui 使用

共享上下文

宿主和插件共享同一个 ImGui 上下文。您必须SetContext() 方法中调用:

ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));

窗口 ID

始终使用唯一的窗口 ID 以避免与宿主或其他插件冲突:

ImGui::Begin("My Window##MyPluginName", &showWindow);

可用功能

  • 窗口、选项卡、树、表格、绘制列表
  • 通过 D3D11 设备加载纹理
  • 通过 ImGui::GetBackgroundDrawList() 进行覆盖层渲染
  • 通过 #include "imgui/IconsFontAwesome6.h" 使用 FontAwesome 6 图标(例如 ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT

覆盖层渲染 (SDK v2)

使用 WorldToScreen() 在实体位置绘制标签/形状:

float sx, sy;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &sx, &sy)) {
    auto* drawList = ImGui::GetBackgroundDrawList();
    drawList->AddText(ImVec2(sx, sy - 20), IM_COL32(255, 255, 0, 255), "Monster");
    drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, IM_COL32(255, 0, 0, 255));
}

覆盖层模式

重写 WantsOverlay() 返回 true 以请求宿主进入覆盖层模式:

bool WantsOverlay() override { return m_OverlayEnabled; }

在覆盖层模式下,宿主窗口是透明的,位于游戏上方。您的 DrawUI() 调用直接渲染到游戏屏幕上。

可拖拽覆盖层模式 (SDK v4)

宿主覆盖层使用 WS_EX_TRANSPARENT 在菜单隐藏时使窗口点击穿透。这意味着 ImGui 窗口无法接收鼠标输入,除非菜单可见。要创建可拖拽的覆盖层窗口(如内置的 Vitals Overlay),请使用此双模式模式:

#include "imgui/IconsFontAwesome6.h"

void MyPlugin::RenderOverlay() {
    bool menuVisible = m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false;

    if (menuVisible) {
        // === 拖拽模式 ===
        // 带背景、拖拽提示、交互控件的窗口
        ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY), ImGuiCond_Appearing);
        ImGui::SetNextWindowBgAlpha(m_Alpha);

        ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
            ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
            ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
            ImGuiWindowFlags_NoFocusOnAppearing;

        ImGui::Begin("##MyOverlay", nullptr, flags);

        // Drag hint (the entire window is draggable since there's no title bar)
        ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
            ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT " Drag to reposition");
        ImGui::Spacing();

        // ... your content here (tabs, text, icons, buttons — all interactive) ...

        // Persist position when user drags the window
        ImVec2 pos = ImGui::GetWindowPos();
        if (pos.x != m_PosX || pos.y != m_PosY) {
            m_PosX = pos.x;
            m_PosY = pos.y;
            // Save to settings on next SaveSettings() call
        }

        ImGui::End();
    }
    else {
        // === 非交互模式 ===
        // 静态覆盖层 — 无拖拽,无鼠标交互
        ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY));
        ImGui::SetNextWindowBgAlpha(m_Alpha);

        ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
            ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
            ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
            ImGuiWindowFlags_NoFocusOnAppearing |
            ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
            ImGuiWindowFlags_NoInputs;

        ImGui::Begin("##MyOverlay", nullptr, flags);
        // ... your content here (display-only, no interactive controls) ...
        ImGui::End();
    }
}

关键要点:

  • ImGuiCond_Appearing 仅在首次显示时设置位置;之后 ImGui 跟踪拖拽位置
  • NoTitleBar + 无 NoMove = 窗口可从任何空白区域拖拽(ImGui 默认行为)
  • 非交互模式中的 NoInputs 防止覆盖层通过 WS_EX_TRANSPARENT 窃取焦点
  • 为向后兼容始终对 IsMenuVisible 指针进行空检查: m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false
  • 将位置保存到设置文件中,使其在会话之间持久化

7. 设置持久化

推荐模式

void OnEnable(bool isGameOpened) override {
    LoadSettings();  // Load from Plugins/YourPlugin/config/settings.txt
}

void SaveSettings() override {
    // Write to Plugins/YourPlugin/config/settings.txt
    // The host calls this periodically and on shutdown
}

文件位置

将设置存储在 <PluginDirectory>/config/ 中:

std::filesystem::path settingsPath =
    std::filesystem::path(m_Directory) / "config" / "settings.txt";

简单键值格式

对于具有许多设置的插件,简单的键=值文本格式效果很好:

// Save
std::ofstream file(configDir / "settings.txt");
file << "ShowOverlay=" << (m_Settings.ShowOverlay ? 1 : 0) << "\n";
file << "WindowAlpha=" << m_Settings.WindowAlpha << "\n";
file << "PosX=" << m_Settings.PosX << "\n";
file << "PosY=" << m_Settings.PosY << "\n";

// Load
std::ifstream file(settingsPath);
std::string line;
while (std::getline(file, line)) {
    auto eq = line.find('=');
    if (eq == std::string::npos) continue;
    std::string key = line.substr(0, eq);
    std::string val = line.substr(eq + 1);
    if (key == "ShowOverlay") m_Settings.ShowOverlay = (val == "1");
    else if (key == "WindowAlpha") m_Settings.WindowAlpha = std::stof(val);
    else if (key == "PosX") m_Settings.PosX = std::stof(val);
    else if (key == "PosY") m_Settings.PosY = std::stof(val);
}

8. 常用方法

获取玩家 HP 百分比

auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100

列出内圈内的所有怪物

auto snapshot = m_Context->GetSnapshot();
for (auto& e : snapshot->Entities) {
    if (e.entityType == EntityTypes::Monster &&
        e.Zone == NearbyZone::InnerCircle) {
        // e.CurrentHP, e.Path, e.WorldX/Y/Z...
    }
}

检查玩家是否拥有特定增益

auto vitals = m_Context->GetPlayerVitals();
for (auto& buff : vitals.Buffs) {
    if (buff.Name == "flask_effect_life") {
        // buff.TimeLeft, buff.Charges...
    }
}

获取当前区域信息

auto snapshot = m_Context->GetSnapshot();
std::string area = snapshot->CurrentAreaName;
bool isTown = snapshot->IsTown;
int level = snapshot->CurrentAreaLevel;

在实体世界位置绘制文字 (SDK v2)

for (auto& e : snapshot->Entities) {
    if (e.entityType != EntityTypes::Monster) continue;
    float sx, sy;
    if (m_Context->WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, &sx, &sy)) {
        auto* dl = ImGui::GetBackgroundDrawList();
        dl->AddText(ImVec2(sx, sy - 15), IM_COL32(255, 255, 0, 255), "Monster");
    }
}

从背包读取物品词缀

// First, request an inventory scan (call periodically, e.g. every 2s)
m_Context->RequestInventoryScan(-1);

// Then read from snapshot (next frame)
auto snapshot = m_Context->GetSnapshot();
for (auto& inv : snapshot->Inventories) {
    for (auto& item : inv.Items) {
        auto mods = m_Context->ReadExtendedItemMods(item.Address);
        // mods.ExplicitMods, mods.ImplicitMods...
    }
}

按类型统计实体数量

auto snapshot = m_Context->GetSnapshot();
int monsters = 0, items = 0;
for (auto& e : snapshot->Entities) {
    if (e.entityType == EntityTypes::Monster) monsters++;
    if (e.entityType == EntityTypes::Item) items++;
}

检测区域变更

static uint64_t lastAreaChange = 0;
auto snapshot = m_Context->GetSnapshot();
if (snapshot->AreaChangeCounter != lastAreaChange) {
    lastAreaChange = snapshot->AreaChangeCounter;
    // Area changed! Reset state...
}

检查是否在加载画面

if (m_Context->GetCurrentState() == GameStateTypes::AreaLoadingState) {
    // Currently loading...
}

检测怪物击杀(基于消失)

死亡实体从快照中被过滤掉(EntityState::Useless),因此无法检测 HP 降到 0。相反,通过 ID 追踪实体并检测它们从附近区域消失的时机:

// In your tracker class:
struct TrackedEntity {
    uint32_t Id;
    int Rarity;
    PluginSDK::NearbyZone Zone;
};

std::unordered_map<uint32_t, TrackedEntity> m_PrevEntities;

void DetectKills(const std::vector<PluginSDK::RadarEntity>& entities) {
    std::unordered_set<uint32_t> currentIds;

    // Update tracking map with current monsters
    for (const auto& e : entities) {
        if (e.entityType != EntityTypes::Monster) continue;
        if (e.entityState == EntityStates::MonsterFriendly) continue;
        currentIds.insert(e.Id);
        m_PrevEntities[e.Id] = { e.Id, e.Rarity, e.Zone };
    }

    // Disappeared from InnerCircle/OuterCircle = killed
    for (auto it = m_PrevEntities.begin(); it != m_PrevEntities.end(); ) {
        if (currentIds.count(it->first) == 0) {
            if (it->second.Zone == NearbyZone::InnerCircle ||
                it->second.Zone == NearbyZone::OuterCircle) {
                OnMonsterKilled(it->second.Rarity);
            }
            it = m_PrevEntities.erase(it);
        } else {
            ++it;
        }
    }
}

为什么有效: ~120 格单位内的实体突然消失几乎可以确定是被击杀了(不是走出了范围)。Far 区域的实体会自然地在实体列表中出现和消失 — 不要计入这些。

重要: 在区域变更时(AreaChangeCounter 改变)清除 m_PrevEntities 以避免误报。

读取原始游戏内存 (SDK v2)

// Read a struct from a known address
struct MyGameStruct { int field1; float field2; };
MyGameStruct data{};
if (m_Context->ReadProcessMemory(someAddress, &data, sizeof(data))) {
    // data.field1, data.field2 are now populated
}

// Read a string from memory
std::string str = m_Context->ReadString(stringAddress);

使用模式扫描结果 (SDK v2)

uintptr_t gameStatesAddr = m_Context->GetPatternAddress("Game States");
if (gameStatesAddr != 0) {
    // Read data at the resolved pattern address
    uint64_t value = 0;
    m_Context->ReadProcessMemory(gameStatesAddr, &value, sizeof(value));
}

检查可行走地形 (SDK v2)

int gridW = 0, gridH = 0;
const uint8_t* grid = m_Context->GetWalkableGrid(&gridW, &gridH);
if (grid && gridW > 0 && gridH > 0) {
    int x = (int)snapshot->Player.GridPositionX;
    int y = (int)snapshot->Player.GridPositionY;
    if (x >= 0 && x < gridW && y >= 0 && y < gridH) {
        bool walkable = grid[y * gridW + x] != 0;
    }
}

使用 MemoryReader 进行类型化内存读取 (SDK v3)

PluginSDK::MemoryReader mem(m_Context);

// Read a struct from a known address
struct GameData { int level; float health; };
auto data = mem.Read<GameData>(address);

// Read a StdVector of pointers
auto ptrs = mem.ReadStdVector<uintptr_t>(vectorAddr);
for (auto ptr : ptrs) { /* process each pointer */ }

// Read a StdMap<int, float>
auto entries = mem.ReadStdMap<int, float>(mapAddr);
for (auto& [key, value] : entries) { /* key, value */ }

获取背包名称 (SDK v3)

for (auto& inv : snapshot->Inventories) {
    const char* name = m_Context->GetInventoryName(inv.Id);
    // name is e.g. "MainInventory1", "Weapon1", "Currency1"
}

读取实体组件地址

for (auto& e : snapshot->Entities) {
    auto& cc = e.ComponentCache;
    if (cc.HasLife()) {
        // cc.LifeAddr contains the Life component address
        // Use MemoryReader to read component structs
    }
    if (cc.HasRender()) {
        // cc.RenderAddr has the Render component address
    }
}

通过调试监视检查实体组件 (SDK v4)

// Get all entities with debug info
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
    bool open = ImGui::TreeNode(e.Path.c_str());
    if (open) {
        m_Context->WatchEntity(e.Id);
        auto comp = m_Context->GetWatchedEntityData(e.Id);
        if (comp.HasLife) {
            ImGui::Text("HP: %d/%d  ES: %d/%d  MP: %d/%d",
                comp.Life.Health.Current, comp.Life.Health.Total,
                comp.Life.EnergyShield.Current, comp.Life.EnergyShield.Total,
                comp.Life.Mana.Current, comp.Life.Mana.Total);
        }
        if (comp.HasActor) {
            ImGui::Text("Animation: %s (%d)  Skills: %d",
                comp.Actor.AnimationName.c_str(), comp.Actor.AnimationId,
                (int)comp.Actor.ActiveSkills.size());
        }
        ImGui::TreePop();
    } else {
        m_Context->UnwatchEntity(e.Id);
    }
}

带槽位网格的背包检查 (SDK v4)

auto invList = m_Context->GetPlayerInventoryList();
if (!invList.empty()) {
    m_Context->WatchInventory(invList[0].first);
    auto inv = m_Context->GetWatchedInventoryData();
    if (inv.InventoryId >= 0) {
        ImGui::Text("Grid: %dx%d  Items: %d",
            inv.TotalBoxesX, inv.TotalBoxesY, (int)inv.Items.size());
        for (auto& item : inv.Items) {
            ImGui::Text("[R%d iLvl%d] %s (%s)  Mods: %d/%d/%d/%d",
                item.Rarity, item.ItemLevel,
                item.BaseTypeName.c_str(), item.Path.c_str(),
                (int)item.ImplicitMods.size(), (int)item.ExplicitMods.size(),
                (int)item.EnchantMods.size(), (int)item.HellscapeMods.size());
        }
    }
}

导航 UI 元素树 (SDK v4)

uintptr_t uiRoot = m_Context->GetGameUiRootAddress();
if (uiRoot) {
    PluginSDK::MemoryReader mem(m_Context);
    // Read children vector at offset 0x010
    auto children = mem.ReadStdVector<uintptr_t>(uiRoot + 0x010);
    for (auto childAddr : children) {
        // Read StringId at offset 0x448
        uintptr_t strPtr = mem.Read<uintptr_t>(childAddr + 0x448);
        if (strPtr) {
            std::string name = m_Context->ReadString(strPtr);
            ImGui::Text("Child: %s (0x%llX)", name.c_str(), childAddr);
        }
    }
}

使用显示辅助函数读取物品词缀 (SDK v3)

auto mods = m_Context->ReadExtendedItemMods(item.Address);
ImGui::TextColored(
    PluginSDK::GetRarityColor(mods.Rarity),
    "Rarity: %s", PluginSDK::GetRarityName(mods.Rarity));
for (auto& mod : mods.ExplicitMods) {
    ImGui::BulletText("%s", mod.Key.c_str());
}

9. 构建和部署

构建设置

设置
Configuration Release
Platform x64
C++ Standard /std:c++20
Runtime Library /MD (Multi-threaded DLL)
Configuration Type DLL

插件项目必需文件

  • 插件 .cpp 文件
  • ImGui 源文件: imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp
  • 到 POEFixer 根目录的包含路径(用于 SDK 和 ImGui 头文件)
  • 可选: 复制 Plugins/ExamplePlugin/sdk/PluginHelpers.h 以获取 MemoryReader 封装和实用函数

包含路径

您的 .vcxproj 需要这些额外的包含目录:

<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

如果使用本地第三方库(例如 lib/ 子文件夹中的 SQLite3),在解决方案路径之前添加 $(ProjectDir)lib,使本地头文件优先:

<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

第三方库

SQLite3(静态链接)

要在插件中使用 SQLite3,必须将合并源码直接编译到 DLL 中 — Windows LoadLibrary 不会在 DLL 自身目录中搜索依赖项,因此动态链接 sqlite3.dll 将以错误 126 失败。

步骤:

  1. sqlite3.csqlite3.h 复制到插件的 lib/ 目录
  2. 创建 lib/sqlite3-vcpkg-config.h 以覆盖 SQLITE_API(防止 __declspec(dllimport) 错误):
    #ifndef SQLITE_API
    #define SQLITE_API
    #endif
    #define SQLITE_ENABLE_UNLOCK_NOTIFY 1
    #define SQLITE_OS_WIN 1
    #define SQLITE_ENABLE_COLUMN_METADATA 1
  3. sqlite3.c 作为 C 文件添加到 .vcxproj 中并禁用警告:
    <ClCompile Include="lib\sqlite3.c">
      <CompileAs>CompileAsC</CompileAs>
      <WarningLevel>TurnOffAllWarnings</WarningLevel>
      <SDLCheck>false</SDLCheck>
      <PreprocessorDefinitions>SQLITE_THREADSAFE=1;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>

stb_image(纹理加载)

要从图像文件(PNG、JPG)加载纹理,在一个 .cpp 文件中包含 stb_image:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

然后使用 m_Context->D3DDevice 中的 D3D11 设备创建 GPU 纹理。

输出

设置输出目录为:

$(SolutionDir)x64\Release\Plugins\YourPlugin\

部署

将构建的 DLL 复制到主可执行文件旁边的 Plugins/YourPlugin/YourPlugin.dll

调试

  1. 以 Debug 模式构建插件 DLL
  2. 启动宿主应用程序
  3. 在 Visual Studio 中: Debug → Attach to Process → 选择宿主 .exe
  4. 在插件源代码中设置断点
  5. 当代码被调用时调试器将中断

10. 故障排除

问题 解决方案
插件未加载 检查 DLL 名称是否与文件夹名称完全匹配
"SDK version mismatch" 使用最新的 SDK 头文件重新构建插件(当前版本: 4)
LoadLibrary 错误 126 DLL 存在未解析的依赖项。对于 SQLite3 等第三方库,将其静态编译到 DLL 中(见第 9 节)。使用 dumpbin /dependents YourPlugin.dll 检查。
加载时崩溃 检查 CRT 不匹配 — 两者都必须使用 /MD
ImGui 不渲染 确保在 SetContext() 中调用了 ImGui::SetCurrentContext()
数据为空/零 在读取数据之前检查 IsAttached()IsInGame()
背包为空 调用 RequestInventoryScan(-1) — 背包数据是按需提供的
"Missing exports" 错误 确保 CreatePluginDestroyPlugin 使用 extern "C" 导出
插件导致宿主崩溃 这不应该发生 — 所有插件调用都受 SEH 保护。检查日志。
数据过时 GetSnapshot() 返回最新帧的数据。不要缓存指针。
内存读取返回 0 确认 IsAttached() 为 true 且地址有效
WorldToScreen 返回 false 位置可能在摄像机后方或屏幕外
覆盖层窗口不可点击 菜单隐藏时宿主使用 WS_EX_TRANSPARENT。使用 IsMenuVisible() 仅在菜单活动时显示交互控件。见第 6 节的可拖拽覆盖层模式。
击杀/死亡检测不工作 死亡实体从快照中移除。使用基于消失的检测而非 HP 转变。见第 8 节。
C2491 "dllimport function" 错误 第三方库头文件定义了 __declspec(dllimport)。创建一个本地覆盖头文件,将 API 宏设置为空(见第 9 节的 SQLite3 示例)。

← Home

Clone this wiki locally