-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide PT
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.
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 dePluginAbi.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/.
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.
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; retornaPluginSDK::Plugin*. -
DestroyPlugin— destrutor; recebePluginSDK::Plugin*. -
PluginSDK_AttachHost— conecta oContext. Definido para você dentro dePluginSDK.he emitido automaticamente quandoPLUGIN_EXPORTSestá 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));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.
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.
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 transitionO 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(Entitycompleta),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();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.
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) |
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 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.
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);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.
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 thresholdValores StringId são identificadores estáveis do lado do jogo; prefira-os a caminhos hardcoded quando existirem.
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.
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
});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.
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.
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.
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.
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.
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.
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);
}
};- 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.
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.
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) |
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.
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.
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.
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.
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 patternSe 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.
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.
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.
-
OnAreaChangedispara antes do grid de walkable ser re-parseado. Não atualizeWalkableGridHandlea partir do evento — faça polling por frame emDrawUIe troque quandoData()mudar. (§11) -
Entity::Zoneé sempreNonepara 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. -
Components.ReadMods()retorna apenas flags de resumo — sem listas de mods. Para listas de mods por categoria, chameInventory.ReadItemMods(entityAddr). (§8) -
Itens dropados no chão podem não ter
EntitySubtype. Se você está filtrando por itens no mundo, prefiraEntityType == Item || EntityType == Chestem vez de uma checagem de subtipo mais estreita. -
Directory()retorna um caminho absoluto. Não acrescente o diretório do EXE você mesmo — você obteráEXEDIR\EXEDIR\Plugins\Xe suas escritas de configuração cairão fora da pasta do plugin. -
ctx()retornaconst Context*. Métodos mutantes comoEventsService::Subscriberequeremconst_cast. Isso é intencional — serviços que não mutam devem ser impossíveis de mutar por acidente. -
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. -
Os helpers de conveniência releem o componente a cada chamada.
GetHealthPercent(addr)faz um novoReadLife(addr)internamente. Se você já tem o structLifede uma chamada anterior, acesse seus campos diretamente em vez disso.
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.
| 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}
|
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Método | Propósito |
|---|---|
Debug / Info / Warn / Error(msg) |
Emite no nível correspondente |
Log(level, msg) |
String de nível customizado |
| 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) |
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;
}
// ...
}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-arquivosexamples/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 viaRender.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.