Skip to content

Plugin Development Guide PT

Lafko edited this page Jul 9, 2026 · 14 revisions

← Home


Guia de Desenvolvimento de Plugins

Plugins do POEFixer são DLLs C++ nativas carregadas em tempo de execução a partir de Plugins/<PluginName>/<PluginName>.dll. Eles leem o estado do jogo em tempo real, desenham overlays ImGui, persistem suas próprias configurações e se inscrevem em eventos do host.


1. Visão geral

O SDK de plugins possui uma arquitetura de três camadas:

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  (16 files)
                         │
                         ▼
                   GameClient + GameLibrary
  • Autores de plugins incluem exatamente um header: POEFixer/plugin_sdk/PluginSDK.h.
  • Esse header declara tudo no namespace PluginSDK:: e por baixo dele puxa o ABI em C de PluginAbi.h. Você pode mencionar que o segundo existe; você quase nunca olha para ele.
  • Todos os containers std::* vivem dentro da DLL do plugin. Apenas POD atravessa a fronteira do host. Isso significa que um plugin compilado com uma versão de toolchain não pode se misturar com a STL do host — os únicos tipos compartilhados são inteiros, floats, ponteiros e structs pequenos.

Encontre os headers do SDK em:

  • POEFixer/plugin_sdk/PluginSDK.h — o wrapper C++ que autores de plugins usam.
  • POEFixer/plugin_sdk/PluginAbi.h — o ABI puro em C por baixo.

Plugins de referência fornecidos no repositório (leia-os como documentação): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.


2. Plugin Hello-world

Plugin mínimo que carrega e imprime uma mensagem no log do host:

#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; }

Compile como Plugins/Hello/Hello.dll, reinicie o host, ative pela aba Plugins.


3. Configuração do projeto

Use Plugins/ExamplePlugin/ExamplePlugin.vcxproj como template canônico. As configurações essenciais:

  • Configuration type: DynamicLibrary
  • Platform toolset: v143 (Visual Studio 2022)
  • Character set: Unicode
  • Language standard: stdcpp20
  • Runtime library: MultiThreadedDLL (Release) / MultiThreadedDebugDLL (Debug). DEVE coincidir com o host.
  • Preprocessor definitions: PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS
  • Additional include directories: $(SolutionDir)POEFixer
  • Output directory: $(SolutionDir)x64\Release\Plugins\<YourPlugin>\
  • Target name: deve coincidir com o nome da pasta (Plugins/MyPlugin/MyPlugin.dll)

O host varre cada subpasta em Plugins/ e procura por <FolderName>.dll. A DLL deve exportar três símbolos:

  • CreatePlugin — factory; retorna PluginSDK::Plugin*.
  • DestroyPlugin — destrutor; recebe PluginSDK::Plugin*.
  • PluginSDK_AttachHost — conecta o Context. Definido para você dentro de PluginSDK.h e emitido automaticamente quando PLUGIN_EXPORTS está definido.

Layout de código-fonte recomendado:

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() retorna um caminho absoluto em UTF-8 enraizado no diretório do EXE do host — não acrescente o caminho do EXE você mesmo. A string do diretório é mantida por valor dentro de PluginSDK::Plugin, então ela sobrevive a realocações de containers do host e ciclos de recarregamento sem preocupações de tempo de vida.

Se você quiser desenhar ImGui, adicione também estes a <ClCompile> (o host os linka também, mas o ImGui do lado do plugin é por DLL):

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

No OnEnable, anexe ao contexto ImGui do host:

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

4. Hooks de ciclo de vida

PluginSDK::Plugin é uma classe base virtual. Sobrescreva estes no seu plugin (em ordem aproximada de chamada):

Método Chamado quando Uso típico
const char* GetName() const Uma vez, logo após a construção Retorna o nome de exibição do seu plugin
void OnEnable(bool isGameAttached) Quando o usuário ativa o plugin (ou na inicialização se persistido) Carregar configurações, assinar eventos, anexar contexto ImGui
void DrawSettings() A cada frame enquanto o painel de configurações do plugin está aberto Controles ImGui para sua configuração
void DrawUI() A cada frame enquanto o plugin está ativado Desenho de overlay ImGui (use ImGui::GetBackgroundDrawList() para overlay do jogo)
bool WantsOverlay() const Consultado a cada frame Retorna true se quiser o host em modo overlay (click-through)
void SaveSettings() Periodicamente (~5s) e ao desativar Persiste configuração para o disco
void OnDisable() Quando o usuário desativa, ou no shutdown do host Liberar recursos, cancelar inscrição de eventos

Apenas GetName é obrigatório; o resto tem defaults seguros.

O host também chama GetSDKVersion() (definido na base, NÃO sobrescreva) imediatamente após CreatePlugin para verificar que plugin e host concordam. Incompatibilidade → plugin recusado.


5. O Context

ctx() retorna const PluginSDK::Context*, um agregado de 16 serviços:

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>
    OverlayService    Overlay;     // SetIncludeSleepingEntities / SetWantsOverlayInput
    FlasksService     Flasks;       // life/mana flasks + charms: charges, usable, active
    PricesService     Prices;       // poe2scout item prices (host-loaded once, shared)
    RuneshapeService  Runeshape;    // Expedition2Encounter devices + per-device rewards
    AtlasService      Atlas;        // endgame-atlas nodes / adjacency / Rite selection / weights
    SekhemaService    Sekhema;      // Trial-of-the-Sekhemas floor graph / choices / content FKs
    void* ImGuiContext;            // pass to ImGui::SetCurrentContext
    void* D3DDevice;               // ID3D11Device* for texture loading
};

Em resumo — para que serve cada serviço:

Serviço Para que você o usa
GameService Snapshot, state flags, informações de tela + janela
EntitiesService Enumerar, buscar por id, observar ciclo de vida
ComponentsService 21 readers + 4 enumeradores + ~10 helpers de conveniência
InventoryService Scan, enumerar, leituras de mods por item
UiService Percurso de árvore, FindPanelByStringId, ComputeScreenRect
RenderService WorldToScreen, GridTo{Large,Mini}Map, transformações
TerrainService Grids de walkable + altura (RAII), locais TGT
MemoryService Primitivas RPM — apenas quando nenhuma chamada de alto nível serve
LogService Debug / Info / Warn / Error
EventsService Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
OverlayService SetIncludeSleepingEntities / SetWantsOverlayInput — amigável para plugins de seleção de mapa
FlasksService Frascos de vida/mana + amuletos — cargas, Usable, Active, por uso, contagem de mods
PricesService LookupPrice / GetRates / GetStatus — preços poe2scout carregados pelo host, compartilhados por todos os plugins
RuneshapeService Runeshapes / Rewards — dispositivos Expedition2Encounter resolvidos + recompensas por dispositivo
AtlasService GetPanel / Nodes / Connections / Selection / GetLineSeed / Weights — dados ao vivo do painel do atlas de endgame
SekhemaService GetPanel / GetFloor / Rooms / Content / leituras de flags de sala — dados do mapa de andar do Trial of the Sekhemas

ctx() é válido a partir do momento em que o host chama OnEnable até OnDisable retornar. Não armazene ctx() em cache através de hot-reloads ou limites de descarregamento de DLL.


6. Lendo o estado do jogo

