Skip to content

Plugin Development Guide ES

Lafko edited this page Jun 24, 2026 · 14 revisions

← Home


Guía de Desarrollo de Plugins

Los plugins de POEFixer son DLLs nativas en C++ cargadas en tiempo de ejecución desde Plugins/<PluginName>/<PluginName>.dll. Leen el estado del juego en vivo, dibujan superposiciones con ImGui, persisten su propia configuración y se suscriben a eventos del anfitrión.


1. Visión general

El SDK de plugins tiene una arquitectura de tres capas:

Plugin DLL  ───►  PluginSDK.h  (header-only C++ wrapper, owns std::string/vector/function)
                         │
                         ▼  inline function-pointer calls only
                  HostAbi  (pure-C ABI, POD structs only)
                         │
                         ▼  SEH-wrapped on the host side
   Host bridge: plugin_manager/bridge/Bridge_<Service>.cpp  (10 files)
                         │
                         ▼
                   GameClient + GameLibrary
  • Los autores de plugins incluyen exactamente una cabecera: POEFixer/plugin_sdk/PluginSDK.h.
  • Esa cabecera declara todo dentro del espacio de nombres PluginSDK:: e importa internamente la ABI de C desde PluginAbi.h. Puedes mencionar que la última existe; casi nunca tendrás que mirarla.
  • Todos los contenedores std::* viven dentro de la DLL del plugin. Solo POD cruza la frontera del anfitrión. Esto significa que un plugin compilado con una versión de toolchain no puede enredarse con la STL del anfitrión — los únicos tipos compartidos son enteros, flotantes, punteros y pequeñas estructuras.

Encuentra las cabeceras del SDK en:

  • POEFixer/plugin_sdk/PluginSDK.h — el envoltorio C++ que utilizan los autores de plugins.
  • POEFixer/plugin_sdk/PluginAbi.h — la ABI pura en C que está por debajo.

Plugins de referencia incluidos con el repositorio (léelos como documentación): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.


2. Plugin hola-mundo

Plugin mínimo que se carga e imprime un mensaje en el registro del anfitrión:

#define PLUGIN_EXPORTS
#include "POEFixer/plugin_sdk/PluginSDK.h"

class HelloPlugin : public PluginSDK::Plugin {
public:
    const char* GetName() const override { return "Hello"; }
    void OnEnable(bool) override { ctx()->Log.Info("Hello, world"); }
};

extern "C" PLUGIN_API PluginSDK::Plugin* CreatePlugin()  { return new HelloPlugin(); }
extern "C" PLUGIN_API void               DestroyPlugin(PluginSDK::Plugin* p) { delete p; }

Compílalo como Plugins/Hello/Hello.dll, reinicia el anfitrión y habilítalo desde la pestaña Plugins.


3. Configuración del proyecto

Usa Plugins/ExamplePlugin/ExamplePlugin.vcxproj como plantilla canónica. Los ajustes esenciales:

  • Tipo de configuración: DynamicLibrary
  • Conjunto de herramientas de plataforma: v143 (Visual Studio 2022)
  • Conjunto de caracteres: Unicode
  • Estándar del lenguaje: stdcpp20
  • Biblioteca en tiempo de ejecución: MultiThreadedDLL (Release) / MultiThreadedDebugDLL (Debug). DEBE coincidir con el anfitrión.
  • Definiciones del preprocesador: PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS
  • Directorios de inclusión adicionales: $(SolutionDir)POEFixer
  • Directorio de salida: $(SolutionDir)x64\Release\Plugins\<YourPlugin>\
  • Nombre del objetivo: debe coincidir con el nombre de la carpeta (Plugins/MyPlugin/MyPlugin.dll)

El anfitrión escanea cada subcarpeta en Plugins/ y busca <FolderName>.dll. La DLL debe exportar tres símbolos:

  • CreatePlugin — fábrica; devuelve PluginSDK::Plugin*.
  • DestroyPlugin — destructor; recibe PluginSDK::Plugin*.
  • PluginSDK_AttachHost — conecta el Context. Definido por ti dentro de PluginSDK.h y emitido automáticamente cuando se establece PLUGIN_EXPORTS.

Disposición recomendada del código fuente:

Plugins/YourPlugin/
  YourPlugin.vcxproj
  YourPlugin.vcxproj.filters
  src/
    YourPlugin.cpp        // class YourPlugin : public PluginSDK::Plugin
    YourPluginSettings.h  // Save()/Load() POCO
  config/                 // runtime-created by SaveSettings()
    settings.json

Directory() devuelve una ruta absoluta UTF-8 enraizada en el directorio del EXE del anfitrión — no antepongas tú mismo la ruta del EXE. La cadena del directorio se almacena por valor dentro de PluginSDK::Plugin, así que sobrevive a las reasignaciones de contenedores del anfitrión y a los ciclos de recarga sin problemas de tiempo de vida.

Si quieres dibujar con ImGui, añade también esto a <ClCompile> (el anfitrión también los enlaza, pero ImGui del lado del plugin es por DLL):

..\..\POEFixer\imgui\imgui.cpp
..\..\POEFixer\imgui\imgui_draw.cpp
..\..\POEFixer\imgui\imgui_tables.cpp
..\..\POEFixer\imgui\imgui_widgets.cpp

En OnEnable, conéctate al contexto ImGui del anfitrión:

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

4. Ganchos del ciclo de vida

PluginSDK::Plugin es una clase base virtual. Sobrescribe estos métodos en tu plugin (en orden aproximado de llamada):

