Skip to content

Plugin Development Guide PT

Lafko edited this page Jun 25, 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  (10 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 10 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>
    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}

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

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.

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.


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


14. 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.


15. 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.


16. 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.


17. 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.


18. 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.

19. 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}

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
EnumerateStats(addr) std::vector<StatEntry> Stats originadas de itens + buffs
EnumerateItemMods(addr) std::vector<Mod> Mods acessíveis a partir de um componente Mods
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)

20. 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;
    }
    // ...
}

21. 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