ctx()->Game.GetSnapshot() retorna um Snapshot por valor — uma visão imutável completa do frame atual. GetSnapshot() percorre abi->entities.enumerate e popula snap.Entities antes de retornar, então o custo escala com a contagem de entidades próximas. Chame uma vez por frame e reutilize.

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

O que o snapshot carrega diretamente (sem necessidade de chamadas adicionais a serviços):

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

O que não está no snapshot — obtenha via serviços: conteúdo do inventário (InventoryService), buffs (ComponentsService::EnumerateBuffs), listas de mods por item (InventoryService::ReadItemMods), painéis de UI (UiService).

Helpers baratos quando você não precisa de um 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();

GetHiveblood lê o contador do recurso da árvore Genesis (Hiveblood) — uma leitura host-tail roteada através do GameService (mesma família de GetGold / GetAreaId):

int32_t hiveblood = 0;
if (ctx()->Game.GetHiveblood(hiveblood)) {
    // in a Genesis map: hiveblood holds the current resource count
}
// Returns false (leaving the out param untouched) when not in game, the chain
// is broken, or the host predates the tail function — always gate on the return.

7. Lendo componentes

Entidades expõem seus componentes via entity.Components — um struct ComponentAddresses de endereços uintptr_t. Passe cada endereço para o ComponentsService::Read* correspondente para obter um snapshot tipado 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");
    }
}

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

ComponentAddresses em si contém 24 slots: os 21 acima mais três marcadores (Buffs, WorldItem, AreaTransition) e OMP (interno do host). Buffs é um marcador de presença — a lista real de buffs vem de EnumerateBuffs. WorldItem / AreaTransition são marcadores de tipo de entidade em vez de componentes reais. Todos os slots têm predicados HasX() correspondentes em ComponentAddresses.

Leitores no estilo de coleção para componentes com dados de tamanho variável:

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>

Helpers de conveniência (one-shot — eles chamam Read* internamente para você):

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)) { ... }

Uma flag Valid em cada struct retornado permite tratar "endereço do componente era 0 / leitura falhou" sem exceções. Se você já possui o struct pai (Life, Mods, …), acesse seus campos diretamente em vez de chamar o helper novamente — o helper relê o componente a cada vez.

Distinguindo efeitos de chão — muitos efeitos de chão compartilham o caminho de entidade único Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect, então o caminho sozinho não consegue distinguir Shocked Ground de Burning Ground. ReadGroundEffect resolve o componente GroundEffect da entidade e sua linha em groundeffects.datc64. Passe o endereço da entidade (o componente GroundEffect não está em Components, então o host o resolve para você — mesma convenção que ReadPathfinding), depois compare com TypeId, a chave estável independente de patch:

for (const auto& e : snap.Entities) {
    if (e.Path != L"Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect") continue;
    PluginSDK::GroundEffect ge = ctx()->Components.ReadGroundEffect(e.Address);
    if (!ge.Valid) continue;
    // ge.TypeId -> "ShockedGround" / "IgnitedGround" / "CausticCloud" / "ChilledGround" / ...
    // ge.Radius -> unidades do mundo; desenhe um círculo em (e.WorldX, e.WorldY, e.WorldZ) com esse raio
    if (ge.TypeId == "ShockedGround") {
        // destaque conforme sua config (cor/alfa indexados por ge.TypeId)
    }
}

O struct GroundEffect:

Campo Significado
Valid false se a entidade não tem componente GroundEffect ou a leitura falhou
TypeId Id de groundeffecttypes — a chave estável para comparar (ex.: ShockedGround)
Radius Raio do efeito em unidades do mundo; 0 quando a variante não o define
EndEffect Comportamento de término: fadeout / close / end
BuffVisual1 Id de buffvisuals (ex.: ground_fire_burn_white); vazio se não definido
BuffVisual2 Nome em buffdefinitions (ex.: ground_tar_gold); vazio se não definido
AoFile Primeiro caminho visual .ao/.aoc; vazio se nenhum
GroundEffectsRowAddr / GroundEffectTypesRowAddr Ponteiros brutos de linha dat (estáveis por sessão) para referência cruzada avançada

A posição no mundo do efeito vem da própria entidade (Entity.WorldX/Y/Z, ou os componentes Render/Positioned), então não é duplicada no struct. ReadGroundEffect relê a cada chamada, então armazene em cache o resultado por intervalo de scan. Retorna um GroundEffect inválido em hosts construídos antes desta API (está na cauda append-only do SDK v6 e é verificado contra null).

Stats avaliadas por skill & DPS (EnumerateSkillStats)

EnumerateSkillStats(skillDetailsAddr) expõe os próprios containers de stats avaliadas do jogo para uma skill — incluindo a família de DPS que o painel de skills do jogo exibe. Passe ActiveSkill::SkillDetailsAddr a partir de um resultado de EnumerateActiveSkills do mesmo frame (endereços de skill ficam obsoletos entre frames/mudanças de área; um endereço obsoleto retorna com segurança um vetor vazio, assim como um host construído antes desta API).

Cada SkillStatEntry retornado é {SetIndex, StatId, Value}:

  • SetIndex 0 é o conjunto de stats do contexto atual da skill — presente em toda skill, persistente (sobrevive a painéis fechados), e a fonte exata da linha de DPS do painel de skills.
  • Conjuntos posteriores são os conjuntos de stats por parte da skill — em skills de invocação/comando, as stats do lado do minion ficam ali.
  • StatId é o índice da linha de Stats.dat + 1 (a chave de stat em tempo de execução do engine; 0 é o sentinela "sem stat" do jogo). Resolva os nomes fazendo dump de Stats.dat.
  • Value é um int32 bruto; muitas stats da família DPS são fixed-point ×100.

IDs de runtime úteis:

StatId Stat (linha de Stats.dat + 1) Escala
691 hundred_times_attacks_per_second ×100
692 hundred_times_damage_per_second ×100
695 hundred_times_casts_per_second ×100
1982 / 1983 hundred_times_average_damage_per_hit / ..._per_skill_use ×100
694 base_spell_cast_time_ms ms
2079 skill_show_average_damage_instead_of_dps flag
auto skills = ctx()->Components.EnumerateActiveSkills(snap.Player.Components.Actor);
for (const auto& s : skills) {
    for (const auto& st : ctx()->Components.EnumerateSkillStats(s.SkillDetailsAddr)) {
        if (st.StatId == 692) {  // hundred_times_damage_per_second
            ctx()->Log.Info((s.Name + " DPS: " +
                             std::to_string(st.Value / 100.0)).c_str());
        }
    }
}

Ressalvas importantes:

  • A família de DPS é virtual. O engine calcula essas stats via callbacks (DPS = rate/100 × avg damage) e armazena o resultado apenas para os contextos que exibiu. O conjunto do contexto atual carrega o último valor que o próprio jogo avaliou; contextos alternativos (abas de tooltip de infusão, previews de troca de arma) são avaliados de forma transitória no hover e não são legíveis de forma persistente. Os valores podem, portanto, ficar alguns pontos percentuais atrás do tooltip ao vivo em monstros com buffs de dano dinâmicos — a própria lista de skills do jogo e o tooltip discordam da mesma forma.
  • O DPS do minion vive no minion. Os conjuntos de uma skill de invocação descrevem apenas o invocado; os números de "Basic Attack" do tooltip vêm do Actor da entidade do minion — enumere as entidades, encontre o monstro amigável e então EnumerateActiveSkills(minion.Components.Actor)EnumerateSkillStats(...) na skill de ataque dele.
  • EnumerateActiveSkills retorna duas entradas por nome de skill (contextos de avaliação diferentes, ex.: troca de armas) — consulte ambas se estiver procurando uma stat específica.