Método Cuándo se llama Uso típico
const char* GetName() const Una vez, justo después de la construcción Devuelve el nombre para mostrar de tu plugin
void OnEnable(bool isGameAttached) Cuando el usuario habilita el plugin (o al inicio si está persistido) Cargar configuración, suscribirse a eventos, adjuntar contexto ImGui
void DrawSettings() Cada frame mientras el panel de configuración del plugin está abierto Controles ImGui para tu configuración
void DrawUI() Cada frame mientras el plugin está habilitado Dibujado de superposición ImGui (usa ImGui::GetBackgroundDrawList() para superposición del juego)
bool WantsOverlay() const Consultado cada frame Devuelve true si quieres el anfitrión en modo superposición (transparente al click)
void SaveSettings() Periódicamente (~5s) y al deshabilitar Persistir configuración a disco
void OnDisable() Cuando el usuario deshabilita, o al apagar el anfitrión Liberar recursos, desuscribir eventos

Solo GetName es obligatorio; el resto tiene valores predeterminados seguros.

El anfitrión también llama a GetSDKVersion() (definido en la base, NO sobrescribir) inmediatamente después de CreatePlugin para verificar que el plugin y el anfitrión están de acuerdo. Si no coinciden → plugin rechazado.


5. El Context

ctx() devuelve const PluginSDK::Context*, un agregado de 10 servicios:

struct Context {
    GameService       Game;        // snapshot, state flags, screen size
    EntitiesService   Entities;    // enumerate, find-by-id, watch
    ComponentsService Components;  // 21 component readers + collection enumerators
    InventoryService  Inventory;   // scan + iterate + per-item helpers
    UiService         Ui;          // tree walk, FindPanelByStringId, screen-rect
    RenderService     Render;      // WorldToScreen + isometric map projection
    TerrainService    Terrain;     // walkable grid (RAII), height, TGT locations
    MemoryService     Memory;      // direct memory primitives (last resort)
    LogService        Log;         // Debug/Info/Warn/Error
    EventsService     Events;      // Subscribe / Unsubscribe / On<X>
    void* ImGuiContext;            // pass to ImGui::SetCurrentContext
    void* D3DDevice;               // ID3D11Device* for texture loading
};

De un vistazo — para qué sirve cada servicio:

Servicio Para qué lo usas
GameService Snapshot, banderas de estado, información de pantalla y ventana
EntitiesService Enumerar, buscar por id, observar el ciclo de vida
ComponentsService 21 lectores + 4 enumeradores + ~10 ayudantes de conveniencia
InventoryService Escanear, enumerar, leer mods por ítem
UiService Recorrer el árbol, FindPanelByStringId, ComputeScreenRect
RenderService WorldToScreen, GridTo{Large,Mini}Map, transformaciones
TerrainService Rejillas de caminabilidad y altura (RAII), ubicaciones TGT
MemoryService Primitivas RPM — solo cuando no hay una llamada de mayor nivel adecuada
LogService Debug / Info / Warn / Error
EventsService Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}

ctx() es válido desde el momento en que el anfitrión llama a OnEnable hasta que OnDisable retorna. No almacenes en caché ctx() entre recargas en caliente o fronteras de descarga de DLL.


6. Lectura del estado del juego

ctx()->Game.GetSnapshot() devuelve un Snapshot de tipo por valor — una vista inmutable completa del frame actual. GetSnapshot() recorre abi->entities.enumerate y rellena snap.Entities antes de retornar, por lo que el costo escala con el número de entidades cercanas. Llámalo una vez por frame y reutilízalo.

PluginSDK::Snapshot snap = ctx()->Game.GetSnapshot();
if (snap.State != PluginSDK::GameState::InGame) return;

ctx()->Log.Info(snap.CurrentAreaName.c_str());
if (snap.IsTown || snap.IsHideout) return;  // safe area

// snap.Vitals.HPPercent, snap.Vitals.MaxES, snap.Vitals.IsPaused
// snap.Player.GridPositionX, snap.Player.Path (wstring), snap.Player.Components
// snap.Entities is a std::vector<Entity> — every nearby entity, fully populated
// snap.LargeMap / snap.MiniMap — visibility + projection inputs
// snap.AreaChangeCounter — increments each portal transition

Lo que el snapshot lleva directamente (sin necesidad de más llamadas a servicios):

  • Estado y banderas: State, IsAttached, IsWindowValid, GameWindowForeground, IsTown, IsHideout, IsPaused, IsSkillTreeVisible.
  • Área: CurrentAreaName, CurrentAreaHash, CurrentAreaLevel, AreaChangeCounter.
  • Mundo: Player (Entity completo), Entities (std::vector<Entity> completo), Vitals, LargeMap, MiniMap, WorldToScreenMatrix[16].
  • Ventana: ScreenWidth, ScreenHeight, ProcessId, GameWindow, LastUpdateTime, WorldToGridConvertor.

Lo que no está en el snapshot — recupéralo vía servicios: contenido del inventario (InventoryService), buffs (ComponentsService::EnumerateBuffs), listas de mods por ítem (InventoryService::ReadItemMods), paneles de UI (UiService).

Ayudantes económicos para cuando no necesitas un snapshot completo:

if (ctx()->Game.IsInGame())       { ... }
if (ctx()->Game.IsForeground())   { ... }   // game window focused
if (ctx()->Game.IsOverlayMode())  { ... }   // host is in overlay (click-through)
if (ctx()->Game.IsMenuVisible())  { ... }   // ESC menu, settings, etc.
auto sz = ctx()->Game.GetScreenSize();       // ScreenSize { Width, Height } floats
HWND hw = ctx()->Game.GetGameWindow();
DWORD pid = ctx()->Game.GetProcessId();
PluginSDK::GameState st = ctx()->Game.GetState();

7. Lectura de componentes

Las entidades exponen sus componentes a través de entity.Components — una estructura ComponentAddresses de direcciones uintptr_t. Pasa cada dirección al ComponentsService::Read* correspondiente para obtener una instantánea de tipo por valor.

for (const auto& e : snap.Entities) {
    if (!e.Components.HasLife()) continue;
    PluginSDK::Life life = ctx()->Components.ReadLife(e.Components.Life);
    if (life.Valid && life.Health.Current > 0) {
        ctx()->Log.Info("alive monster");
    }
}

