-
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(当前为 5) - 用途: 宿主检查兼容性 — 必须匹配
-
返回值: 如果插件想在覆盖层模式(游戏上方的透明覆盖层)中渲染则为
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)(Translation pending)
SDK v5 adds direct UI element reading without the debug watch mechanism. Navigate the UI tree, check visibility, read text, and compute screen rectangles.
| Function | Return | Purpose |
|---|---|---|
ReadUiElement(addr) |
UiElementData |
Read core UI element properties |
GetUiChildren(addr) |
vector<uintptr_t> |
Get all child element addresses |
GetUiChildAt(addr, index) |
uintptr_t |
Get single child by index |
ReadUiChildChain(root, indices, count) |
uintptr_t |
Navigate child index path from root |
IsUiElementVisible(addr) |
bool |
Check visibility (including ancestors) |
GetUiStringId(addr) |
string |
Get element's string identifier |
ComputeUiScreenRect(addr, outX, outY, outW, outH) |
bool |
Compute screen rectangle with recursive scaling |
GetUiText(addr) |
string |
Get element's display text |
21 typed component reader functions. Each takes a component address from EntityComponentCache and returns a struct with Valid flag.
| Function | Return Struct | Component |
|---|---|---|
ReadLifeComponent(addr) |
PluginLifeData |
Life (HP/ES/Mana) |
ReadRenderComponent(addr) |
PluginRenderData |
Render (position, bounds) |
ReadPositionedComponent(addr) |
PluginPositionedData |
Positioned (reaction) |
ReadTargetableComponent(addr) |
PluginTargetableData |
Targetable (flags) |
ReadChestComponent(addr) |
PluginChestData |
Chest (opened, quality) |
ReadShrineComponent(addr) |
PluginShrineData |
Shrine (available) |
ReadStackComponent(addr) |
PluginStackData |
Stack (size) |
ReadChargesComponent(addr) |
PluginChargesData |
Charges |
ReadPlayerComponent(addr) |
PluginPlayerData |
Player (name, level) |
ReadAnimatedComponent(addr) |
PluginAnimatedData |
Animated (animation) |
ReadTransitionableComponent(addr) |
PluginTransitionableData |
Transitionable |
ReadTriggerableBlockageComponent(addr) |
PluginTriggerableBlockageData |
TriggerableBlockage |
ReadMinimapIconComponent(addr) |
PluginMinimapIconData |
MinimapIcon |
ReadStateMachineComponent(addr) |
PluginStateMachineData |
StateMachine |
ReadBaseComponent(addr) |
PluginBaseData |
Base (cell size, influence) |
ReadModsComponent(addr) |
PluginModsData |
Mods (rarity, mod lists) |
ReadStatsComponent(addr) |
PluginStatsData |
Stats (key-value pairs) |
ReadBuffsComponent(addr) |
PluginBuffsData |
Buffs (active buffs) |
ReadActorComponent(addr) |
PluginActorData |
Actor (skills, deploy) |
ReadNpcComponent(addr) |
PluginNpcData |
NPC (hidden, icon) |
ReadDiesAfterTimeComponent(addr) |
PluginDiesAfterTimeData |
DiesAfterTime |
| Helper | Signature | Description |
|---|---|---|
GetHealthPercent |
float (uintptr_t lifeAddr) |
HP percentage (0-100) |
GetEsPercent |
float (uintptr_t lifeAddr) |
Energy Shield percentage |
GetManaPercent |
float (uintptr_t lifeAddr) |
Mana percentage |
IsAlive |
bool (uintptr_t lifeAddr) |
True if HP > 0 |
IsChestOpenedHelper |
bool (uintptr_t chestAddr) |
True if chest opened |
GetWorldPosition |
bool (uintptr_t renderAddr, float*, float*, float*) |
Extract world position |
GetItemRarityFromMods |
int (uintptr_t modsAddr) |
Item rarity from Mods |
IsItemIdentifiedHelper |
bool (uintptr_t modsAddr) |
True if identified |
GetStackCountHelper |
int (uintptr_t stackAddr) |
Stack count |
GetPlayerNameHelper |
string (uintptr_t playerAddr) |
Player name |
// Read entity health via component reader
auto snapshot = m_Context->GetSnapshot();
for (auto& entity : snapshot->Entities) {
if (entity.ComponentCache.HasLife()) {
auto life = m_Context->ReadLifeComponent(entity.ComponentCache.LifeAddr);
if (life.Valid) {
float hpPct = m_Context->GetHealthPercent(entity.ComponentCache.LifeAddr);
}
}
}
// Navigate UI tree
uintptr_t gameUi = m_Context->GetGameUiRootAddress();
const int path[] = {5, 1, 2};
uintptr_t btn = m_Context->ReadUiChildChain(gameUi, path, 3);
if (m_Context->IsUiElementVisible(btn)) {
float x, y, w, h;
m_Context->ComputeUiScreenRect(btn, &x, &y, &w, &h);
}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() |
ActorAddr |
uintptr_t |
HasActor() |
AnimatedAddr |
uintptr_t |
HasAnimated() |
BaseAddr |
uintptr_t |
HasBase() |
ChargesAddr |
uintptr_t |
HasCharges() |
ModsAddr |
uintptr_t |
HasMods() |
StackAddr |
uintptr_t |
HasStack() |
TransitionableAddr |
uintptr_t |
HasTransitionable() |
StateMachineAddr |
uintptr_t |
HasStateMachine() |
来自 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) |
(Translation pending)
| Field | Type | Description |
|---|---|---|
Valid |
bool |
Whether the read succeeded |
X / Y
|
float |
Element position |
Width / Height
|
float |
Element size |
ScaleX / ScaleY
|
float |
Scale factors |
IsVisible |
bool |
Visibility flag |
IsEnabled |
bool |
Enabled flag |
ChildCount |
int |
Number of children |
ParentAddr |
uintptr_t |
Parent element address |
SelfAddr |
uintptr_t |
Self pointer |
All component reader functions return structs with a Valid field. See the English guide for full field listings of all 22 structs (PluginLifeData, PluginRenderData, PluginPositionedData, PluginTargetableData, PluginChestData, PluginShrineData, PluginStackData, PluginChargesData, PluginPlayerData, PluginAnimatedData, PluginTransitionableData, PluginTriggerableBlockageData, PluginMinimapIconData, PluginStateMachineData, PluginBaseData, PluginModsData, PluginStatsData, PluginBuffsData, PluginActorData, PluginNpcData, PluginDiesAfterTimeData).
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 头文件重新构建插件(当前版本: 5) |
| 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 示例)。 |