Campos de Entity

Cada Entity (incluindo snap.Player e membros de snap.Entities) carrega o mesmo conjunto de campos:

Grupo Campos
Identidade Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
Classificação EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far)
Posição GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
Vitais rápidos CurrentHP, MaxHP, CurrentES, MaxES (evita um ReadLife se você só precisa dos totais)
Strings Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, caminho de asset)
Estado IsSleeping, IsChestOpened
Componentes Components (sub-struct ComponentAddresses)

Observando uma entidade específica

Se você precisa rastrear uma entidade através de frames (por exemplo, um baú que o jogador está abrindo) e não quer varrer a lista completa de entidades a cada frame, registre uma observação:

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) retorna std::optional<Entity> para lookups one-shot, e GetPlayer() sempre retorna o jogador local.

Itens no chão (contêineres WorldItem)

Itens largados no chão aparecem em snap.Entities como entidades EntityType::Item no caminho Metadata/MiscellaneousObjects/WorldItem. Estas são entidades contêiner — elas não carregam Mods / Base / Stack / Sockets diretamente. A entidade real do item fica a uma indireção de distância.

Para obter a entidade interna do item como um snapshot Entity regular, use 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ó tem sucesso em contêineres WorldItem reais — chamá-lo com um endereço de item de inventário retorna std::nullopt. Se você quiser o mesmo formato de dados dos itens de inventário (sem percorrer componentes manualmente), a família Inventory.ReadItem* da próxima seção resolve contêineres WorldItem de forma transparente.


8. Inventário

ctx()->Inventory.Scan(inventoryId) dispara um rescan do lado do host. Use -1 para varrer todos os inventários.

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 também expõe um struct Grid descrevendo onde o inventário é desenhado na tela:

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 pegar um único inventário por id (retorna o mesmo struct com Items já populado):

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

Ou se você só quer o vetor de itens sem o struct envoltório:

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

Mods de item: duas APIs, dois escopos

ComponentsService::ReadMods(addr) retorna apenas flags de resumo (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). Ele não carrega as listas de mods por categoria.

Para o quadro completo (resumo + listas de mods), use 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)  { ... }

Outras leituras diretas por entidade (mais baratas que rescanear quando você já tem um endereço de item):

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);

Itens no chão via API de inventário. Todas as sete leituras Inventory.ReadItem* acima (e ReadItemMods) aceitam TANTO endereços de itens de inventário QUANTO endereços de contêineres WorldItem. Endereços de contêineres são resolvidos automaticamente para o item interno antes da leitura, então o mesmo caminho de código do plugin funciona para itens em mochilas e itens no chão:

// `addr` pode ser um endereço de item de inventário ou um contêiner WorldItem.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int          rarity   = ctx()->Inventory.ReadItemRarity(addr);
std::string  baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);

Se você precisar dos endereços de componentes do item interno diretamente (por exemplo, para chamar ctx()->Components.ReadStack(...) ou percorrer sockets), use Entities.GetWorldItemInner da seção 7 em vez disso.

Texto de mod no estilo do jogo + estatísticas base / agregadas (v6, 2026-06-24). Formate qualquer chave de stat no mesmo texto que o tooltip do jogo exibe, e leia os valores defensivos base de um item e as propriedades agregadas de mapas/pedras do caminho:

// Renderiza um mod da forma que o jogo faz ("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()) { /* desenha `text` */ }
}

// Valores defensivos base do item. EnergyShield é o valor calculado pelo jogo;
// Ward/Armour/Evasion são os valores base do item. Valid == false quando o item
// não tem componente Armour (moedas, gemas, joias, pedras do caminho, ...).
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// Estatísticas agregadas indexadas por stat id — ex.: Item Rarity de uma pedra do caminho (8205),
// Pack Size (8206), Monster Rarity (8207), Monster Effectiveness (8208),
// Waystone Drop Chance (8209).
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // mapeie statId -> rótulo você mesmo; os valores são percentuais com sinal
}

FormatStat usa o conjunto de descrições de stats .csd do host (baixado na primeira utilização), portanto retorna uma string vazia até que esses dados estejam prontos — use os campos brutos de Mod como alternativa. ReadItemBaseStats / ReadItemAggregatedStats aceitam endereços de item de inventário OU de contêiner WorldItem.


9. Árvore de UI

A árvore de UI do jogo é exposta como endereços uintptr_t de elementos. Comece a partir de uma raiz, percorra os filhos, leia os campos do elemento.

A maneira limpa de encontrar um painel conhecido pelo seu 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
}

Percurso manual da árvore quando você não conhece o 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

Valores StringId são identificadores estáveis do lado do jogo; prefira-os a caminhos hardcoded quando existirem.

Nota 0.5.x. Ui.GetStringId() retorna o identificador correto nos clientes atuais (0.5.x) — o offset do campo StringId do elemento mudou (0x4480x4C0) e a bridge do host foi corrigida para acompanhar. (Para o StringId numérico no qual as folhas de campo do HUD do trial renderizam seus valores — um campo diferente de GetText() — o SekhemaHelperctx()->Sekhema.GetUiStringId().)


10. Renderização & projeção

Três helpers de projeção, dois sistemas de coordenadas.

Perspectiva (mundo 3D → tela) — mesma projeção que o jogo usa para desenhar coisas no mundo. Bom para nameplates, marcadores de debug, indicadores de alvo:

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étrica (grid → minimapa) — para overlays no estilo radar desenhados no mapa grande ou pequeno. Estes respeitam o zoom, pan e rotação do mapa visível:

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 matemática em lote (pular chamadas de função por entidade), pegue a transformação uma vez e faça a projeção inline:

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.

Veja Plugins/Radar/src/Radar.cpp para um radar funcional construído inteiramente sobre essas chamadas.


11. Terreno & grid de walkable

O grid de walkable é um bitmap de 4 bits por tile indicando quais células de terreno o jogador pode pisar. O host o atualiza a cada mudança de área; plugins recebem um handle estável que sobrevive até o plugin liberá-lo (via 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 espelha o mesmo formato mas guarda um float por tile (Data() é const float*, mais ElementCount() e SizeBytes()).

Não se inscreva em OnAreaChange para atualizar o handle. O evento dispara quando o worker do host detecta uma mudança de área, mas o novo grid de walkable pode ainda não ter sido parseado — você ficaria com um ponteiro obsoleto por um ou dois frames. Em vez disso, faça polling por frame em DrawUI:

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

Isso é barato (uma chamada ABI + uma comparação de ponteiros). Veja Plugins/Radar/src/Radar.cpp para a versão de produção.

Outros acessores 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

Inscreva-se em eventos emitidos pelo host. Cada Subscribe retorna um Token que você pode mais tarde passar para Unsubscribe. O destrutor de EventsService (disparado quando o plugin é desativado ou descarregado) libera automaticamente qualquer coisa ainda pendente — então você não precisa estritamente cancelar a inscrição manualmente, mas é educado.

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);
    }
};

Os quatro tipos de eventos são AreaChange, Frame, GameAttached, GameDetached. Há também um Subscribe(EventKind, callback) genérico se você preferir construir uma tabela de dispatch.