Hay 21 lectores de componentes: ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime.

ComponentAddresses en sí mismo contiene 24 ranuras: los 21 anteriores más tres marcadores (Buffs, WorldItem, AreaTransition) y OMP (interno del anfitrión). Buffs es un marcador de presencia — la lista real de buffs proviene de EnumerateBuffs. WorldItem / AreaTransition son marcadores de tipo de entidad en lugar de componentes reales. Todas las ranuras tienen predicados HasX() correspondientes en ComponentAddresses.

Lectores estilo colección para componentes con datos de tamaño variable:

auto buffs  = ctx()->Components.EnumerateBuffs(e.Components.Buffs);            // std::vector<Buff>
auto skills = ctx()->Components.EnumerateActiveSkills(e.Components.Actor);     // std::vector<ActiveSkill>
auto stats  = ctx()->Components.EnumerateStats(e.Components.Stats);            // std::vector<StatEntry>
auto mods   = ctx()->Components.EnumerateItemMods(e.Components.Mods);          // std::vector<Mod>

Ayudantes de conveniencia (de una sola pasada — internamente llaman a Read* por ti):

float hpPct  = ctx()->Components.GetHealthPercent(e.Components.Life);
bool  alive  = ctx()->Components.IsAlive(e.Components.Life);
float esPct  = ctx()->Components.GetEsPercent(e.Components.Life);
float mpPct  = ctx()->Components.GetManaPercent(e.Components.Life);
int   rarity = ctx()->Components.GetItemRarity(e.Components.Mods);
bool  ident  = ctx()->Components.IsItemIdentified(e.Components.Mods);
int   stack  = ctx()->Components.GetStackCount(e.Components.Stack);
bool  open   = ctx()->Components.IsChestOpened(e.Components.Chest);
std::string name = ctx()->Components.GetPlayerName(e.Components.Player);
float wx, wy, wz;
if (ctx()->Components.GetWorldPosition(e.Components.Render, wx, wy, wz)) { ... }

Una bandera Valid en cada estructura devuelta te permite manejar "la dirección del componente era 0 / la lectura falló" sin excepciones. Si ya tienes la estructura padre (Life, Mods, …), accede a sus campos directamente en lugar de llamar al ayudante de nuevo — el ayudante vuelve a leer el componente cada vez.

Campos de Entity

Cada Entity (incluyendo snap.Player y los miembros de snap.Entities) lleva el mismo conjunto de campos:

Grupo Campos
Identidad Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
Clasificación EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far)
Posición GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
Vitales rápidos CurrentHP, MaxHP, CurrentES, MaxES (evita un ReadLife si solo necesitas los totales)
Cadenas Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, ruta del asset)
Estado IsSleeping, IsChestOpened
Componentes Components (subestructura ComponentAddresses)

Observar una entidad específica

Si necesitas rastrear una entidad a lo largo de los frames (p. ej. un cofre que el jugador está abriendo) y no quieres escanear la lista completa de entidades en cada frame, registra una observación:

ctx()->Entities.Watch(entityId);
// ...later:
if (auto opt = ctx()->Entities.GetWatchedComponents(entityId)) {
    PluginSDK::ComponentAddresses comps = *opt;
    PluginSDK::Life l = ctx()->Components.ReadLife(comps.Life);
}
bool active = ctx()->Entities.IsWatched(entityId);
ctx()->Entities.Unwatch(entityId);

FindById(id) devuelve std::optional<Entity> para búsquedas puntuales, y GetPlayer() siempre devuelve el jugador local.

Ítems en el suelo (contenedores WorldItem)

Los ítems tirados al suelo aparecen en snap.Entities como entidades EntityType::Item en la ruta Metadata/MiscellaneousObjects/WorldItem. Son entidades contenedoras — no llevan directamente Mods / Base / Stack / Sockets. La entidad real del ítem está a una indirección de distancia.

Para obtener la entidad interna del ítem como un snapshot Entity regular, usa Entities.GetWorldItemInner:

for (const auto& e : snap.Entities) {
    if (e.EntityType != PluginSDK::EntityType::Item) continue;
    auto inner = ctx()->Entities.GetWorldItemInner(e.Address);
    if (!inner) continue;  // mid-spawn, retry next frame
    // inner->Path        — "Metadata/Items/Armours/Gloves/..."
    // inner->Components  — Mods / Base / Stack / Sockets / etc.
    PluginSDK::Mods mods = ctx()->Components.ReadMods(inner->Components.Mods);
    int iLvl   = mods.ItemLevel;
    int rarity = mods.Rarity;
}

GetWorldItemInner sólo tiene éxito en contenedores WorldItem reales — llamarlo con una dirección de ítem de inventario devuelve std::nullopt. Si quieres la misma forma de datos que para ítems de inventario (sin recorrer manualmente los componentes), la familia Inventory.ReadItem* de la siguiente sección resuelve los contenedores WorldItem de forma transparente.


8. Inventario

ctx()->Inventory.Scan(inventoryId) desencadena un re-escaneo en el lado del anfitrión. Usa -1 para escanear todos los inventarios.

ctx()->Inventory.Scan(-1);
std::vector<PluginSDK::Inventory> all = ctx()->Inventory.GetAll();

for (const auto& inv : all) {
    const char* name = ctx()->Inventory.GetName(inv.InventoryId);
    ctx()->Log.Info(name);
    for (const auto& item : inv.Items) {
        ctx()->Log.Info(item.BaseTypeName.c_str());
        // item.SlotX, item.SlotY, item.Width, item.Height (grid metrics)
        // item.Rarity, item.ItemLevel, item.RequiredLevel, item.CraftedModCount
        // item.IsIdentified, item.IsCorrupted, item.IsCurrency
        // item.Path (Metadata/Items/...), item.BaseTypeName, item.UniqueName
        // item.Address — entity address for direct lookups below
    }
}

