-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide ZH
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - x64 Release 构建
-
运行时库:
/MD(Multi-threaded DLL) — 必须与宿主匹配
- 在 Visual Studio 中创建一个新的 C++ DLL 项目
- 将包含路径设置为指向 POEFixer 源目录(用于 SDK 和 ImGui 头文件)
- 将 ImGui 源文件添加到项目中:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - 在插件源代码中包含 Plugin SDK 头文件:
或使用 ExamplePlugin 的便捷头文件:
#include "plugin_sdk/PluginAPI.h" #include "plugin_sdk/PluginContext.h" #include "imgui/imgui.h"
#include "sdk/PluginHelpers.h" // Includes all SDK headers + MemoryReader + utilities
- 在项目的预处理器定义中定义
PLUGIN_EXPORTS和_CRT_SECURE_NO_WARNINGS
Plugins/
YourPlugin/
YourPlugin.dll <-- DLL 名称必须与文件夹名称匹配
config/
settings.txt <-- 可选的设置文件
data/
... <-- 可选的数据目录(数据库、缓存等)
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 元素浏览器
包含 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
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 不是线程安全的
每个插件必须实现 IPlugin 接口(定义在 plugin_sdk/PluginAPI.h 中):
- 调用时机: 创建后立即调用一次
-
参数: 相对路径,如
"Plugins/YourPlugin" - 用途: 保存此路径以加载设置/资源
-
调用时机:
SetPluginDirectory之后调用一次 -
参数: 宿主的
PluginContext指针(在插件生命周期内有效) - 用途: 保存此指针 — 它是您访问所有游戏数据的入口
-
重要: 在此处调用
ImGui::SetCurrentContext(ctx->ImGuiContext)
- 调用时机: 当用户启用插件时,或如果之前已启用则在启动时
-
参数: 如果游戏进程当前已连接则为
true - 用途: 加载设置、分配资源、初始化状态
- 调用时机: 当用户禁用插件时
- 用途: 释放资源、停止后台工作
- 调用时机: 每帧,仅在插件启用时
- 用途: 使用 ImGui 渲染覆盖层
-
注意: 使用唯一的窗口 ID(如
"MyWindow##MyPlugin")以避免冲突
- 调用时机: 每帧,在 Plugins 设置选项卡中(仅在启用时)
- 用途: 使用 ImGui 渲染插件配置
- 调用时机: 定期调用,以及在应用程序关闭时
-
用途: 将设置保存到磁盘(例如
Plugins/YourPlugin/config/settings.txt)
-
返回值: Plugins 选项卡中显示的名称(例如
"My Plugin")
-
返回值:
PLUGIN_SDK_VERSION(当前为 4) - 用途: 宿主检查兼容性 — 必须匹配
-
返回值: 如果插件想在覆盖层模式(游戏上方的透明覆盖层)中渲染则为
true -
默认值:
false— 插件仅在普通设置窗口中渲染 -
用途: 当任何插件返回
true时,即使没有内置功能需要,宿主也会进入覆盖层模式
您的 DLL 必须导出这两个 C 函数:
extern "C" PLUGIN_API IPlugin* CreatePlugin() {
return new MyPlugin();
}
extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
delete plugin;
}PluginContext 结构体(定义在 plugin_sdk/PluginContext.h 中)提供用于访问游戏数据的函数指针。所有类型位于 PluginSDK 命名空间中。
返回游戏状态的完整快照。每帧更新一次。包含:
| 字段 | 类型 | 说明 |
|---|---|---|
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,当实体在InnerCircle或OuterCircle近接范围内从实体列表中消失时计为击杀。详见第 8 节: 常用方法。
玩家生命值的便捷快捷方式。
返回当前游戏状态枚举。
游戏进程已连接且可读取时为 true。
当前在游戏中时为 true(不是加载中,不在登录界面)。
游戏窗口是前台窗口时为 true。
返回游戏进程 ID。
读取物品实体的所有词缀。
返回: 0=普通, 1=魔法, 2=稀有, 3=传奇
返回货币/可堆叠物品的堆叠数量。
返回物品的基础类型名称。
返回物品的元数据路径。
返回物品的基础类型名称(例如 "Divine Orb", "Chaos Orb")。与返回元数据路径的 ReadItemName 不同,此函数从 BaseItemTypeData.BaseTypeName 读取实际基础类型名称。
返回物品的传奇名称(来自 Words.dat,例如 "Headhunter", "Brimstone Call")。非传奇物品返回空字符串。
当宿主当前处于覆盖层模式(游戏窗口上方的透明覆盖层)时返回 true。用此来调整渲染 — 例如在游戏覆盖层上绘制与在设置窗口中绘制之间切换。
当宿主设置菜单可见时(覆盖层可交互)返回 true。当菜单隐藏时,覆盖层窗口是点击穿透的(WS_EX_TRANSPARENT),因此 ImGui 窗口无法接收鼠标输入。
用于实现可拖拽覆盖层模式:
- 菜单可见: 显示拖拽手柄,允许交互(选项卡、按钮)
-
菜单隐藏: 移除拖拽手柄,添加
ImGuiWindowFlags_NoInputs使窗口不可交互
完整实现请参见第 6 节: 可拖拽覆盖层模式。
直接访问游戏进程内存。所有读取操作都是安全的(失败时返回 0/空值)。
返回游戏可执行模块的基地址。未连接时返回 0。
返回游戏模块的字节大小。未连接时返回 0。
从游戏进程读取原始字节块。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));从游戏内存读取 null 终止的 ASCII 字符串(最多 128 个字符)。
从游戏内存读取 null 终止的 Unicode(宽)字符串(最多 128 个 wchar)。
通过名称获取已解析的模式扫描地址。未找到时返回 0。
标准模式:
| 名称 | 说明 |
|---|---|
"Game States" |
GameStates 向量根节点 |
"File Root" |
文件注册表 |
"AreaChangeCounter" |
区域转换计数器 |
"Terrain Rotator Helper" |
旋转数据 |
"Terrain Rotation Selector" |
旋转选择器 |
"GameCullSize" |
屏幕裁剪值 |
将世界空间位置转换为屏幕坐标。如果位置在屏幕上可见则返回 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");
}请求宿主扫描背包。传入 -1 扫描所有背包,或传入特定背包 ID。快照中的背包数据在扫描完成后(下一帧)填充。
注意: 背包数据不会自动刷新 — 您必须调用此函数来触发扫描。如果需要持续的背包数据,请定期调用(例如每 2 秒)。
返回可行走网格数据的指针。网格是 2D 数组,其中 0 = 不可行走,非零 = 可行走。如果数据不可用则返回 nullptr。
返回网格位置的地形高度。超出范围或数据不可用时返回 0。
这些函数直接从游戏内存读取 C++ 标准库容器,映射宿主的 Core::Process 方法。
从游戏内存读取 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);
}从游戏内存读取 StdList(16 字节结构体: {Head, Size})。遍历链表并返回连续缓冲区。调用者必须 free()。
从游戏内存读取 StdBucket(读取内嵌的 StdVector)。调用者必须 free()。
遍历 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);从游戏内存读取 StdWString(带内联/堆缓冲区的 32 字节结构体)。
返回背包 ID 的人类可读名称(例如 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1")。
SDK v4 提供对宿主调试数据的直接访问 — 实体组件、背包详情和 UI 元素树 — 对应内置的 Debug 选项卡。
返回包含调试元数据(Id、Address、Path、Type、SubType、State、Rarity、Zone)的所有实体列表。对应 Debug→Entity List 选项卡。
开始监视实体的组件。宿主的工作线程将每帧读取此实体的完整组件数据。
停止监视实体的组件。当用户折叠实体树节点时调用此函数以释放资源。
返回被监视实体的完整组件数据。包含所有 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);
}
}返回 ServerData 组件的基地址。
返回所有玩家背包 ID 及其地址(来自 ServerData)。
开始监视背包以进行详细调试检查。宿主读取槽位占用情况、物品详情和词缀。
返回当前被监视背包的完整数据: 网格尺寸、槽位占用情况、带稀有度和词缀的物品。
// 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 根元素地址(用于游戏内 UI 树导航)。
返回顶层 UI 根地址。
返回当前 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)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)— 稀有度显示辅助函数
写入宿主的日志系统。级别: "Debug", "Info", "Warning", "Error"
宿主的 ImGui 上下文。在 SetContext() 中调用 ImGui::SetCurrentContext()。
宿主的 ID3D11Device*。强制转换后用于加载纹理。
所有类型位于 PluginSDK 命名空间中。插件通常添加 using namespace PluginSDK;。
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 区域)。
活跃的增益/减益:
| 字段 | 类型 | 说明 |
|---|---|---|
Name |
string |
内部增益名称(例如 "flask_effect_life") |
TimeLeft |
float |
剩余秒数 |
Charges |
short |
层数 |
TotalTime |
float |
总持续时间 |
小地图/大地图状态:
| 字段 | 类型 | 说明 |
|---|---|---|
CenterX/Y |
float |
地图中心 |
SizeX/Y |
float |
地图尺寸 |
ShiftX/Y |
float |
当前平移偏移 |
DefaultShiftX/Y |
float |
默认偏移值 |
Zoom |
float |
缩放级别 |
Scale |
float |
地图缩放因子 |
IsVisible |
bool |
地图当前是否显示 |
| 字段 | 类型 | 说明 |
|---|---|---|
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
由 ReadExtendedItemMods() 返回:
| 字段 | 类型 | 说明 |
|---|---|---|
ImplicitMods |
vector<ItemModData> |
隐性词缀 |
ExplicitMods |
vector<ItemModData> |
显性词缀 |
EnchantMods |
vector<ItemModData> |
附魔词缀 |
HellscapeMods |
vector<ItemModData> |
Hellscape 词缀 |
CrucibleMods |
vector<ItemModData> |
Crucible 词缀 |
Rarity |
int |
0=普通, 1=魔法, 2=稀有, 3=传奇 |
| 字段 | 类型 | 说明 |
|---|---|---|
Key |
string |
词缀统计键 |
Values |
vector<float> |
词缀掷骰值 |
每实体缓存的组件地址(通过 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() |
来自 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>> |
所有组件名称→地址对 |
来自 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 |
来自 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> |
带路径、稀有度和词缀的物品 |
| 字段 | 类型 | 说明 |
|---|---|---|
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 词缀 |
| 字段 | 类型 | 说明 |
|---|---|---|
Name |
string |
技能名称 |
UseStage |
int |
当前使用阶段 |
CastType |
int |
施放类型 |
TotalUses |
int |
总使用次数 |
TotalCooldownTimeInMs |
int |
冷却时间(毫秒) |
CanBeUsed |
bool |
技能是否当前可用 |
| 字段 | 类型 | 说明 |
|---|---|---|
Name |
string |
内部增益名称 |
TotalTime |
float |
总持续时间 |
TimeLeft |
float |
剩余秒数 |
Charges |
short |
层数 |
FlaskSlot |
short |
药剂栏位索引 |
Effectiveness |
short |
增益效能 |
SourceEntityId |
uint32_t |
施加此增益的实体 |
| 字段 | 类型 | 说明 |
|---|---|---|
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)
宿主和插件共享同一个 ImGui 上下文。您必须在 SetContext() 方法中调用:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));始终使用唯一的窗口 ID 以避免与宿主或其他插件冲突:
ImGui::Begin("My Window##MyPluginName", &showWindow);- 窗口、选项卡、树、表格、绘制列表
- 通过 D3D11 设备加载纹理
- 通过
ImGui::GetBackgroundDrawList()进行覆盖层渲染 - 通过
#include "imgui/IconsFontAwesome6.h"使用 FontAwesome 6 图标(例如ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)
使用 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() 调用直接渲染到游戏屏幕上。
宿主覆盖层使用 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 - 将位置保存到设置文件中,使其在会话之间持久化
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);
}auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100auto 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;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 以避免误报。
// 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);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));
}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;
}
}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 */ }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
}
}// 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);
}
}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());
}
}
}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);
}
}
}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());
}| 设置 | 值 |
|---|---|
| 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,必须将合并源码直接编译到 DLL 中 — Windows LoadLibrary 不会在 DLL 自身目录中搜索依赖项,因此动态链接 sqlite3.dll 将以错误 126 失败。
步骤:
- 将
sqlite3.c和sqlite3.h复制到插件的lib/目录 - 创建
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
- 将
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>
要从图像文件(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。
- 以 Debug 模式构建插件 DLL
- 启动宿主应用程序
- 在 Visual Studio 中: Debug → Attach to Process → 选择宿主 .exe
- 在插件源代码中设置断点
- 当代码被调用时调试器将中断
| 问题 | 解决方案 |
|---|---|
| 插件未加载 | 检查 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" 错误 | 确保 CreatePlugin 和 DestroyPlugin 使用 extern "C" 导出 |
| 插件导致宿主崩溃 | 这不应该发生 — 所有插件调用都受 SEH 保护。检查日志。 |
| 数据过时 |
GetSnapshot() 返回最新帧的数据。不要缓存指针。 |
| 内存读取返回 0 | 确认 IsAttached() 为 true 且地址有效 |
| WorldToScreen 返回 false | 位置可能在摄像机后方或屏幕外 |
| 覆盖层窗口不可点击 | 菜单隐藏时宿主使用 WS_EX_TRANSPARENT。使用 IsMenuVisible() 仅在菜单活动时显示交互控件。见第 6 节的可拖拽覆盖层模式。 |
| 击杀/死亡检测不工作 | 死亡实体从快照中移除。使用基于消失的检测而非 HP 转变。见第 8 节。 |
| C2491 "dllimport function" 错误 | 第三方库头文件定义了 __declspec(dllimport)。创建一个本地覆盖头文件,将 API 宏设置为空(见第 9 节的 SQLite3 示例)。 |