O const_cast é necessário porque Events muta seu mapa interno de tokens. A classe base retorna const Context* para desencorajar mutar outros serviços acidentalmente.

Agrupando assinaturas

Se seu plugin é dono de várias assinaturas, o padrão do ExamplePlugin é uma maneira limpa de manter enable/disable simétricos — agrupe tokens e contadores em um único struct de estado e roteie tudo através de um par único 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;
}

Veja Plugins/ExamplePlugin/examples/ExampleEvents.h para o padrão completo.


13. OverlayService — captura de entrada e entidades adormecidas

ctx()->Overlay expõe dois "request flags" por plugin que alteram o comportamento global do overlay. O host armazena o estado por plugin indexado pelo seu ponteiro this e o combina via OR com os próprios flags internos do host (visibilidade do menu principal, bloqueio do AutoCraft, o seletor interno de Adicionar-Entidade-do-Mapa, o request de cada outro plugin). Limpos automaticamente quando o plugin é desativado/descarregado — um plugin travado ou com bugs não pode fixar permanentemente o overlay num estado estranho.

SetIncludeSleepingEntities

Por padrão, EntitiesService.Enumerate e Snapshot.Entities ocultam entidades com EntityState::Useless (o filtro "adormecido" do host que descarta monstros / NPCs / baús dormentes distantes). Isso mantém o custo do snapshot por frame sob controle — uma área típica tem centenas de entidades Useless que o plugin não precisa.

Para UIs de seleção de entidades no mapa e visualizadores de debug que precisam do pool completo de entidades da área (para que o usuário possa clicar em uma entidade que ainda não está ativa), desative o filtro:

ctx()->Overlay.SetIncludeSleepingEntities(true);
// Agora ctx()->Entities.Enumerate vê entidades Useless também.
// Entity::IsSleeping marca especificamente aquelas que vieram da coleção
// SleepingEntities separada do host (ortogonal a EntityState::Useless).

Custo: aproximadamente +5–15% de CPU de snapshot por frame enquanto ativado. Deixe DESATIVADO a menos que precise.

SetWantsOverlayInput

A janela do overlay é normalmente click-through (WS_EX_TRANSPARENT): cada clique do mouse passa diretamente para o jogo abaixo. Este é o padrão correto para overlays somente leitura (radar, barras de vida, leituras de DPS) — o jogador continua jogando sem perceber o overlay.

No momento em que seu plugin quiser que o usuário clique em algo dentro do overlay — confirmar um popup, selecionar uma entidade no mapa, arrastar um marcador — esse padrão falha. SetWantsOverlayInput(true) solicita ao host que comece a consumir cliques do mouse onde suas janelas ImGui os cobrem:

ctx()->Overlay.SetWantsOverlayInput(true);
// ...
// Quando terminar (usuário selecionou, fechou popup, pressionou Escape):
ctx()->Overlay.SetWantsOverlayInput(false);

A lógica por frame do host reivindica cliques SOMENTE onde o cursor está sobre uma janela ImGui visível — em qualquer outro lugar, o click-through é preservado para que o jogador ainda possa se mover/atacar/saquear ao redor do seu popup.

Escopo (importante):

  • Botões do mouse (LMB/RMB): SIM — controlado por este flag.
  • Posição do mouse / hover: SEMPRE funciona independentemente. Tooltips de hover não precisam deste flag.
  • Teclado: SEMPRE chega ao plugin via WindowProc do host, independente deste flag. ImGui::IsKeyPressed(ImGuiKey_Escape) funciona de qualquer forma.

Ressalva do background-draw-list — LEIA ISTO para plugins de seleção no mapa

Se você desenha marcadores clicáveis via ImGui::GetBackgroundDrawList() (típico para overlays de radar / mapa grande), o background draw list não tem uma janela ImGui por trás. O hit-test do host percorre ctx->Windows, não encontra nada sob o cursor e reativa o click-through — seus marcadores são visíveis mas não clicáveis.

A solução: abra uma janela ImGui real cobrindo a área do seletor e coloque um ImGui::InvisibleButton dentro dela. A janela é o que o host verifica no hit-test; o InvisibleButton fornece ImGui::IsItemClicked() para detecção de cliques. Esboço:

auto snap = ctx()->Game.GetSnapshot();
ImVec2 screenSize{(float)snap.ScreenWidth, (float)snap.ScreenHeight};

ImGui::SetNextWindowPos({0, 0});
ImGui::SetNextWindowSize(screenSize);
ImGui::Begin("##picker", nullptr,
    ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoTitleBar |
    ImGuiWindowFlags_NoMove       | ImGuiWindowFlags_NoResize    |
    ImGuiWindowFlags_NoScrollbar);

ImGui::InvisibleButton("##picker_hit", screenSize);
bool clickedThisFrame = ImGui::IsItemClicked();

// Desenhe marcadores via ImGui::GetForegroundDrawList() (ou GetWindowDrawList()):
ImDrawList* dl = ImGui::GetForegroundDrawList();
ctx()->Terrain.EnumerateTgtLocations([&](const auto& tgt) {
    float sx, sy;
    if (ctx()->Render.GridToLargeMap(tgt.X, tgt.Y, 0.f, sx, sy)) {
        ImVec2 mp = ImGui::GetMousePos();
        bool hovered = std::hypot(mp.x - sx, mp.y - sy) < 12.f;
        dl->AddCircleFilled({sx, sy}, 8.f, hovered ? 0xFF00FFFF : 0xFFFFFF00);
        if (clickedThisFrame && hovered) {
            // usuário selecionou este POI
        }
    }
    return true;
});
ImGui::End();

Janelas menores cobrindo apenas a região do seletor funcionam da mesma forma — o host não se importa com o tamanho, apenas que alguma janela esteja sob o cursor.

Exemplo prático — Adicionar POI do Mapa

class RadarPlugin : public PluginSDK::Plugin {
    bool m_pickerMode = false;

    void DrawUI() override {
        if (!ctx()->Game.IsInGame()) return;
        ImGui::SetCurrentContext((ImGuiContext*)ctx()->ImGuiContext);

        // Alternância por hotkey. O teclado chega ao plugin independente do
        // estado de captura, então isso funciona mesmo quando o overlay é click-through.
        if (ImGui::IsKeyPressed(ImGuiKey_F8, /*repeat=*/false)) {
            m_pickerMode = !m_pickerMode;
            ctx()->Overlay.SetIncludeSleepingEntities(m_pickerMode);
            ctx()->Overlay.SetWantsOverlayInput     (m_pickerMode);
        }
        if (m_pickerMode && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
            m_pickerMode = false;
            ctx()->Overlay.SetIncludeSleepingEntities(false);
            ctx()->Overlay.SetWantsOverlayInput     (false);
        }

        if (!m_pickerMode) return;
        // ... UI do seletor (veja a ressalva do background-draw-list acima para o
        //     padrão ImGui::Begin + InvisibleButton).
    }

    void OnDisable() override {
        // Precaução adicional. O host também limpa os flags na desativação, mas
        // a limpeza explícita mantém o estado consistente se algum frame síncrono
        // ocorrer entre OnDisable e o bookkeeping do PluginManager.
        ctx()->Overlay.SetIncludeSleepingEntities(false);
        ctx()->Overlay.SetWantsOverlayInput     (false);
    }
};