Cada Inventory también expone una estructura Grid que describe dónde se dibuja el inventario en pantalla:

if (inv.Grid.Valid) {
    float originX = inv.Grid.GridScreenX;
    float originY = inv.Grid.GridScreenY;
    float cell    = inv.Grid.CellSize;
    // Slot (x, y) screen-space top-left = (originX + x*cell, originY + y*cell)
}

Para obtener un inventario único por id (devuelve la misma estructura con Items ya poblado):

PluginSDK::Inventory backpack = ctx()->Inventory.Get(/*inventoryId=*/0);

O si solo quieres el vector de ítems sin la estructura envolvente:

std::vector<PluginSDK::InventoryItem> items = ctx()->Inventory.GetItems(0);

Mods de ítems: dos APIs, dos alcances

ComponentsService::ReadMods(addr) devuelve solo banderas de resumen (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). No lleva las listas de mods por tipo.

Para la imagen completa (resumen + listas de mods), utiliza InventoryService::ReadItemMods(entityAddr):

PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(item.Address);
if (!im.Valid) return;
// Same summary fields as the Mods component, plus:
for (const auto& m : im.ImplicitMods)  { ... }   // std::vector<Mod>
for (const auto& m : im.ExplicitMods)  { ... }
for (const auto& m : im.EnchantMods)   { ... }
for (const auto& m : im.HellscapeMods) { ... }
for (const auto& m : im.CrucibleMods)  { ... }

Otras lecturas directas por entidad (más económicas que re-escanear cuando ya tienes la dirección de un ítem):

int          rarity = ctx()->Inventory.ReadItemRarity(item.Address);
int          stack  = ctx()->Inventory.ReadItemStackCount(item.Address);
std::string  base   = ctx()->Inventory.ReadItemBaseTypeName(item.Address);
std::string  uniq   = ctx()->Inventory.ReadItemUniqueName(item.Address);
std::string  path   = ctx()->Inventory.ReadItemPath(item.Address);

Ítems en el suelo a través de la API de inventario. Las siete lecturas Inventory.ReadItem* anteriores (y ReadItemMods) aceptan TANTO direcciones de ítems de inventario COMO direcciones de contenedores WorldItem. Las direcciones de contenedores se resuelven automáticamente al ítem interno antes de la lectura, por lo que la misma ruta de código de plugin funciona para ítems en bolsas y para ítems en el suelo:

// `addr` puede ser una dirección de ítem de inventario o un contenedor WorldItem.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int          rarity   = ctx()->Inventory.ReadItemRarity(addr);
std::string  baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);

Si necesitas directamente las direcciones de componentes del ítem interno (por ejemplo, para llamar a ctx()->Components.ReadStack(...) o recorrer sockets), usa en su lugar Entities.GetWorldItemInner de la sección 7.

Texto de mod al estilo del juego + estadísticas base / agregadas (v6, 2026-06-24). Formatea cualquier clave de estadística al mismo texto que muestra el tooltip del juego, y lee los valores defensivos base de un ítem y las propiedades agregadas de mapas/piedras guía:

// Renderiza un mod tal como lo muestra el juego ("19% increased Monster Damage").
for (const auto& m : im.ExplicitMods) {
    std::string text = ctx()->Inventory.FormatStat(m.StatKey, m.Value0, m.Value1);
    if (!text.empty()) { /* draw `text` */ }
}

// Valores defensivos base del ítem. EnergyShield es el valor calculado en el juego;
// Ward/Armour/Evasion son los valores base del ítem. Valid == false cuando el ítem
// no tiene componente Armour (monedas, gemas, joyería, piedras guía, ...).
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// Estadísticas agregadas indexadas por id de stat — por ejemplo, Item Rarity (8205),
// Pack Size (8206), Monster Rarity (8207), Monster Effectiveness (8208),
// Waystone Drop Chance (8209) de una piedra guía.
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // asigna statId -> etiqueta tú mismo; los valores son porcentajes con signo
}

FormatStat utiliza el conjunto de descripciones de estadísticas .csd del anfitrión (descargado en el primer uso), por lo que devuelve una cadena vacía hasta que esos datos estén listos — recurre a los campos crudos de Mod como alternativa. ReadItemBaseStats / ReadItemAggregatedStats aceptan tanto direcciones de ítems de inventario COMO direcciones de contenedores WorldItem.


9. Árbol de UI

El árbol de UI del juego se expone como direcciones uintptr_t de elementos. Empieza desde una raíz, recorre los hijos, lee los campos del elemento.

La forma limpia de encontrar un panel conocido por su StringId:

uintptr_t gameUiRoot = ctx()->Ui.GetGameUiRoot();
uintptr_t invPanel   = ctx()->Ui.FindPanelByStringId(gameUiRoot, "Inventory");
if (invPanel && ctx()->Ui.IsVisible(invPanel)) {
    // panel is on-screen
}

Recorrido manual del árbol cuando no conoces el StringId:

uintptr_t root = ctx()->Ui.GetUiRoot();
PluginSDK::UiElement e = ctx()->Ui.Read(root);
ctx()->Log.Info(("children=" + std::to_string(e.ChildCount)).c_str());

for (uintptr_t child : ctx()->Ui.GetChildren(root)) {
    std::string sid = ctx()->Ui.GetStringId(child);
    if (sid == "InventoriesPanel") { /* found it */ }
}

// Or use a known index path:
int path[] = { 5, 1, 2, 0 };
uintptr_t logInButton = ctx()->Ui.FollowPath(root, path, 4);

// Compute screen-space rect (post-scale, post-transform):
float x, y, w, h;
if (ctx()->Ui.ComputeScreenRect(invPanel, x, y, w, h)) {
    // draw an overlay box at (x,y,w,h)
}

// Get displayed text:
std::string label = ctx()->Ui.GetText(child);

