-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - x64 Release build
-
Runtime Library:
/MD(Multi-threaded DLL) — must match the host
- Create a new C++ DLL project in Visual Studio
- Set the include path to point to the POEFixer source directory (for SDK and ImGui headers)
- Add ImGui source files to your project:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Include the Plugin SDK headers in your plugin source:
Or use the convenience header from 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
- Define
PLUGIN_EXPORTSand_CRT_SECURE_NO_WARNINGSin your project's Preprocessor Definitions
Plugins/
YourPlugin/
YourPlugin.dll <-- DLL name MUST match folder name
config/
settings.txt <-- Optional settings file
data/
... <-- Optional data directory (databases, caches, etc.)
The ExamplePlugin demonstrates the recommended project layout:
Plugins/ExamplePlugin/
ExamplePlugin.cpp <-- Main plugin entry point + factory exports
sdk/
PluginHelpers.h <-- MemoryReader, WideToNarrow, entity/rarity helpers
examples/
ExampleBuffs.h <-- Buff list with filtering and progress bars
ExampleEntities.h <-- Entity debug list with watch mechanism, component trees, JSON dump
ExampleInventory.h <-- ServerData, inventory selector, slot grid, item mods with rarity
ExampleMemory.h <-- Hex viewer, Read<T> demo, pattern scanner
ExampleUiExplorer.h <-- Full UI element explorer with search, navigation, highlighting
A more complete plugin example with SQLite3, icon atlas, and overlay rendering:
Plugins/KillCount/
KillCount.cpp <-- Main plugin entry point, IPlugin lifecycle, settings UI
KillCount.h <-- Plugin class declaration
KillTracker.cpp/h <-- Kill/chest/death counting engine
OverlayRenderer.cpp/h <-- ImGui overlay with drag-to-reposition pattern
IconAtlas.cpp/h <-- Sprite sheet texture loading (D3D11 + stb_image)
Database.cpp/h <-- SQLite3 wrapper for persistent stats
DisplaySettings.h <-- Settings struct
sdk/
PluginHelpers.h <-- Copied from ExamplePlugin
lib/
sqlite3.c/h <-- SQLite3 amalgamation (compiled as C)
sqlite3-vcpkg-config.h <-- Local override for static linking
The DLL file name must match the folder name exactly:
- Folder:
Plugins/MyPlugin/→ DLL:MyPlugin.dll - The host scans each subfolder in
Plugins/and looks for<FolderName>.dll
Load DLL (LoadLibrary)
→ CreatePlugin() -- Factory: instantiate your IPlugin
→ SetContext(ctx) -- Receive host services
→ SetPluginDirectory(dir) -- Receive your folder path
→ GetSDKVersion() -- Compatibility check
→ GetName() -- Display name for UI
→ [if enabled] OnEnable() -- Initialize resources
↓
Main Loop (every frame):
→ DrawUI() -- Render your overlay (only if enabled)
→ DrawSettings() -- Render settings in Plugins tab
→ WantsOverlay() -- Host checks if plugin wants overlay mode
↓
Periodically / on shutdown:
→ SaveSettings() -- Persist your settings
↓
→ OnDisable() -- Cleanup resources
→ DestroyPlugin(plugin) -- Factory: delete your IPlugin
→ FreeLibrary -- Unload DLL
- All
Draw*methods are called on the main/render thread -
GetSnapshot()and other PluginContext functions are thread-safe - Do NOT spawn threads that call ImGui — ImGui is not thread-safe
Every plugin must implement the IPlugin interface (defined in plugin_sdk/PluginAPI.h):
- When called: Once, immediately after creation
-
Parameter: Relative path like
"Plugins/YourPlugin" - Purpose: Store this path for loading settings/resources
-
When called: Once, after
SetPluginDirectory -
Parameter: Pointer to the host's
PluginContext(valid for plugin lifetime) - Purpose: Store this pointer — it's your gateway to all game data
-
Important: Call
ImGui::SetCurrentContext(ctx->ImGuiContext)here
- When called: When user enables the plugin, or on startup if previously enabled
-
Parameter:
trueif the game process is currently attached - Purpose: Load settings, allocate resources, initialize state
- When called: When user disables the plugin
- Purpose: Release resources, stop background work
- When called: Every frame, only when plugin is enabled
- Purpose: Render your overlay using ImGui
-
Note: Use unique window IDs like
"MyWindow##MyPlugin"to avoid conflicts
- When called: Every frame, in the Plugins settings tab (only when enabled)
- Purpose: Render plugin configuration using ImGui
- When called: Periodically and on application shutdown
-
Purpose: Save your settings to disk (e.g.,
Plugins/YourPlugin/config/settings.txt)
-
Returns: Display name shown in the Plugins tab (e.g.,
"My Plugin")
-
Returns:
PLUGIN_SDK_VERSION(currently 4) - Purpose: Host checks this for compatibility — must match
-
Returns:
trueif the plugin wants to render in overlay mode (transparent overlay on top of game) -
Default:
false— plugin only renders in normal settings window -
Purpose: When any plugin returns
true, the host enters overlay mode even if no built-in features require it
Your DLL must export these two C functions:
extern "C" PLUGIN_API IPlugin* CreatePlugin() {
return new MyPlugin();
}
extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
delete plugin;
}The PluginContext struct (defined in plugin_sdk/PluginContext.h) provides function pointers for accessing game data. All types are in the PluginSDK namespace.
Returns a complete snapshot of the game state. Updated once per frame. Contains:
| Field | Type | Description |
|---|---|---|
CurrentState |
GameStateTypes |
Current game state |
CurrentAreaName |
string |
Area name (e.g., "The Riverways") |
CurrentAreaHash |
string |
Unique area instance hash |
CurrentAreaLevel |
uint8_t |
Monster level of current area |
IsTown |
bool |
True if in town |
IsHideout |
bool |
True if in hideout |
IsPaused |
bool |
True if game is paused |
IsSkillTreeVisible |
bool |
True if skill tree panel is open |
WorldToGridConvertor |
float |
Conversion factor for world→grid |
Player |
RadarEntity |
Local player entity data |
Entities |
vector<RadarEntity> |
All nearby entities |
LargeMap / MiniMap
|
MapData |
Map overlay data |
Vitals |
PlayerVitals |
Player HP/ES/MP + buffs |
ScreenWidth / ScreenHeight
|
int |
Game window dimensions |
ProcessId |
DWORD |
Game process ID |
GameWindow |
HWND |
Game window handle |
GameWindowForeground |
bool |
True if game is foreground |
IsAttached |
bool |
True if attached to game process |
IsWindowValid |
bool |
True if game window is valid |
LastUpdateTime |
uint64_t |
Timestamp of last data update |
AreaChangeCounter |
uint64_t |
Increments on area change |
Inventories |
vector<InventoryInfo> |
Player inventory contents |
CurrencyTotals |
map<string,int> |
Currency counts by path |
InventoryGrid |
InventoryGridInfo |
Inventory UI grid info |
WorldToScreenMatrix |
XMFLOAT4X4 |
3D→2D projection matrix |
Important: Entity Filtering Dead entities (those with
EntityState == Useless) are filtered OUT of the snapshot before plugins receive it. This means you will never observe an HP transition from alive to dead. If you need to detect kills, use disappearance-based detection instead — track entity IDs by zone and count them as killed when they disappear from the entity list while inInnerCircleorOuterCircleproximity. See Section 8: Common Recipes for details.
Convenience shortcut for player vitals.
Returns the current game state enum.
True if the game process is attached and readable.
True if currently in game (not loading, not on login screen).
True if the game window is the foreground window.
Returns the game process ID.
Read all mods from an item entity.
Returns: 0=Normal, 1=Magic, 2=Rare, 3=Unique
Returns stack count for currency/stackable items.
Returns the item's base type name.
Returns the item's metadata path.
Returns the item's human-readable display name (e.g., "Divine Orb", "Chaos Orb"). Unlike ReadItemName which returns the metadata path, this reads the actual name from BaseItemTypeData.DisplayNamePtr.
Returns true if the host is currently in overlay mode (transparent overlay on top of the game window). Use this to adjust your rendering — e.g., draw on game overlay vs. draw in a settings window.
Returns true when the host's settings menu is visible (overlay is interactive). When the menu is hidden, the overlay window is click-through (WS_EX_TRANSPARENT), so ImGui windows cannot receive mouse input.
Use this to implement the draggable overlay pattern:
- Menu visible: Show a drag handle, allow interaction (tabs, buttons)
-
Menu hidden: Remove drag handle, add
ImGuiWindowFlags_NoInputsto make the window non-interactive
See Section 6: Draggable Overlay Pattern for the full implementation.
Direct access to game process memory. All reads are safe (return 0/empty on failure).
Returns the game executable module base address. Returns 0 if not attached.
Returns the game module size in bytes. Returns 0 if not attached.
Read a block of raw bytes from the game process. The buffer must have at least size bytes allocated. Returns true on success.
// 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));Read a null-terminated ASCII string from game memory (max 128 chars).
Read a null-terminated Unicode (wide) string from game memory (max 128 wchars).
Get a resolved pattern scan address by name. Returns 0 if not found.
Standard patterns:
| Name | Description |
|---|---|
"Game States" |
GameStates vector root |
"File Root" |
File registry |
"AreaChangeCounter" |
Area transition counter |
"Terrain Rotator Helper" |
Rotation data |
"Terrain Rotation Selector" |
Rotation selector |
"GameCullSize" |
Screen cull value |
Convert a world-space position to screen coordinates. Returns true if the position is visible on screen.
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");
}Request the host to scan inventories. Pass -1 to scan all inventories, or a specific inventory ID. Inventory data in the snapshot is populated after the scan completes (next frame).
Note: Inventory data is not automatically refreshed — you must call this function to trigger a scan. Call it periodically (e.g., every 2 seconds) if you need continuous inventory data.
Returns a pointer to the walkable grid data. The grid is a 2D array where 0 = not walkable, non-zero = walkable. Returns nullptr if data is not available.
Returns the terrain height at a grid position. Returns 0 if out of bounds or data unavailable.
These functions read C++ standard library containers directly from game memory, mirroring the host's Core::Process methods.
Read a StdVector (24-byte struct: {First, Last, End}) from game memory. Returns a malloc'd buffer of elements. Caller must free() the returned pointer. Returns nullptr on failure.
// 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);
}Read a StdList (16-byte struct: {Head, Size}) from game memory. Traverses the linked list and returns a contiguous buffer. Caller must free().
Read a StdBucket from game memory (reads the embedded StdVector). Caller must free().
Traverse a StdMap (16-byte struct: {Head, Size}) and call callback for each key-value pair. Returns the number of nodes visited.
// 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);Read a StdWString (32-byte struct with inline/heap buffer) from game memory.
Returns the human-readable name for an inventory ID (e.g., 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1").
SDK v4 provides direct access to the host's debug data — entity components, inventory details, and UI element tree — matching the built-in Debug tabs.
Returns a list of all entities with debug metadata (Id, Address, Path, Type, SubType, State, Rarity, Zone). This mirrors the Debug→Entity List tab.
Start watching an entity's components. The host's worker thread will read full component data for this entity each frame.
Stop watching an entity's components. Call this when the user collapses the entity tree node to free resources.
Returns the full component data for a watched entity. Contains sub-structs for all 8 recognized components (Life, Render, Positioned, Targetable, Animated, Stats, Actor, Buffs) plus the list of all component addresses.
// 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);
}
}Returns the ServerData component base address.
Returns all player inventory IDs and their addresses (from ServerData).
Start watching an inventory for detailed debug inspection. The host reads slot occupancy, item details, and mods.
Returns full data for the currently watched inventory: grid dimensions, slot occupancy, items with rarity and mods.
// 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()));
}Returns the root game UI element address (for in-game UI tree navigation).
Returns the top-level UI root address.
Returns the current GameCullSize value, used for UI scale calculations. Combined with screen dimensions, this enables accurate UI element position/size computation.
// 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)The sdk/PluginHelpers.h header (included with ExamplePlugin) provides a type-safe MemoryReader class that wraps the raw PluginContext functions:
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);The MemoryReader also provides convenience wrappers for ReadString(), ReadUnicodeString(), GetBaseAddress(), GetModuleSize(), and GetPatternAddress().
Additional utilities in PluginHelpers.h:
-
WideToNarrow(wstring)— safe wstring→string conversion (ASCII lossy) -
GetEntityTypeName(type)— enum to display name (including ExpeditionMarker/ExpeditionRemnant) -
GetNearbyZoneName(zone)— zone to display name -
GetRarityName(rarity)/GetRarityColor(rarity)— rarity display helpers
Write to the host's log system. Levels: "Debug", "Info", "Warning", "Error"
The host's ImGui context. Call ImGui::SetCurrentContext() with this in SetContext().
The host's ID3D11Device*. Cast and use for loading textures.
All types are in the PluginSDK namespace. Plugins typically add using namespace PluginSDK;.
Per-entity data available in snapshot->Entities:
| Field | Type | Description |
|---|---|---|
Id |
uint32_t |
Unique entity ID |
Address |
uintptr_t |
Memory address (for item API calls) |
EntityDetailsAddress |
uintptr_t |
Entity details struct address |
RenderComponentAddress |
uintptr_t |
Render component address (shortcut) |
IsValid |
bool |
Entity validity flag |
entityType |
EntityTypes |
Entity category |
entitySubtype |
EntitySubtypes |
Entity subcategory |
entityState |
EntityStates |
Entity state |
Rarity |
int |
0=Normal, 1=Magic, 2=Rare, 3=Unique |
Reaction |
uint8_t |
0=Hostile, 1=Neutral, 2=Friendly |
GridPositionX/Y |
float |
Position on terrain grid |
TerrainHeight |
float |
Terrain height at entity position |
WorldX/Y/Z |
float |
World-space position |
ModelBoundsZ |
float |
Model height |
Path |
wstring |
Entity metadata path |
PlayerName |
wstring |
Player name (if player entity) |
TgtPath |
string |
Target path (narrow string) |
CurrentHP/MaxHP |
int |
Entity health |
CurrentES/MaxES |
int |
Entity energy shield |
IsSleeping |
bool |
Far-away entity flag |
IsChestOpened |
bool |
Chest opened state |
Zone |
NearbyZone |
Proximity to player |
ComponentCache |
EntityComponentCache |
Component addresses |
Important: Dead entities (
EntityState::Useless) are removed from the snapshot before it reaches plugins. You will never see a monster's HP drop to zero — it simply disappears from the list. Use theZonefield to distinguish kills (entity disappeared from InnerCircle/OuterCircle) from entities going out of range (entity was in Far zone).
Active buff/debuff:
| Field | Type | Description |
|---|---|---|
Name |
string |
Internal buff name (e.g., "flask_effect_life") |
TimeLeft |
float |
Seconds remaining |
Charges |
short |
Stack count |
TotalTime |
float |
Total duration |
Minimap/largemap state:
| Field | Type | Description |
|---|---|---|
CenterX/Y |
float |
Map center |
SizeX/Y |
float |
Map dimensions |
ShiftX/Y |
float |
Current pan offset |
DefaultShiftX/Y |
float |
Default shift values |
Zoom |
float |
Zoom level |
Scale |
float |
Map scale factor |
IsVisible |
bool |
Map is currently shown |
| Field | Type | Description |
|---|---|---|
Id |
int |
Inventory ID |
TotalBoxesX/Y |
int |
Grid dimensions |
Ptr |
uintptr_t |
Inventory memory address |
Items |
vector<InventoryItemInfo> |
Items in inventory |
Item fields: Address, Name (metadata path), Path (same as Name), DisplayName (human-readable, e.g. "Divine Orb"), SlotX/Y, Width/Height, StackCount, IsCurrency
Returned by ReadExtendedItemMods():
| Field | Type | Description |
|---|---|---|
ImplicitMods |
vector<ItemModData> |
Implicit modifiers |
ExplicitMods |
vector<ItemModData> |
Explicit modifiers |
EnchantMods |
vector<ItemModData> |
Enchant modifiers |
HellscapeMods |
vector<ItemModData> |
Hellscape modifiers |
CrucibleMods |
vector<ItemModData> |
Crucible modifiers |
Rarity |
int |
0=Normal, 1=Magic, 2=Rare, 3=Unique |
| Field | Type | Description |
|---|---|---|
Key |
string |
Mod stat key |
Values |
vector<float> |
Mod roll values |
Component addresses cached per entity (available via RadarEntity.ComponentCache):
| Field | Type | Has-method |
|---|---|---|
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() |
Entity metadata from GetEntityDebugList():
| Field | Type | Description |
|---|---|---|
Id |
uint32_t |
Entity ID |
Address |
uintptr_t |
Memory address |
Path |
string |
Metadata path |
EntityType |
int |
Entity type (cast to EntityTypes) |
EntitySubType |
int |
Entity subtype (cast to EntitySubtypes) |
EntityState |
int |
Entity state (cast to EntityStates) |
Rarity |
int |
0=Normal, 1=Magic, 2=Rare, 3=Unique |
Zone |
NearbyZone |
Proximity to player |
ComponentAddresses |
vector<pair<string,uintptr_t>> |
All component name→address pairs |
Full component data from GetWatchedEntityData():
| Field | Type | Description |
|---|---|---|
EntityId |
uint32_t |
Which entity this data is for |
Valid |
bool |
Whether data was successfully read |
HasLife / Life
|
bool / DebugLifeComp
|
Life component — Life.Health, Life.EnergyShield, Life.Mana (each a DebugVital with .Current, .Total, .Regeneration, .ReservedFlat, .ReservedPercent) |
HasRender / Render
|
bool / DebugRenderComp
|
Position (WorldX/Y/Z, GridX/Y), TerrainHeight, ModelBounds (X/Y/Z) |
HasPositioned / Positioned
|
bool / DebugPositionedComp
|
Reaction value, IsFriendly flag |
HasTargetable / Targetable
|
bool / DebugTargetableComp
|
IsTargetable, IsHighlightable, IsTargettedByPlayer, HiddenFromPlayer, NeedsTrue, MeetsQuestState, NeedsFalse |
HasAnimated / Animated
|
bool / DebugAnimatedComp
|
Animation Path (string), Id (uint32) |
HasStats / Stats
|
bool / DebugStatsComp
|
CurrentWeaponIndex, IsShapeshifted, StatsItems/StatsBuff (vectors of stat ID→value pairs) |
HasActor / Actor
|
bool / DebugActorComp
|
AnimationId, AnimationName, ActiveSkills (vector of DebugActiveSkill), DeployedCounts[256] |
HasBuffs / Buffs
|
bool / vector<DebugBuff>
|
Active buffs: Name, TotalTime, TimeLeft, Charges, FlaskSlot, Effectiveness, SourceEntityId |
Inventory details from GetWatchedInventoryData():
| Field | Type | Description |
|---|---|---|
InventoryId |
int |
Inventory ID (-1 if none) |
Address |
uintptr_t |
Inventory address |
TotalBoxesX/Y |
int |
Grid dimensions |
ServerRequestCounter |
int |
Server sync counter |
SlotOccupied |
vector<bool> |
Per-slot occupancy |
Items |
vector<DebugInventoryItem> |
Items with path, rarity, and mods |
| Field | Type | Description |
|---|---|---|
Address |
uintptr_t |
Item entity address |
Path |
string |
Item metadata path |
DisplayName |
string |
Human-readable item name |
SlotX / SlotY
|
int |
Grid position in inventory |
Rarity |
int |
0=Normal, 1=Magic, 2=Rare, 3=Unique |
ItemLevel |
int |
Item level |
RequiredLevel |
int |
Required character level |
IsIdentified |
bool |
Whether item is identified |
IsCorrupted |
bool |
Whether item is corrupted |
CraftedModCount |
int |
Number of crafted mods |
ImplicitMods |
vector<DebugModInfo> |
Implicit modifiers |
ExplicitMods |
vector<DebugModInfo> |
Explicit modifiers |
EnchantMods |
vector<DebugModInfo> |
Enchant modifiers |
HellscapeMods |
vector<DebugModInfo> |
Hellscape modifiers |
| Field | Type | Description |
|---|---|---|
Name |
string |
Skill name |
UseStage |
int |
Current use stage |
CastType |
int |
Cast type |
TotalUses |
int |
Total uses count |
TotalCooldownTimeInMs |
int |
Cooldown in milliseconds |
CanBeUsed |
bool |
Whether skill can currently be used |
| Field | Type | Description |
|---|---|---|
Name |
string |
Internal buff name |
TotalTime |
float |
Total duration |
TimeLeft |
float |
Seconds remaining |
Charges |
short |
Stack count |
FlaskSlot |
short |
Flask slot index |
Effectiveness |
short |
Buff effectiveness |
SourceEntityId |
uint32_t |
Entity that applied this buff |
| Field | Type | Description |
|---|---|---|
Name |
string |
Mod display name |
StatKey |
string |
Stat key identifier |
AffixName |
string |
Affix name |
GenerationType |
int |
1=Prefix, 2=Suffix, 3=Implicit |
Value0 |
float |
First value (NaN if none) |
Value1 |
float |
Second value (NaN if none) |
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 grid units), OuterCircle(2, ~120 grid units), 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)
The host and plugin share the same ImGui context. You must call:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));in your SetContext() method.
Always use unique window IDs to avoid conflicts with host or other plugins:
ImGui::Begin("My Window##MyPluginName", &showWindow);- Windows, tabs, trees, tables, draw lists
- Texture loading via D3D11 device
- Overlay rendering via
ImGui::GetBackgroundDrawList() - FontAwesome 6 icons via
#include "imgui/IconsFontAwesome6.h"(e.g.,ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)
Use WorldToScreen() to draw labels/shapes at entity positions:
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));
}Override WantsOverlay() to return true to request the host to enter overlay mode:
bool WantsOverlay() override { return m_OverlayEnabled; }When in overlay mode, the host window is transparent and positioned over the game. Your DrawUI() calls render directly onto the game screen.
The host overlay uses WS_EX_TRANSPARENT to make the window click-through when the menu is hidden. This means ImGui windows cannot receive mouse input unless the menu is visible. To create a draggable overlay window (like the built-in Vitals Overlay), use this dual-mode pattern:
#include "imgui/IconsFontAwesome6.h"
void MyPlugin::RenderOverlay() {
bool menuVisible = m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false;
if (menuVisible) {
// === DRAGGABLE MODE ===
// Window with background, drag hint, interactive controls
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 {
// === NON-INTERACTIVE MODE ===
// Static overlay — no drag, no mouse interaction
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();
}
}Key points:
-
ImGuiCond_Appearingsets position only on first show; ImGui then tracks drag position -
NoTitleBar+ noNoMove= window is draggable from any empty area (ImGui default behavior) -
NoInputsin non-interactive mode prevents the overlay from stealing focus throughWS_EX_TRANSPARENT - Always null-check
IsMenuVisiblepointer for backward compatibility:m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false - Save position to your settings file so it persists across sessions
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
}Store settings in <PluginDirectory>/config/:
std::filesystem::path settingsPath =
std::filesystem::path(m_Directory) / "config" / "settings.txt";For plugins with many settings, a simple key=value text format works well:
// 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...
}Dead entities are filtered out of the snapshot (EntityState::Useless), so you cannot detect HP dropping to 0. Instead, track entities by ID and detect when they disappear from nearby zones:
// 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;
}
}
}Why this works: Entities within ~120 grid units that suddenly vanish were almost certainly killed (not just walked out of range). Entities in the Far zone naturally pop in and out of the entity list — don't count those.
Important: Clear m_PrevEntities on area change (AreaChangeCounter changed) to avoid false positives.
// 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.DisplayName.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());
}| Setting | Value |
|---|---|
| Configuration | Release |
| Platform | x64 |
| C++ Standard | /std:c++20 |
| Runtime Library |
/MD (Multi-threaded DLL) |
| Configuration Type | DLL |
- Your plugin
.cppfile(s) - ImGui source files:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Include path to POEFixer root (for SDK headers and ImGui headers)
- Optional: Copy
Plugins/ExamplePlugin/sdk/PluginHelpers.hfor theMemoryReaderwrapper and utility functions
Your .vcxproj should have these additional include directories:
<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>If using local third-party libraries (e.g., SQLite3 in a lib/ subfolder), add $(ProjectDir)lib before the solution path so local headers take priority:
<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>To use SQLite3 in a plugin, you must compile the amalgamation source directly into your DLL — Windows LoadLibrary does not search the DLL's own directory for dependencies, so dynamically linking sqlite3.dll will fail with error 126.
Steps:
- Copy
sqlite3.candsqlite3.hinto your plugin'slib/directory - Create
lib/sqlite3-vcpkg-config.hto overrideSQLITE_API(prevents__declspec(dllimport)errors):#ifndef SQLITE_API #define SQLITE_API #endif #define SQLITE_ENABLE_UNLOCK_NOTIFY 1 #define SQLITE_OS_WIN 1 #define SQLITE_ENABLE_COLUMN_METADATA 1
- Add
sqlite3.cto your.vcxprojas a C file with warnings disabled:<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>
For loading textures from image files (PNG, JPG), include stb_image in one .cpp file:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"Then use D3D11 device from m_Context->D3DDevice to create GPU textures.
Set your output directory to:
$(SolutionDir)x64\Release\Plugins\YourPlugin\
Copy your built DLL to Plugins/YourPlugin/YourPlugin.dll next to the main executable.
- Build your plugin DLL in Debug mode
- Start the host application
- In Visual Studio: Debug → Attach to Process → select the host .exe
- Set breakpoints in your plugin source
- The debugger will break when your code is called
| Problem | Solution |
|---|---|
| Plugin not loading | Check DLL name matches folder name exactly |
| "SDK version mismatch" | Rebuild plugin with latest SDK headers (current version: 4) |
| LoadLibrary error 126 | DLL has unresolved dependencies. For third-party libs like SQLite3, compile them statically into the DLL (see Section 9). Use dumpbin /dependents YourPlugin.dll to check. |
| Crash on load | Check CRT mismatch — both must use /MD
|
| ImGui not rendering | Ensure ImGui::SetCurrentContext() is called in SetContext()
|
| Data is empty/zero | Check IsAttached() and IsInGame() before reading data |
| Inventory is empty | Call RequestInventoryScan(-1) — inventory data is on-demand |
| "Missing exports" error | Ensure CreatePlugin and DestroyPlugin are exported with extern "C"
|
| Plugin crashes host | This shouldn't happen — all plugin calls are SEH-protected. Check the logs. |
| Stale data |
GetSnapshot() returns the latest frame's data. Don't cache the pointer. |
| Memory read returns 0 | Verify IsAttached() is true and the address is valid |
| WorldToScreen returns false | The position may be behind the camera or off-screen |
| Overlay window not clickable | The host uses WS_EX_TRANSPARENT when menu is hidden. Use IsMenuVisible() to show interactive controls only when menu is active. See the Draggable Overlay Pattern in Section 6. |
| Kill/death detection doesn't work | Dead entities are removed from the snapshot. Use disappearance-based detection instead of HP transition. See Section 8. |
| C2491 "dllimport function" errors | Your third-party library headers define __declspec(dllimport). Create a local override header that sets the API macro to empty (see SQLite3 example in Section 9). |