Ciclo de vida e agregação

  • Ambos os flags são idempotentes — chamar Set(true) duas vezes seguidas é uma no-op na segunda vez, o contador não é incrementado duas vezes.
  • Ambos são combinados via OR com o próprio estado do host e o flag de cada outro plugin. Múltiplos plugins em modo de seleção simultâneo coexistem sem problemas.
  • O host limpa automaticamente todos os flags de um plugin quando ele é desativado (via aba Plugins, desativação por crash ou encerramento). Um crash no meio de um frame não vai fixar permanentemente o overlay em modo de captura — mas plugins bem-comportados ainda emparelham suas chamadas de ativação/desativação para que outros plugins e o jogo permaneçam responsivos entre elas.
  • Latência: uma chamada Set atualiza o flag de forma síncrona, mas a mudança de comportamento efetiva se manifesta no próximo frame do host (entrada do overlay) ou no próximo tick do worker do GameClient (entidades adormecidas). Subframe, imperceptível.
  • Todos os métodos são seguros para chamar de qualquer thread.

14. Prices — precificação de itens carregada pelo host

O host carrega os preços de mercado uma vez por sessão do poe2scout em uma thread em segundo plano e os expõe a todos os plugins via ctx()->Prices. Os plugins não buscam preços por conta própria — há um único banco de dados de preços compartilhado por trás do radar interno, os overlays do host e todos os plugins, de modo que a API é chamada uma vez em vez de uma vez por consumidor.

  • A liga de preços é escolhida pelo usuário em Configuração → Configurações (padrão Runes of Aldur) e persistida pelo host. Os plugins sempre leem a liga selecionada pelo usuário; eles não a escolhem.
  • O carregamento é uma única vez com backoff por categoria (nova tentativa após 1 → 5 → 10 → 20 → 30 → 60 min em caso de falha, depois desiste até o app reiniciar). Não há atualização periódica — os preços são estáveis durante toda a sessão.
  • Cada preço é denominado em Caos. GetRates() fornece a conversão de Divine / Exalted se você quiser exibir nessas unidades.

Consultando um preço

PluginSDK::PriceResult p = ctx()->Prices.LookupPrice("Divine Orb");
if (p.found) {
    ctx()->Log.Info(("Divine Orb = " + std::to_string(p.chaos) + "c").c_str());
    // p.category — qual bucket do poe2scout foi correspondido (currency, fragments, runes,
    // …, ou uma categoria de item único). Útil para distinguir moeda vs. único.
}

LookupPrice recebe o nome de exibição de um item (nome de moeda, nome único ou tipo base) e faz correspondência fuzzy no lado do host em todas as categorias carregadas. Uma falha retorna found == false com todos os preços em zero.

Campo PriceResult Tipo Significado
found bool Um preço foi encontrado
chaos float Preço em Chaos Orbs (a unidade canônica)
divine float Mesmo preço expresso em Divine Orbs
exalt float Mesmo preço expresso em Exalted Orbs
category std::string Categoria do poe2scout que correspondeu (currency / fragments / runes / … / uma categoria de único)

Taxas de conversão e status de carregamento

PluginSDK::PriceRates  r = ctx()->Prices.GetRates();   // divineInChaos, exaltedInChaos
PluginSDK::PriceStatus s = ctx()->Prices.GetStatus();
if (!s.loaded) {
    // Ainda não está pronto (ainda carregando, ou todas as categorias falharam).
    // s.catsOk / s.catsPending / s.catsFailed mostram até onde o carregador chegou.
}

GetStatus().loaded é o gate a verificar antes de exibir preços — muda para verdadeiro somente quando as taxas de conversão mais pelo menos a primeira categoria chegarem. Até lá, renderize um estado "carregando preços…" em vez de zeros.


15. Dispositivos Runeshape

ctx()->Runeshape expõe os dispositivos Expedition2Encounter ("Runeshape") que o host resolveu na área atual, juntamente com a recompensa que cada receita concederia. O host faz a travessia da cadeia de dispositivos e a correspondência offline de receitas; seu plugin apenas lê o resultado. É isso que alimenta a tag de recompensa do radar interno e a janela Runeshape do NinjaPricer — um plugin de terceiros pode renderizar os mesmos dados.

for (const PluginSDK::Runeshape& rs : ctx()->Runeshape.Runeshapes()) {
    // rs.color dá a cada dispositivo uma cor distinta (use para agrupar/colorir).
    // rs.bestIndex é o índice da recompensa de maior preço (ou -1).
    for (const PluginSDK::RuneshapeReward& rw : ctx()->Runeshape.Rewards(rs.entityId)) {
        if (rw.priced)
            ctx()->Log.Info((rw.name + " x" + std::to_string(rw.count) +
                             " = " + std::to_string(rw.totalChaos) + "c").c_str());
        if (rw.propagatingCount > 0)   // 0.5.4: runa(s) que se propagam para o próximo remnant
            ctx()->Log.Info(("  propagates: " + rw.propagatingRunes).c_str());
    }
}
Campo Runeshape Tipo Significado
entityId uint64_t Id de entidade do dispositivo — passe para Rewards()
color uint32_t RGBA comprimido, estável por dispositivo (para agrupamento / coloração)
isUnique bool O dispositivo oferece uma receita de item único
holeCount int Número de orifícios de runa no âncora
anchorName std::string Nome da runa âncora
rewardCount int Número de slots de recompensa
bestIndex int Índice da recompensa de maior totalChaos, ou -1
propagatingSlots std::vector<int> Índice(s) do(s) slot(s) de orifício de runa cuja runa se propaga para o próximo remnant (carryover 0.5.4); normalmente 1, às vezes 2
Campo RuneshapeReward Tipo Significado
name std::string Nome do item de recompensa
count int Quantidade concedida
unitChaos float Preço de Caos por unidade (do serviço Prices)
totalChaos float unitChaos × count
priced bool Um preço foi encontrado para esta recompensa
propagatingRunes std::string Runa(s) no(s) slot(s) propagantes desta receita — o que se propaga se você completar esta receita; ex.: "Power" ou "Cold, Time"; vazio se a receita não cobrir o slot
propagatingCount int Número de runas propagantes para esta recompensa
propagatingHasRare bool Alguma runa propagante é rara ("roxa"/valiosa)

Os preços das recompensas vêm do mesmo banco de dados ctx()->Prices, portanto uma recompensa sem preço (priced == false) geralmente significa apenas que os preços ainda não foram carregados, ou que o item não está listado no poe2scout.

Propagação de runas (0.5.4). Cada remnant escolhe aleatoriamente um slot de runa cuja runa se propaga para o próximo remnant (no jogo: o destaque com coroa dourada na lista Runeshape Recipes). Runeshape::propagatingSlots é a lista bruta de slots; como é uma posição de slot, a runa que se propaga difere por receita, portanto RuneshapeReward::propagatingRunes a resolve por recompensa. É isso que alimenta o ponto-slot amarelo do NinjaPricer e o marcador por recompensa.


16. Dados do painel do atlas

ctx()->Atlas expõe o painel ao vivo do atlas de endgame — os nós de mapa, sua adjacência por âncora, a seleção Rite atual e os pesos de elegibilidade brutos — lidos no lado do host através dos offsets de atlas da GameLibrary. É isso que alimenta o overlay de atlas interno e o plugin de referência ForetoldRewards. Tudo parte de GetPanel(): 0 significa que a UI do atlas não está construída (fora do jogo / painel fechado).