int cull = ctx()->Ui.GetCullValue();  // host's UI cull threshold

Los valores StringId son identificadores estables del lado del juego; prefiérelos sobre rutas codificadas a mano cuando existan.


10. Renderizado y proyección

Tres ayudantes de proyección, dos sistemas de coordenadas.

Perspectiva (3D mundo → pantalla) — la misma proyección que usa el juego para dibujar cosas en el mundo. Útil para placas de identificación, marcadores de depuración, indicadores de objetivo:

float sx, sy;
if (ctx()->Render.WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, sx, sy)) {
    ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, IM_COL32(255,0,0,255));
}

Isométrico (rejilla → minimapa) — para superposiciones tipo radar dibujadas en el mapa grande o el minimapa. Estas respetan el zoom, paneo y rotación del mapa visible:

if (!snap.LargeMap.IsVisible) return;
for (const auto& e : snap.Entities) {
    float sx, sy;
    if (ctx()->Render.GridToLargeMap(e.GridPositionX, e.GridPositionY, e.TerrainHeight, sx, sy)) {
        ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, color);
    }
}
// Mirror: ctx()->Render.GridToMiniMap(gx, gy, worldZ, sx, sy)

Para cálculos por lotes (sin llamadas de función por entidad), obtén la transformación una vez y realiza la proyección en línea:

PluginSDK::MapTransform t = ctx()->Render.GetLargeMapTransform();
if (t.IsVisible) {
    // dx = gx - t.PlayerGridX; dy = gy - t.PlayerGridY;
    // sx = t.CenterX + (dx - dy) * t.ScaleX;
    // sy = t.CenterY + (worldZ * worldToGrid - (dx + dy)) * t.ScaleY;
}
// And ctx()->Render.GetMiniMapTransform() for the minimap.

Consulta Plugins/Radar/src/Radar.cpp para un radar funcional construido enteramente sobre estas llamadas.


11. Terreno y rejilla de caminabilidad

La rejilla de caminabilidad es un mapa de bits de 4 bits por tile que indica qué celdas del terreno puede pisar el jugador. El anfitrión la actualiza en cada cambio de área; los plugins reciben un handle estable que sobrevive hasta que el plugin lo libera (vía RAII).

PluginSDK::WalkableGridHandle h = ctx()->Terrain.GetWalkableGrid();
if (h.Valid()) {
    const uint8_t* data       = h.Data();
    const int      w          = h.Width();
    const int      height     = h.Height();
    const size_t   sizeBytes  = h.SizeBytes();   // (w * height) / 2
    // POE2 packs two cells per byte:
    //   gx & 1 == 0  →  low nibble  (data[gy * (w/2) + gx/2] & 0x0F)
    //   gx & 1 == 1  →  high nibble ((data[gy * (w/2) + gx/2] >> 4) & 0x0F)
    // Non-zero nibble = walkable.
    //
    // Always bound your byte index against sizeBytes — the host enforces an
    // atomic snapshot, but defense-in-depth has caught at least one real bug.
}
// h is RAII — destructor releases the host reference automatically.

HeightGridHandle refleja la misma forma pero mantiene un float por tile (Data() es const float*, además de ElementCount() y SizeBytes()).

No te suscribas a OnAreaChange para refrescar el handle. El evento se dispara cuando el worker del anfitrión detecta un cambio de área, pero la nueva rejilla de caminabilidad puede no haber sido parseada todavía — mantendrías un puntero obsoleto durante uno o dos frames. En su lugar, sondea por frame en DrawUI:

auto current = ctx()->Terrain.GetWalkableGrid();
if (current.Data() != m_walkable.Data()) {
    m_walkable = std::move(current);   // swap when the host re-parses
}

Esto es económico (una llamada ABI + una comparación de punteros). Consulta Plugins/Radar/src/Radar.cpp para la versión de producción.

Otros accesores de terreno:

bool  ok        = ctx()->Terrain.IsWalkable(gx, gy);
float worldZ    = ctx()->Terrain.GetTerrainHeight(gx, gy);
float worldToG  = ctx()->Terrain.GetWorldToGridConvertor();

ctx()->Terrain.EnumerateTgtLocations([](const PluginSDK::TgtLocation& loc) {
    // loc.Path, loc.TileX, loc.TileY, loc.X, loc.Y
    return true;  // continue
});

12. Eventos

Suscríbete a eventos emitidos por el anfitrión. Cada Subscribe devuelve un Token que más tarde puedes pasar a Unsubscribe. El destructor de EventsService (disparado cuando el plugin se deshabilita o se descarga) libera automáticamente cualquier suscripción pendiente — por lo que estrictamente no necesitas desuscribirte manualmente, pero es cortés hacerlo.

class MyPlugin : public PluginSDK::Plugin {
    PluginSDK::EventsService::Token m_areaTok{};
    PluginSDK::EventsService::Token m_frameTok{};

public:
    void OnEnable(bool) override {
        auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);

        m_areaTok = ev.OnAreaChange([this]{
            ctx()->Log.Info("area changed");
        });

        m_frameTok = ev.OnFrame([this]{
            // called every frame; keep work cheap
        });

        ev.OnGameAttached([this]{ ctx()->Log.Info("game attached"); });
        ev.OnGameDetached([this]{ ctx()->Log.Info("game detached"); });
    }

    void OnDisable() override {
        auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);
        ev.Unsubscribe(m_areaTok);
        ev.Unsubscribe(m_frameTok);
    }
};

Los cuatro tipos de eventos son AreaChange, Frame, GameAttached, GameDetached. También existe un Subscribe(EventKind, callback) genérico si prefieres construir una tabla de despacho.

El const_cast es necesario porque Events muta su mapa interno de tokens. La clase base devuelve const Context* para desincentivar la mutación accidental de otros servicios.

Agrupando suscripciones