if (!ctx()->Atlas.GetPanel()) return;                     // atlas UI absent
for (const PluginSDK::AtlasNode& n : ctx()->Atlas.Nodes()) {
    // n.gridX / n.gridY are stable per map; n.marker / n.mapState carry UI state.
    std::string name = ctx()->Atlas.GetNodeName(n.uiAddress);  // resolve lazily
}
uint32_t seed = ctx()->Atlas.GetLineSeed();               // 0 = no Rite line
Método Retorna Propósito
GetPanel() uintptr_t Endereço do painel do atlas; 0 = painel ausente
Nodes(detail) std::vector<AtlasNode> Todos os nós do atlas (gridX/Y, uiAddress, marker, flags, biome, mapState); o detail padrão pula nomes — resolva via GetNodeName()
Connections() std::vector<AtlasConnection> Adjacência por âncora (x/y + até 5 neighbors); depende do viewport
Selection(which) std::vector<AtlasGridPoint> which=0 mapas revelados da linha Rite, which=1 âncoras escolhidas (na ordem de escolha)
GetLineSeed() uint32_t Seed de seleção de recompensas do Rite; 0 = sem linha Rite / painel ausente
Weights() std::vector<AtlasWeight> Linhas brutas de pesos de elegibilidade (key, value)
GetNodeName(uiAddress) std::string Nome de exibição para o uiAddress de um nó ("" quando não resolvido)

O AtlasServiceAbi passado por valor está congelado (outro membro do HostAbi foi acrescentado depois dele), então qualquer leitura futura do atlas deve chegar como uma nova função tail do HostAbi — nunca como um novo membro do AtlasServiceAbi.


17. Dados do trial de Sekhema

ctx()->Sekhema expõe os dados do mapa de andar do Trial of the Sekhemas — o grafo estático de salas, as escolhas da run atual e as linhas de FK de conteúdo de cada sala — além das leituras de flags de StateMachine de que as salas do trial precisam. As chamadas de grafo recebem explicitamente o endereço UI do painel do trial: comece por GetPanel() (a resolução direta por índice de filho do host) ou rode seu próprio BFS da árvore de UI pré-filtrado com o barato ProbeFloor(). É isso que alimenta o plugin de referência SekhemaHelper.

uintptr_t panel = ctx()->Sekhema.GetPanel();
if (!panel) return;                                       // not on a trial floor
PluginSDK::SekhemaFloor floor = ctx()->Sekhema.GetFloor(panel);
for (const PluginSDK::SekhemaRoom& r : ctx()->Sekhema.Rooms(panel)) {
    bool chosen = r.layer < (int)floor.choices.size() && floor.choices[r.layer] == r.index;
    // r.connections lists the room indices in the NEXT layer
}
Método Retorna Propósito
GetPanel() uintptr_t Painel do trial resolvido pelo host; 0 = ausente / modo controle / fora do jogo
ProbeFloor(uiAddress) int Número de camadas do FloorData em uiAddress, 0 = não é um andar de trial (pré-filtro BFS barato)
GetFloor(panel) SekhemaFloor Cabeçalho do andar: layerCount, roomCounts, choices (índice escolhido por camada, 0xFF=nenhum), counter
Rooms(panel) std::vector<SekhemaRoom> Grafo estático de salas na ordem (layer, index); cada uma tem connections para a camada seguinte
Content(panel) std::vector<SekhemaContentEntry> Linhas de FK de conteúdo por sala, resolvidas para rowId / rowName (despache por tablePath)
GetRoomUsedFlag(sm) int Flag de StateMachine usada/fechada: 1=usada, 0=ativa, -1=ilegível
GetStateMachineValue(sm, i, out) bool Um valor de shared-state (8 B por entrada define_shared_state, na ordem de definição)
GetUiStringId(uiAddress) std::string O StringId de um elemento de UI como UTF-8 — o campo numérico no qual as folhas do HUD do trial renderizam (não Ui.GetText)

18. Persistência de configurações

Convenção: <plugin directory>/config/settings.json. Directory() retorna o caminho absoluto em UTF-8 da pasta do seu plugin.

Para configurações triviais, um writer JSON feito à mão funciona bem e mantém a DLL autocontida. Veja Plugins/Radar/src/RadarSettings.h para um exemplo funcional. O 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 dados estruturados (objetos aninhados, arrays), inclua uma biblioteca JSON real na pasta do seu plugin. O host não impõe uma escolha.

SaveSettings é chamado periodicamente (~5s) e ao desativar; você não precisa chamá-lo você mesmo.


19. Logging

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");

Todos os quatro níveis são roteados para o logger central do host. As mensagens aparecem na aba Logs do host e no arquivo de log em disco. Formate você mesmo antes de chamar; o host não aceita varargs no estilo printf.

Internamente, os métodos de conveniência emitem as strings "Debug", "Info", "Warning" e "Error" (Warn mapeia para "Warning"). A ponte do host faz uma correspondência case-insensitive, então um plugin chamando Log("warn", "msg") ainda roteia corretamente — mas os métodos de conveniência são mais claros.


20. Memória (usuário avançado)

Primitivas diretas de memória. Prefira os serviços de alto nível sempre que possível — eles entendem offsets, lidam com mudanças de ABI e são SEH-safe. Leituras diretas de memória são apropriadas apenas quando não existe nenhuma chamada de alto nível para o que você precisa.

// 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

Se você se encontra recorrendo a estes com frequência, pergunte-se se os dados que você precisa pertencem aos serviços de alto nível.


21. Bridge / segurança SEH

Cada chamada cross-DLL entre o host e um plugin roda dentro de um bloco __try / __except no lado do host. Um plugin mal comportado que dereferencia um ponteiro obsoleto, divide por zero, ou de outra forma falha dentro de uma chamada do SDK recebe um erro logado — o processo do host não crasha, o jogo continua rodando, e o usuário pode continuar usando outros plugins.

Isso não significa que plugins podem ser desleixados. SEH captura o sintoma, não a causa. Se seu plugin lança falhas a cada frame, o usuário vê uma enxurrada de logs de erro e seus dados ficam efetivamente indisponíveis. Trate retornos nulos das chamadas do SDK, verifique flags Valid em dados de componentes, e não dereferencie endereços uintptr_t diretamente — passe-os através das chamadas ComponentsService / Ui / Memory que já envelopam RPM apropriadamente.

O host pode lidar com um plugin bugado. Ele não pode lidar com uma DLL de plugin travada — um DrawSettings que leva 100ms bloqueia toda a thread de UI. Mantenha o trabalho por frame barato.


22. Armadilhas comuns

Uma lista curta de coisas que autores de plugins encontram ao integrar pela primeira vez. A maioria está documentada inline acima; coletadas aqui como checklist.

  1. OnAreaChange dispara antes do grid de walkable ser re-parseado. Não atualize WalkableGridHandle a partir do evento — faça polling por frame em DrawUI e troque quando Data() mudar. (§11)
  2. Entity::Zone é sempre None para o jogador local. É uma classificação de distância-do-jogador, então o jogador está por definição na distância zero. Não mostre isso em displays de informações do jogador.
  3. Components.ReadMods() retorna apenas flags de resumo — sem listas de mods. Para listas de mods por categoria, chame Inventory.ReadItemMods(entityAddr). (§8)
  4. Itens dropados no chão podem não ter EntitySubtype. Se você está filtrando por itens no mundo, prefira EntityType == Item || EntityType == Chest em vez de uma checagem de subtipo mais estreita.
  5. Directory() retorna um caminho absoluto. Não acrescente o diretório do EXE você mesmo — você obterá EXEDIR\EXEDIR\Plugins\X e suas escritas de configuração cairão fora da pasta do plugin.
  6. ctx() retorna const Context*. Métodos mutantes como EventsService::Subscribe requerem const_cast. Isso é intencional — serviços que não mutam devem ser impossíveis de mutar por acidente.
  7. ImGui::SetCurrentContext é por DLL. Chame-o em cada ponto de entrada que desenha (OnEnable, DrawUI, DrawSettings) porque a DLL do plugin tem seu próprio estado ImGui por padrão.
  8. Os helpers de conveniência releem o componente a cada chamada. GetHealthPercent(addr) faz um novo ReadLife(addr) internamente. Se você já tem o struct Life de uma chamada anterior, acesse seus campos diretamente em vez disso.

23. Referência rápida de serviços

Resumo de uma linha de cada método público em cada serviço. Use as seções em prosa acima para as assinaturas de tipos completas e notas de uso.

GameService

Método Retorna Propósito
GetSnapshot() Snapshot Visão completa por frame, incluindo Entities
GetState() GameState Enum: InGame, Login, Loading, …
IsAttached() bool Processo do jogo anexado
IsInGame() bool State == InGame
IsForeground() bool Janela do jogo tem foco
IsMenuVisible() bool Menu ESC / settings aberto
IsOverlayMode() bool Host está em overlay (click-through)
GetProcessId() DWORD PID do jogo
GetGameWindow() HWND Handle da janela do jogo
GetScreenSize() ScreenSize floats {Width, Height}
GetGold() int Contador de ouro do personagem (0 quando fora do jogo)
GetAreaId() std::string Id WorldArea bruto da zona atual (carrega o número do andar das Sekhemas)
GetHiveblood(out) bool Recurso da árvore Genesis (Hiveblood) → out; false em hosts antigos / fora do jogo

EntitiesService

Método Retorna Propósito
Enumerate(cb) Visita cada entidade próxima (retorna false para parar)
GetPlayer() Entity A entidade do jogador local
FindById(id) std::optional<Entity> Lookup por id de entidade
GetWorldItemInner(addr) std::optional<Entity> Entidade interna do item para um contêiner WorldItem (itens no chão)
Watch(id) Fixa uma entidade para que seus componentes permaneçam legíveis
Unwatch(id) Libera uma observação
IsWatched(id) bool Estado de observação
GetWatchedComponents(id) std::optional<ComponentAddresses> Lê componentes fixados

ComponentsService

Método Retorna Propósito
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime Struct do componente 21 leitores, um por tipo de componente
EnumerateBuffs(addr) std::vector<Buff> Buffs ativos na entidade
EnumerateActiveSkills(addr) std::vector<ActiveSkill> Skills de um componente Actor
EnumerateSkillStats(skillDetailsAddr) std::vector<SkillStatEntry> Conjuntos de stats avaliadas de UMA skill ({SetIndex, StatId, Value}, StatId = linha de Stats.dat + 1) — o conjunto 0 é o contexto atual, incl. o DPS do painel de skills (692, ×100); ver §7
EnumerateStats(addr) std::vector<StatEntry> Stats originadas de itens + buffs
EnumerateItemMods(addr) std::vector<Mod> Mods acessíveis a partir de um componente Mods
ReadGroundEffect(entityAddr) GroundEffect Tipo de efeito de chão + raio de uma entidade VisibleServerGroundEffect — passe o endereço da ENTIDADE; compare com TypeId (ShockedGround/IgnitedGround/…). Distingue efeitos que compartilham um único caminho de entidade
GetHealthPercent / GetEsPercent / GetManaPercent float Helpers de conveniência de %
IsAlive(addr) bool Health > 0
GetItemRarity(addr) int Raridade de um componente Mods
IsItemIdentified(addr) bool Flag de identificado
GetStackCount(addr) int Contagem atual do stack
IsChestOpened(addr) bool Flag de baú aberto
GetPlayerName(addr) std::string Nome do jogador de um componente Player
GetWorldPosition(renderAddr, x, y, z) bool Acessor de conveniência para coordenadas do mundo

InventoryService

Método Retorna Propósito
Scan(inventoryId) Dispara um rescan no host (-1 = todos)
Get(inventoryId) Inventory Um inventário, itens já populados
GetItems(inventoryId) std::vector<InventoryItem> Apenas itens
GetAll() std::vector<Inventory> Todos os inventários escaneados
GetName(inventoryId) const char* Nome de exibição ("Backpack", "Stash", …)
ReadItemRarity(addr) int Raridade por entidade (resolve automaticamente contêineres WorldItem)
ReadItemStackCount(addr) int Stack por entidade (resolve automaticamente contêineres WorldItem)
ReadItemBaseTypeName(addr) std::string Tipo base, resolve automaticamente contêineres WorldItem
ReadItemUniqueName(addr) std::string Nome único, resolve automaticamente contêineres WorldItem
ReadItemPath(addr) std::string Caminho Metadata/Items/..., resolve automaticamente contêineres WorldItem
ReadItemMods(addr) ItemMods Flags de resumo + 5 vetores de mods por categoria, resolve automaticamente contêineres WorldItem
FormatStat(statKey, v0, v1) std::string Texto no estilo do jogo para uma chave de stat + valor(es) via formatador .csd do host; vazio até que as descrições sejam carregadas
ReadItemBaseStats(addr) ItemBaseStats Valores defensivos base (Energy Shield calculado; Ward/Armour/Evasion base); Valid falso sem componente Armour; resolve automaticamente contêineres WorldItem
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> {statId, valor} agregados (pedra do caminho: Item Rarity 8205 / Pack Size 8206 / Monster Rarity 8207 / Monster Effectiveness 8208 / Waystone Drop Chance 8209); resolve automaticamente contêineres WorldItem

UiService

Método Retorna Propósito
Read(addr) UiElement Campos do elemento (rect, flags, contagem de filhos)
GetChildren(addr) std::vector<uintptr_t> Endereços dos elementos filhos
GetChildAt(addr, index) uintptr_t Único filho por índice
FollowPath(root, indices, count) uintptr_t Percorre um caminho de índices conhecido
IsVisible(addr) bool Elemento está na tela
GetStringId(addr) std::string Identificador estável do lado do jogo
GetText(addr) std::string Texto renderizado
ComputeScreenRect(addr, x, y, w, h) bool Rect final no espaço de tela
GetGameUiRoot() uintptr_t Raiz da UI in-game
GetUiRoot() uintptr_t Raiz de UI de topo
GetCullValue() int Threshold de cull de UI do host
FindPanelByStringId(parent, stringId) uintptr_t Lookup direcionado de descendente

RenderService

Método Retorna Propósito
WorldToScreen(wx, wy, wz, sx, sy) bool Projeção em perspectiva
GridToLargeMap(gx, gy, worldZ, sx, sy) bool Projeta para o overlay do mapa grande
GridToMiniMap(gx, gy, worldZ, sx, sy) bool Projeta para o minimapa
GetLargeMapTransform() MapTransform Transformação pré-multiplicada para matemática em lote
GetMiniMapTransform() MapTransform Mesma, para o minimapa