Si tu plugin posee varias suscripciones, el patrón de ExamplePlugin es una manera limpia de mantener la simetría entre enable/disable — agrupa los tokens y contadores en una sola estructura de estado y enruta todo a través de un único par SubscribeAll/UnsubscribeAll:

struct EventsDemoState {
    std::atomic<int> frameCount{0}, areaChangeCount{0};
    PluginSDK::EventsService::Token frameTok{}, areaTok{};
    bool subscribed = false;
};

void SubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
    auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
    s.frameTok = ev.OnFrame      ([&s]{ s.frameCount.fetch_add(1); });
    s.areaTok  = ev.OnAreaChange ([&s]{ s.areaChangeCount.fetch_add(1); });
    s.subscribed = true;
}

void UnsubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
    auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
    ev.Unsubscribe(s.frameTok);
    ev.Unsubscribe(s.areaTok);
    s.frameTok = {}; s.areaTok = {};
    s.subscribed = false;
}

Consulta Plugins/ExamplePlugin/examples/ExampleEvents.h para el patrón completo.


13. Persistencia de la configuración

Convención: <plugin directory>/config/settings.json. Directory() devuelve la ruta absoluta UTF-8 a la carpeta de tu plugin.

Para configuraciones triviales, un escritor JSON hecho a mano funciona bien y mantiene la DLL autocontenida. Consulta Plugins/Radar/src/RadarSettings.h para un ejemplo funcional. El esqueleto:

struct MySettings {
    bool  DrawEnabled = true;
    float Opacity     = 0.9f;

    void Save(const std::string& directory) const {
        std::filesystem::path p =
            std::filesystem::path(directory) / "config" / "settings.json";
        std::error_code ec;
        std::filesystem::create_directories(p.parent_path(), ec);
        std::ofstream out(p);
        if (!out.is_open()) return;
        out << "{\n";
        out << "  \"DrawEnabled\":" << (DrawEnabled ? "true" : "false") << ",\n";
        out << "  \"Opacity\":"     << Opacity << "\n";
        out << "}\n";
    }

    void Load(const std::string& directory) {
        std::filesystem::path p =
            std::filesystem::path(directory) / "config" / "settings.json";
        if (!std::filesystem::exists(p)) return;
        // ... parse ...
    }
};

// In your plugin:
void OnEnable(bool) override   { m_settings.Load(Directory()); }
void SaveSettings() override   { m_settings.Save(Directory()); }

Para datos estructurados (objetos anidados, arrays), incorpora una biblioteca JSON real en la carpeta de tu plugin. El anfitrión no impone una elección.

SaveSettings se llama periódicamente (~5s) y al deshabilitar; no necesitas llamarlo tú mismo.


14. Registro de logs

ctx()->Log.Debug("verbose detail");
ctx()->Log.Info ("normal status");
ctx()->Log.Warn ("something unexpected");
ctx()->Log.Error("operation failed");
ctx()->Log.Log  ("custom-level", "message");

Los cuatro niveles se enrutan al logger central del anfitrión. Los mensajes aparecen en la pestaña Logs del anfitrión y en el archivo de log en disco. Formatea tú mismo antes de llamar; el anfitrión no acepta varargs estilo printf.

Internamente, los métodos de conveniencia emiten las cadenas "Debug", "Info", "Warning" y "Error" (Warn se asigna a "Warning"). El puente del anfitrión hace una comparación insensible a mayúsculas/minúsculas, por lo que un plugin que llame a Log("warn", "msg") también se enruta correctamente — pero los métodos de conveniencia son más claros.


15. Memoria (usuario avanzado)

Primitivas de memoria directas. Prefiere los servicios de alto nivel siempre que sea posible — entienden los offsets, manejan cambios de ABI y son seguros frente a SEH. Las lecturas directas de memoria solo son apropiadas cuando no existe una llamada de mayor nivel para lo que necesitas.

// Read a fixed-size value:
uint64_t value = 0;
ctx()->Memory.Read(addr, &value, sizeof(value));

// Read game strings (null-terminated, narrow or wide):
std::string  s  = ctx()->Memory.ReadString (strAddr);
std::wstring ws = ctx()->Memory.ReadWString(wstrAddr);

// Read a std::wstring container in the game's memory (handles SSO):
std::wstring inner = ctx()->Memory.ReadStdWString(containerAddr);

// Read a std::vector<T>; returns raw bytes you reinterpret_cast:
std::vector<uint8_t> raw = ctx()->Memory.ReadStdVector(vecAddr, sizeof(MyT), /*maxElems=*/1024);
const MyT* items = reinterpret_cast<const MyT*>(raw.data());
size_t count = raw.size() / sizeof(MyT);

// Module info:
uintptr_t base = ctx()->Memory.GetBaseAddress();
uintptr_t sz   = ctx()->Memory.GetModuleSize();
uintptr_t pat  = ctx()->Memory.GetPatternAddress("GameStates");  // resolves a named pattern

Si te encuentras recurriendo a estas a menudo, pregúntate si los datos que necesitas pertenecen a los servicios de mayor nivel.


16. Puente / Seguridad SEH

Cada llamada entre DLLs entre el anfitrión y un plugin se ejecuta dentro de un bloque __try / __except en el lado del anfitrión. Un plugin con mal comportamiento que dereferencia un puntero obsoleto, divide por cero o falla de otra forma dentro de una llamada SDK obtiene un error registrado — el proceso del anfitrión no crashea, el juego sigue funcionando y el usuario puede seguir utilizando otros plugins.

Eso no significa que los plugins puedan ser descuidados. SEH atrapa el síntoma, no la causa. Si tu plugin lanza fallos en cada frame, el usuario ve una avalancha de logs de error y tus datos son efectivamente no disponibles. Maneja los retornos nulos de las llamadas SDK, comprueba las banderas Valid en los datos de componentes y no dereferences direcciones uintptr_t directamente — pásalas a través de las llamadas ComponentsService / Ui / Memory que ya envuelven RPM correctamente.

El anfitrión puede lidiar con un plugin con errores. No puede lidiar con una DLL de plugin colgada — un DrawSettings que tarda 100ms bloquea todo el hilo de UI. Mantén el trabajo por frame económico.


17. Trampas comunes

Una lista corta de cosas con las que los autores de plugins se topan al integrarse por primera vez. La mayoría están documentadas en línea más arriba; recopiladas aquí como una lista de verificación.

  1. OnAreaChange se dispara antes de que la rejilla de caminabilidad sea reparseada. No refresques WalkableGridHandle desde el evento — sondea por frame en DrawUI e intercambia cuando Data() cambie. (§11)
  2. Entity::Zone siempre es None para el jugador local. Es una clasificación de distancia desde el jugador, por lo que el jugador está por definición a distancia cero. No lo muestres en visualizaciones de información del jugador.
  3. Components.ReadMods() devuelve solo banderas de resumen — sin listas de mods. Para listas de mods por tipo, llama a Inventory.ReadItemMods(entityAddr). (§8)
  4. Los ítems caídos en el suelo pueden carecer de EntitySubtype. Si estás filtrando ítems en el mundo, prefiere EntityType == Item || EntityType == Chest sobre una comprobación de subtipo más estrecha.
  5. Directory() devuelve una ruta absoluta. No antepongas tú mismo el directorio del EXE — obtendrás EXEDIR\EXEDIR\Plugins\X y tus escrituras de configuración aterrizarán fuera de la carpeta del plugin.
  6. ctx() devuelve const Context*. Los métodos mutadores como EventsService::Subscribe requieren const_cast. Esto es intencional — los servicios que no mutan deberían ser imposibles de mutar accidentalmente.
  7. ImGui::SetCurrentContext es por DLL. Llámalo en cada punto de entrada que dibuje (OnEnable, DrawUI, DrawSettings) porque la DLL del plugin tiene su propio estado ImGui por defecto.
  8. Los ayudantes de conveniencia releen el componente en cada llamada. GetHealthPercent(addr) hace un ReadLife(addr) fresco internamente. Si ya tienes la estructura Life de una llamada anterior, accede a sus campos directamente en su lugar.

18. Referencia rápida de servicios

Resumen de una línea de cada método público en cada servicio. Usa las secciones en prosa de arriba para las firmas de tipo completas y notas de uso.

GameService

Método Devuelve Propósito
GetSnapshot() Snapshot Vista completa por frame, incluyendo Entities
GetState() GameState Enum: InGame, Login, Loading, …
IsAttached() bool Proceso del juego adjuntado
IsInGame() bool State == InGame
IsForeground() bool La ventana del juego tiene el foco
IsMenuVisible() bool Menú ESC / ajustes abiertos
IsOverlayMode() bool El anfitrión está en superposición (transparente al click)
GetProcessId() DWORD PID del juego
GetGameWindow() HWND Handle de ventana del juego
GetScreenSize() ScreenSize floats {Width, Height}

EntitiesService

Método Devuelve Propósito
Enumerate(cb) Visita cada entidad cercana (devuelve false para detener)
GetPlayer() Entity La entidad del jugador local
FindById(id) std::optional<Entity> Búsqueda por id de entidad
GetWorldItemInner(addr) std::optional<Entity> Entidad interna del ítem para un contenedor WorldItem (ítems en el suelo)
Watch(id) Fija una entidad para que sus componentes permanezcan legibles
Unwatch(id) Libera una observación
IsWatched(id) bool Estado de observación
GetWatchedComponents(id) std::optional<ComponentAddresses> Lee los componentes fijados

ComponentsService

Método Devuelve Propósito
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime Estructura de componente 21 lectores, uno por tipo de componente
EnumerateBuffs(addr) std::vector<Buff> Buffs activos en la entidad
EnumerateActiveSkills(addr) std::vector<ActiveSkill> Habilidades de un componente Actor
EnumerateStats(addr) std::vector<StatEntry> Stats provenientes de ítems + buffs
EnumerateItemMods(addr) std::vector<Mod> Mods alcanzables desde un componente Mods
GetHealthPercent / GetEsPercent / GetManaPercent float Ayudantes de % de conveniencia
IsAlive(addr) bool Health > 0
GetItemRarity(addr) int Rareza de un componente Mods
IsItemIdentified(addr) bool Bandera de identificado
GetStackCount(addr) int Recuento actual de stack
IsChestOpened(addr) bool Bandera de cofre abierto
GetPlayerName(addr) std::string Nombre del jugador de un componente Player
GetWorldPosition(renderAddr, x, y, z) bool Accesor de conveniencia para coordenadas del mundo

InventoryService

Método Devuelve Propósito
Scan(inventoryId) Desencadena un re-escaneo en el lado del anfitrión (-1 = todos)
Get(inventoryId) Inventory Un inventario, ítems ya poblados
GetItems(inventoryId) std::vector<InventoryItem> Solo ítems
GetAll() std::vector<Inventory> Todos los inventarios escaneados
GetName(inventoryId) const char* Nombre para mostrar ("Backpack", "Stash", …)
ReadItemRarity(addr) int Rareza por entidad (resuelve automáticamente contenedores WorldItem)
ReadItemStackCount(addr) int Stack por entidad (resuelve automáticamente contenedores WorldItem)
ReadItemBaseTypeName(addr) std::string Tipo base, resuelve automáticamente contenedores WorldItem
ReadItemUniqueName(addr) std::string Nombre único, resuelve automáticamente contenedores WorldItem
ReadItemPath(addr) std::string Ruta Metadata/Items/..., resuelve automáticamente contenedores WorldItem
ReadItemMods(addr) ItemMods Banderas de resumen + 5 vectores de mods por tipo, resuelve automáticamente contenedores WorldItem
FormatStat(statKey, v0, v1) std::string Texto al estilo del juego para una clave de stat + valor(es) mediante el formateador .csd del anfitrión; vacío hasta que carguen las descripciones
ReadItemBaseStats(addr) ItemBaseStats Valores defensivos base (Energy Shield calculado; Ward/Armour/Evasion base); Valid falso sin componente Armour; resuelve automáticamente contenedores WorldItem
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> {statId, value} agregados (piedra guía Item Rarity 8205 / Pack Size 8206 / Monster Rarity 8207 / Monster Effectiveness 8208 / Waystone Drop Chance 8209); resuelve automáticamente contenedores WorldItem