TerrainService

Método Retorna Propósito
GetWalkableGrid() WalkableGridHandle Handle RAII para o bitmap de walkable de 4 bits por tile
GetHeightGrid() HeightGridHandle Handle RAII para as alturas de terreno por tile
IsWalkable(gx, gy) bool Predicado de tile único
GetTerrainHeight(gx, gy) float Z no espaço do mundo
GetWorldToGridConvertor() float Fator de conversão mundo → grid
EnumerateTgtLocations(cb) Visita cada instância TGT na área atual

MemoryService

Método Retorna Propósito
Read(addr, buf, size) bool RPM raw
ReadString(addr) std::string String narrow terminada em null
ReadWString(addr) std::wstring String wide terminada em null
ReadStdWString(addr) std::wstring Lê um container std::wstring do lado do jogo (lida com SSO)
ReadStdVector(addr, elemSize, maxElems) std::vector<uint8_t> Bytes raw; reinterprete como seu tipo
GetBaseAddress() uintptr_t Base do módulo do jogo
GetModuleSize() uintptr_t Tamanho do módulo do jogo
GetPatternAddress(name) uintptr_t Lookup de pattern nomeado

LogService

Método Propósito
Debug / Info / Warn / Error(msg) Emite no nível correspondente
Log(level, msg) String de nível customizado

EventsService

Método Retorna Propósito
Subscribe(kind, cb) Token Dispatch genérico
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token Helpers de inscrição em uma linha
Unsubscribe(token) Liberação manual (o destrutor libera automaticamente de qualquer forma)

OverlayService

Método Retorna Propósito
SetIncludeSleepingEntities(enable) Opta por receber entidades EntityState::Useless em EntitiesService.Enumerate
SetWantsOverlayInput(enable) Solicita ao overlay que capture cliques do mouse em vez de ser click-through

Ambos são idempotentes, por plugin, agregados por OR com o host + outros plugins, e limpos automaticamente no Disable/Unload. Veja a seção 13 para o padrão completo (incluindo a ressalva da background-draw-list para plugins de seleção de mapa).

FlasksService

Método Retorna Propósito
GetFlask(slot) std::optional<Flask> Frasco de vida/mana por slot do cinto (0=vida, 1=mana); nullopt fora do intervalo ou fora do jogo
GetCharm(slot) std::optional<Charm> Amuleto por slot do cinto (0..2); nullopt fora do intervalo ou fora do jogo
AllFlasks() std::vector<Flask> Todos os slots de frasco incluindo vazios (entradas FlaskSlotCount())
AllCharms() std::vector<Charm> Todos os slots de amuleto incluindo vazios (entradas CharmSlotCount())
FlaskSlotCount() int32_t Número de slots de frasco (2 no POE2)
CharmSlotCount() int32_t Número de slots de amuleto (3 no POE2)

Veja a seção 8 ("Frascos & amuletos") para as tabelas de campos de Flask / Charm e a limitação do PerUseEffective.

PricesService

Método Retorna Propósito
LookupPrice(name) PriceResult Busca de preço fuzzy pelo lado do host por nome de exibição (found, chaos, divine, exalt, category)
GetRates() PriceRates Taxas de conversão Divino / Exaltado → Caos
GetStatus() PriceStatus Gate loaded + contagens por categoria (catsOk / catsPending / catsFailed)

RuneshapeService

Método Retorna Propósito
Runeshapes() std::vector<Runeshape> Todos os dispositivos Expedition2Encounter resolvidos (id, cor, âncora, bestIndex)
Rewards(entityId) std::vector<RuneshapeReward> Slots de recompensa por dispositivo, cada um com preço via o serviço Prices

AtlasService

Método Retorna Propósito
GetPanel() uintptr_t Endereço do painel do atlas; 0 = ausente
Nodes(detail) std::vector<AtlasNode> Todos os nós do atlas (coordenadas de grade, marker, mapState)
Connections() std::vector<AtlasConnection> Adjacência por âncora (depende do viewport)
Selection(which) std::vector<AtlasGridPoint> 0 = mapas revelados da linha Rite, 1 = âncoras escolhidas
GetLineSeed() uint32_t Seed de seleção de recompensas do Rite (0 = nenhum)
Weights() std::vector<AtlasWeight> Linhas brutas de pesos de elegibilidade (key, value)
GetNodeName(uiAddress) std::string Nome de exibição do nó ("" quando não resolvido)

SekhemaService

Método Retorna Propósito
GetPanel() uintptr_t Painel do trial resolvido pelo host; 0 = ausente
ProbeFloor(uiAddress) int Número de camadas do FloorData (0 = não é um andar de trial); pré-filtro barato
GetFloor(panel) SekhemaFloor Cabeçalho do andar (layerCount, roomCounts, choices, counter)
Rooms(panel) std::vector<SekhemaRoom> Grafo estático de salas na ordem (layer, index)
Content(panel) std::vector<SekhemaContentEntry> Linhas de FK de conteúdo por sala (rowId / rowName)
GetRoomUsedFlag(sm) int 1=usada, 0=ativa, -1=ilegível
GetStateMachineValue(sm, i, out) bool Um valor de shared-state (ordem de definição)
GetUiStringId(uiAddress) std::string StringId de elemento de UI como UTF-8 (campo numérico do HUD)

24. Versionamento

PluginAbi.h define:

constexpr int PLUGIN_SDK_VERSION = 6;

No tempo de carregamento, o host chama plugin->GetSDKVersion() e compara com seu próprio PLUGIN_SDK_VERSION. Incompatibilidade → o host loga um aviso e recusa carregar o plugin.

O host também verifica HostAbi::version e HostAbi::size_bytes dentro de PluginSDK_AttachHost (definido inline por PluginSDK.h quando PLUGIN_EXPORTS está definido). Se qualquer campo discordar do que o plugin foi compilado, ctx() fica não funcional. O accessor da classe base HostCompatible() reporta false nesse caso, e qualquer plugin que queira ser educado deve recusar agir:

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

25. Plugins de exemplo

Quatro plugins no repositório são projetados para serem lidos como documentação:

  • Plugins/ExamplePlugin/ — showcase de superfície ampla. Um plugin que toca quase todos os serviços, organizado como 11 sub-arquivos examples/Example*.h (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) mais um banner de resumo de cobertura. Leia isto quando quiser ver como um serviço é usado, no contexto.

  • Plugins/Radar/ — exemplo focado do mundo real. ~200 linhas. Um overlay de radar construído inteiramente sobre o SDK público — sem offsets, sem leituras raw de memória. Renderiza o mapa de walkable mais pontos por entidade via Render.GridToLargeMap. Leia isto quando quiser ver o mínimo de código para um resultado específico.

  • Plugins/KillCount/ — rastreador de kills/baús/mortes. SQLite + atlas de sprites + estado por área. Mostra como entregar persistência, arquivos de dados embarcados e um overlay tudo em uma única DLL.

  • Plugins/NinjaPricer/ — overlay de preços do poe.ninja. Fetch HTTP (API Exchange) + scan de inventário + precificação por item. Mostra código de rede, ingestão de dados de terceiros e iteração de inventário em um workflow real.


← Home

Clone this wiki locally