UiService

Método Devuelve Propósito
Read(addr) UiElement Campos del elemento (rect, banderas, número de hijos)
GetChildren(addr) std::vector<uintptr_t> Direcciones de elementos hijos
GetChildAt(addr, index) uintptr_t Hijo único por índice
FollowPath(root, indices, count) uintptr_t Recorre una ruta de índices conocida
IsVisible(addr) bool El elemento está en pantalla
GetStringId(addr) std::string Identificador estable del lado del juego
GetText(addr) std::string Texto renderizado
ComputeScreenRect(addr, x, y, w, h) bool Rect final en espacio de pantalla
GetGameUiRoot() uintptr_t Raíz de la UI en el juego
GetUiRoot() uintptr_t Raíz UI de nivel superior
GetCullValue() int Umbral de descarte de UI del anfitrión
FindPanelByStringId(parent, stringId) uintptr_t Búsqueda dirigida de descendientes

RenderService

Método Devuelve Propósito
WorldToScreen(wx, wy, wz, sx, sy) bool Proyección en perspectiva
GridToLargeMap(gx, gy, worldZ, sx, sy) bool Proyectar a la superposición del mapa grande
GridToMiniMap(gx, gy, worldZ, sx, sy) bool Proyectar al minimapa
GetLargeMapTransform() MapTransform Transformación pre-multiplicada para cálculos por lotes
GetMiniMapTransform() MapTransform Lo mismo, para el minimapa

TerrainService

Método Devuelve Propósito
GetWalkableGrid() WalkableGridHandle Handle RAII al mapa de bits de caminabilidad de 4 bits por tile
GetHeightGrid() HeightGridHandle Handle RAII a las alturas de terreno por tile
IsWalkable(gx, gy) bool Predicado de tile único
GetTerrainHeight(gx, gy) float Z en espacio del mundo
GetWorldToGridConvertor() float Factor de conversión mundo → rejilla
EnumerateTgtLocations(cb) Visita cada instancia TGT en el área actual

MemoryService

Método Devuelve Propósito
Read(addr, buf, size) bool RPM crudo
ReadString(addr) std::string Cadena estrecha terminada en nulo
ReadWString(addr) std::wstring Cadena ancha terminada en nulo
ReadStdWString(addr) std::wstring Lee un contenedor std::wstring del lado del juego (maneja SSO)
ReadStdVector(addr, elemSize, maxElems) std::vector<uint8_t> Bytes crudos; reinterprétalos como tu tipo
GetBaseAddress() uintptr_t Base del módulo del juego
GetModuleSize() uintptr_t Tamaño del módulo del juego
GetPatternAddress(name) uintptr_t Búsqueda de patrón con nombre

LogService

Método Propósito
Debug / Info / Warn / Error(msg) Emite al nivel correspondiente
Log(level, msg) Cadena de nivel personalizada

EventsService

Método Devuelve Propósito
Subscribe(kind, cb) Token Despacho genérico
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token Ayudantes de suscripción de una línea
Unsubscribe(token) Liberación manual (el destructor libera automáticamente de todos modos)

19. Versionado

PluginAbi.h define:

constexpr int PLUGIN_SDK_VERSION = 6;

En el momento de la carga, el anfitrión llama a plugin->GetSDKVersion() y lo compara con su propio PLUGIN_SDK_VERSION. Si no coinciden → el anfitrión registra una advertencia y rehúsa cargar el plugin.

El anfitrión también verifica HostAbi::version y HostAbi::size_bytes dentro de PluginSDK_AttachHost (definido en línea por PluginSDK.h cuando se establece PLUGIN_EXPORTS). Si alguno de estos campos no concuerda con aquello contra lo que se compiló el plugin, ctx() no es funcional. El accesor de la clase base HostCompatible() reporta false en ese caso, y cualquier plugin que quiera ser cortés debería rehusarse a actuar:

void OnEnable(bool) override {
    if (!HostCompatible()) {
        ctx()->Log.Error("Host ABI mismatch — disable plugin");
        return;
    }
    // ...
}

20. Plugins de ejemplo

Cuatro plugins en el repositorio están diseñados para leerse como documentación:

  • Plugins/ExamplePlugin/ — escaparate de superficie amplia. Un plugin que toca casi todos los servicios, organizado como 11 subarchivos examples/Example*.h (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) más un banner de resumen de cobertura. Léelo cuando quieras ver cómo se utiliza un servicio, en contexto.

  • Plugins/Radar/ — ejemplo enfocado del mundo real. ~200 líneas. Una superposición de radar construida enteramente sobre el SDK público — sin offsets, sin lecturas directas de memoria. Renderiza el mapa caminable más puntos por entidad vía Render.GridToLargeMap. Léelo cuando quieras ver el código mínimo para un resultado particular.

  • Plugins/KillCount/ — rastreador de kills/cofres/muertes. SQLite + atlas de sprites + estado por área. Muestra cómo entregar persistencia, archivos de datos incorporados y una superposición todo en una DLL.

  • Plugins/NinjaPricer/ — superposición de precios de poe.ninja. Fetch HTTP (Exchange API) + escaneo de inventario + valoración por ítem. Muestra código de red, ingesta de datos de terceros e iteración de inventario en un flujo de trabajo real.


← Home

Clone this wiki locally