Skip to content

Plugin Development Guide UA

Lafko edited this page Jul 7, 2026 · 15 revisions

← Home


Посібник з розробки плагінів

Плагіни POEFixer — це нативні C++ DLL, які завантажуються під час виконання з Plugins/<PluginName>/<PluginName>.dll. Вони читають поточний стан гри, малюють ImGui-оверлеї, зберігають власні налаштування й підписуються на події хоста.


1. Огляд

SDK плагінів має тришарову архітектуру:

Plugin DLL  ───►  PluginSDK.h  (header-only C++ wrapper, owns std::string/vector/function)
                         │
                         ▼  inline function-pointer calls only
                  HostAbi  (pure-C ABI, POD structs only)
                         │
                         ▼  SEH-wrapped on the host side
   Host bridge: plugin_manager/bridge/Bridge_<Service>.cpp  (16 files)
                         │
                         ▼
                   GameClient + GameLibrary
  • Автори плагінів підключають рівно один заголовок: POEFixer/plugin_sdk/PluginSDK.h.
  • Цей заголовок оголошує все в namespace PluginSDK:: і підтягує C ABI з PluginAbi.h нижче. Знати про його існування корисно; заглядати туди майже ніколи не доводиться.
  • Усі контейнери std::* живуть усередині DLL плагіна. Через межу хоста переходить лише POD. Це означає, що плагін, зібраний іншою версією тулчейну, не заплутається зі STL хоста — спільними типами лишаються лише цілі, float, вказівники й невеликі структури.

Заголовки SDK тут:

  • POEFixer/plugin_sdk/PluginSDK.h — C++-обгортка, яку використовують автори плагінів.
  • POEFixer/plugin_sdk/PluginAbi.h — чистий C ABI під капотом.

Референсні плагіни, які постачаються з репозиторієм (читайте їх як документацію): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.


2. Hello-world плагін

Мінімальний плагін, що завантажується і пише повідомлення у лог хоста:

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

Зберіть як Plugins/Hello/Hello.dll, перезапустіть хост, увімкніть у вкладці Plugins.


3. Налаштування проєкту

Як канонічний шаблон використайте Plugins/ExamplePlugin/ExamplePlugin.vcxproj. Ключові налаштування:

  • Тип конфігурації: DynamicLibrary
  • Platform toolset: v143 (Visual Studio 2022)
  • Набір символів: Unicode
  • Стандарт мови: stdcpp20
  • Бібліотека виконання: MultiThreadedDLL (Release) / MultiThreadedDebugDLL (Debug). МАЄ збігатися з хостом.
  • Визначення препроцесора: PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS
  • Додаткові каталоги include: $(SolutionDir)POEFixer
  • Каталог виводу: $(SolutionDir)x64\Release\Plugins\<ВашПлагін>\
  • Ім'я цілі: має збігатися з ім'ям папки (Plugins/MyPlugin/MyPlugin.dll)

Хост сканує кожну підпапку в Plugins/ і шукає <НазваПапки>.dll. DLL мусить експортувати три символи:

  • CreatePlugin — фабрика; повертає PluginSDK::Plugin*.
  • DestroyPlugin — деструктор; приймає PluginSDK::Plugin*.
  • PluginSDK_AttachHost — під'єднує Context. Визначено за вас усередині PluginSDK.h і автоматично емітується, коли задано PLUGIN_EXPORTS.

Рекомендована розкладка джерел:

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() повертає абсолютний UTF-8-шлях, прив'язаний до каталогу EXE хоста — не додавайте шлях до EXE самостійно. Рядок каталогу зберігається за значенням усередині PluginSDK::Plugin, тож він переживає перерозподіли контейнерів хоста й цикли перезавантаження без проблем з часом життя.

Якщо хочете малювати ImGui, додайте також до <ClCompile> (хост їх теж лінкує, але ImGui з боку плагіна — per-DLL):

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

У OnEnable приєднайтеся до ImGui-контексту хоста:

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

4. Хуки життєвого циклу

PluginSDK::Plugin — це віртуальний базовий клас. Перевизначте ці методи у своєму плагіні (приблизно в порядку виклику):

Метод Викликається коли Типове застосування
const char* GetName() const Одноразово, відразу після створення Поверніть відображуване ім'я плагіна
void OnEnable(bool isGameAttached) Коли користувач вмикає плагін (або при старті, якщо стан збережено) Завантажити налаштування, підписатися на події, приєднати ImGui-контекст
void DrawSettings() Щокадру, поки відкрита панель налаштувань плагіна ImGui-контроли для вашого конфіга
void DrawUI() Щокадру, поки плагін увімкнено Малювання ImGui-оверлею (використовуйте ImGui::GetBackgroundDrawList() для оверлею над грою)
bool WantsOverlay() const Опитується щокадру Поверніть true, якщо хочете, щоб хост був у режимі оверлею (click-through)
void SaveSettings() Періодично (~5с) і при вимкненні Зберегти конфіг на диск
void OnDisable() Коли користувач вимикає плагін або при завершенні роботи хоста Звільнити ресурси, скасувати підписки на події

Обов'язковим є лише GetName; решта мають безпечні значення за замовчуванням.

Хост також викликає GetSDKVersion() (визначено у базовому класі, НЕ перевизначайте) одразу після CreatePlugin, щоб упевнитися, що плагін і хост узгоджені. Розбіжність → плагіну відмовлено.


5. Context

ctx() повертає const PluginSDK::Context*, агрегацію з 16 сервісів:

struct Context {
    GameService       Game;        // snapshot, state flags, screen size
    EntitiesService   Entities;    // enumerate, find-by-id, watch
    ComponentsService Components;  // 21 component readers + collection enumerators
    InventoryService  Inventory;   // scan + iterate + per-item helpers
    UiService         Ui;          // tree walk, FindPanelByStringId, screen-rect
    RenderService     Render;      // WorldToScreen + isometric map projection
    TerrainService    Terrain;     // walkable grid (RAII), height, TGT locations
    MemoryService     Memory;      // direct memory primitives (last resort)
    LogService        Log;         // Debug/Info/Warn/Error
    EventsService     Events;      // Subscribe / Unsubscribe / On<X>
    OverlayService    Overlay;     // SetIncludeSleepingEntities / SetWantsOverlayInput
    FlasksService     Flasks;       // life/mana flasks + charms: charges, usable, active
    PricesService     Prices;       // poe2scout item prices (host-loaded once, shared)
    RuneshapeService  Runeshape;    // Expedition2Encounter devices + per-device rewards
    AtlasService      Atlas;        // endgame-atlas nodes / adjacency / Rite selection / weights
    SekhemaService    Sekhema;      // Trial-of-the-Sekhemas floor graph / choices / content FKs
    void* ImGuiContext;            // pass to ImGui::SetCurrentContext
    void* D3DDevice;               // ID3D11Device* for texture loading
};

Огляд — для чого потрібен кожен сервіс:

Сервіс Коли потрібен
GameService Snapshot, прапорці стану, інформація про екран і вікно
EntitiesService Перебір, пошук за id, відстеження життєвого циклу
ComponentsService 21 ридер + 4 перелічувачі + ~10 допоміжних функцій
InventoryService Сканування, перебір, читання модів окремих предметів
UiService Обхід дерева, FindPanelByStringId, ComputeScreenRect
RenderService WorldToScreen, GridTo{Large,Mini}Map, перетворення
TerrainService Сітка прохідності й висот (RAII), TGT-локації
MemoryService RPM-примітиви — лише коли немає відповідного виклику вищого рівня
LogService Debug / Info / Warn / Error
EventsService Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
OverlayService SetIncludeSleepingEntities / SetWantsOverlayInput — зручно для плагінів вибору на мапі
FlasksService Колби life/mana + чарми — заряди, Usable, Active, on-use, кількість модів
PricesService LookupPrice / GetRates / GetStatus — ціни poe2scout, завантажені хостом, спільні для всіх плагінів
RuneshapeService Runeshapes / Rewards — вирішені пристрої Expedition2Encounter + нагороди на пристрій
AtlasService GetPanel / Nodes / Connections / Selection / GetLineSeed / Weights — живі дані панелі атласу ендгейму
SekhemaService GetPanel / GetFloor / Rooms / Content / читання прапорців кімнат — дані мапи поверху Trial of the Sekhemas

ctx() дійсний з моменту виклику хостом OnEnable до повернення OnDisable. Не кешуйте ctx() через межі hot-reload або вивантаження DLL.


6. Читання стану гри

ctx()->Game.GetSnapshot() повертає Snapshot за значенням — повне незмінне представлення поточного кадру. GetSnapshot() проходить по abi->entities.enumerate і заповнює snap.Entities перед поверненням, тож вартість зростає з кількістю сусідніх сутностей. Викликайте раз на кадр і перевикористовуйте.

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

Що snapshot несе безпосередньо (додаткові виклики сервісів не потрібні):

  • Стан і прапорці: State, IsAttached, IsWindowValid, GameWindowForeground, IsTown, IsHideout, IsPaused, IsSkillTreeVisible.
  • Локація: CurrentAreaName, CurrentAreaHash, CurrentAreaLevel, AreaChangeCounter.
  • Світ: Player (повна Entity), Entities (повний std::vector<Entity>), Vitals, LargeMap, MiniMap, WorldToScreenMatrix[16].
  • Вікно: ScreenWidth, ScreenHeight, ProcessId, GameWindow, LastUpdateTime, WorldToGridConvertor.

Чого немає в snapshot — отримуйте через сервіси: вміст інвентаря (InventoryService), баффи (ComponentsService::EnumerateBuffs), списки модів предмета (InventoryService::ReadItemMods), UI-панелі (UiService).

Дешеві хелпери, коли повний snapshot не потрібен:

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

GetHiveblood читає лічильник ресурсу Genesis-дерева (Hiveblood) — читання через хвостову функцію хоста, маршрутизоване через GameService (те саме сімейство, що GetGold / GetAreaId):

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

7. Читання компонентів

Сутності експонують свої компоненти через entity.Components — структуру ComponentAddresses з адрес типу uintptr_t. Передайте кожну адресу у відповідний ComponentsService::Read*, щоб отримати snapshot за значенням.

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

Існує 21 ридер компонентів: ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime.

Сама ComponentAddresses має 24 слоти: вищезгадані 21 плюс три маркери (Buffs, WorldItem, AreaTransition) і OMP (внутрішній для хоста). Buffs — це маркер присутності; фактичний список баффів отримується через EnumerateBuffs. WorldItem / AreaTransition — маркери типу сутності, а не справжні компоненти. Усі слоти мають відповідні предикати HasX() у ComponentAddresses.

Ридери у вигляді колекцій для компонентів зі змінним розміром даних:

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>

Зручні хелпери (one-shot — внутрішньо викликають Read* за вас):

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

Прапорець Valid у кожній поверненій структурі дозволяє опрацьовувати випадок «адреса компонента була 0 / читання провалилось» без винятків. Якщо у вас уже є батьківська структура (Life, Mods, …), звертайтеся до її полів напряму замість повторного виклику хелпера — хелпер читає компонент знову кожного разу.

Розрізнення наземних ефектів — багато наземних ефектів мають однаковий шлях сутності Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect, тому за самим шляхом неможливо відрізнити Shocked Ground від Burning Ground. ReadGroundEffect розв'язує компонент GroundEffect сутності та відповідний рядок groundeffects.datc64. Передайте адресу сутності (компонент GroundEffect відсутній у Components, тому хост розв'язує його за вас — та сама конвенція, що й ReadPathfinding), а потім зіставляйте за TypeId — стабільним, патч-незалежним ключем:

for (const auto& e : snap.Entities) {
    if (e.Path != L"Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect") continue;
    PluginSDK::GroundEffect ge = ctx()->Components.ReadGroundEffect(e.Address);
    if (!ge.Valid) continue;
    // ge.TypeId -> "ShockedGround" / "IgnitedGround" / "CausticCloud" / "ChilledGround" / ...
    // ge.Radius -> одиниці світу; намалюйте коло в (e.WorldX, e.WorldY, e.WorldZ) цього радіуса
    if (ge.TypeId == "ShockedGround") {
        // підсвітьте відповідно до вашого конфіга (колір/альфа за ge.TypeId)
    }
}

Структура GroundEffect:

Поле Значення
Valid false, якщо у сутності немає компонента GroundEffect або читання провалилося
TypeId Id groundeffecttypes — стабільний ключ для зіставлення (напр. ShockedGround)
Radius Радіус ефекту в одиницях світу; 0, якщо варіант не задає значення
EndEffect Поведінка завершення: fadeout / close / end
BuffVisual1 Id buffvisuals (напр. ground_fire_burn_white); порожньо, якщо не задано
BuffVisual2 Назва buffdefinitions (напр. ground_tar_gold); порожньо, якщо не задано
AoFile Перший візуальний шлях .ao/.aoc; порожньо, якщо відсутній
GroundEffectsRowAddr / GroundEffectTypesRowAddr Сирі вказівники dat-рядків (стабільні в межах сесії) для поглибленого перехресного зіставлення

Позиція ефекту у світовому просторі береться з самої сутності (Entity.WorldX/Y/Z або компоненти Render/Positioned), тому вона не дублюється у структурі. ReadGroundEffect перечитує дані при кожному виклику — кешуйте результат на інтервал сканування. На хостах, зібраних до цього API, повертає недійсний GroundEffect (метод розташований у append-only хвості SDK v6 і перевіряється на null).

Поля Entity

Кожна Entity (включно зі snap.Player і членами snap.Entities) несе той самий набір полів:

Група Поля
Identity Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
Classification EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far)
Position GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
Quick vitals CurrentHP, MaxHP, CurrentES, MaxES (avoids a ReadLife if you only need the totals)
Strings Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, asset path)
State IsSleeping, IsChestOpened
Components Components (ComponentAddresses sub-struct)

Відстеження конкретної сутності

Якщо потрібно відстежувати одну сутність крізь кадри (наприклад, скриню, яку відкриває гравець) і не хочеться сканувати весь список сутностей кожного кадру, зареєструйте watch:

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) повертає std::optional<Entity> для одноразових пошуків, а GetPlayer() завжди повертає локального гравця.

Предмети на землі (контейнери WorldItem)

Предмети, кинуті на землю, з'являються у snap.Entities як сутності з EntityType::Item за шляхом Metadata/MiscellaneousObjects/WorldItem. Це сутності-контейнери — вони не несуть напряму Mods / Base / Stack / Sockets. Справжня сутність предмета знаходиться на одну непрямість далі.

Щоб отримати внутрішню сутність предмета як звичайний снапшот Entity, використовуйте 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 спрацьовує лише для справжніх контейнерів WorldItem — виклик з адресою предмета інвентаря поверне std::nullopt. Якщо вам потрібна та сама форма даних, що й для предметів інвентаря (без ручного обходу компонентів), сімейство Inventory.ReadItem* із наступного розділу прозоро автоматично розв'язує контейнери WorldItem.


8. Інвентар

ctx()->Inventory.Scan(inventoryId) запускає повторне сканування на стороні хоста. Використовуйте -1, щоб просканувати всі інвентарі.

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

Кожен Inventory також експонує структуру Grid, що описує, де інвентар намальовано на екрані:

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

Щоб отримати окремий інвентар за id (повертає ту саму структуру з уже заповненим Items):

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

Або якщо потрібен лише вектор предметів без обгортки:

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

Моди предметів: два API, два рівні деталізації

ComponentsService::ReadMods(addr) повертає лише підсумкові прапорці (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). Він не несе списки модів за категоріями.

Для повної картини (підсумок + списки модів) використовуйте 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)  { ... }

Інші прямі читання за сутністю (дешевше за повторне сканування, якщо у вас уже є адреса предмета):

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

Предмети на землі через API інвентаря. Усі сім читань Inventory.ReadItem* вище (і ReadItemMods) приймають ЯК адреси предметів інвентаря, ТАК і адреси контейнерів WorldItem. Адреси контейнерів автоматично розв'язуються до внутрішнього предмета перед читанням, тож один і той самий шлях коду плагіна працює і для предметів у сумках, і для предметів на землі:

// `addr` може бути або адресою предмета інвентаря, або контейнером WorldItem.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int          rarity   = ctx()->Inventory.ReadItemRarity(addr);
std::string  baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);

Якщо вам потрібні адреси компонентів внутрішнього предмета напряму (наприклад, щоб викликати ctx()->Components.ReadStack(...) або пройтися по сокетах), використовуйте натомість Entities.GetWorldItemInner із розділу 7.

Текст мода у стилі гри + базові / агреговані статистики (v6, 2026-06-24). Відформатуйте будь-який ключ статистики в той самий текст, який показує внутрішньоігровий тултіп, і прочитайте базові захисні значення предмета та агреговані властивості мап/вейстонів:

// Відрендерити мод так, як це робить гра ("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()) { /* відобразити `text` */ }
}

// Базові захисні значення предмета. EnergyShield — це внутрішньоігрове (обчислене) значення;
// Ward/Armour/Evasion — базові значення предмета. Valid == false, якщо предмет
// не має компонента Armour (валюта, самоцвіти, ювелірні вироби, вейстони тощо).
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// Агреговані статистики за ключем stat id — наприклад, Item Rarity вейстона (8205),
// Pack Size (8206), Monster Rarity (8207), Monster Effectiveness (8208),
// Waystone Drop Chance (8209).
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // відобразіть statId -> мітку самостійно; значення є знаковими відсотками
}

FormatStat використовує набір описів статистик .csd хоста (завантажується при першому використанні), тому повертає порожній рядок, поки ці дані не готові — використовуйте запасний варіант з необробленими полями Mod. ReadItemBaseStats / ReadItemAggregatedStats обидва приймають як адреси предметів інвентаря, так і адреси контейнерів WorldItem.


9. UI-дерево

UI-дерево гри експонується як адреси елементів типу uintptr_t. Почніть з кореня, обходьте дітей, читайте поля елементів.

Чистий спосіб знайти відому панель за її 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
}

Ручний обхід дерева, якщо 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

Значення StringId — це стабільні ідентифікатори з боку гри; коли вони існують, надавайте їм перевагу над жорстко заданими шляхами.

Примітка щодо 0.5.x. Ui.GetStringId() повертає коректний ідентифікатор на поточних (0.5.x) клієнтах — зсув поля StringId елемента переїхав (0x4480x4C0), і міст хоста було виправлено відповідно. (Для числового StringId, у який field-листки HUD випробування рендерять свої значення — це інше поле, ніж GetText()SekhemaHelper читає ctx()->Sekhema.GetUiStringId().)


10. Render і проєкція

Три проєкційні хелпери, дві системи координат.

Перспектива (3D world → screen) — та сама проєкція, якою гра малює об'єкти у світі. Підходить для табличок з іменами, маркерів дебагу, індикаторів цілей:

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

Ізометрія (grid → minimap) — для оверлеїв на кшталт радара, що малюються на великій або міні-мапі. Вони враховують зум, панорамування й обертання видимої мапи:

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)

Для пакетної математики (без виклику функції на кожну сутність) візьміть трансформацію один раз і виконуйте проєкцію 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.

Дивіться Plugins/Radar/src/Radar.cpp для робочого радара, побудованого виключно на цих викликах.


11. Рельєф і сітка прохідності

Сітка прохідності — це бітмап з 4 бітами на тайл, який показує, на які клітинки рельєфу гравець може ступити. Хост оновлює її при кожній зміні локації; плагіни отримують стабільний хендл, що живе, доки плагін його не звільнить (через 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 дзеркалить ту саму форму, але містить один float на тайл (Data()const float*, плюс ElementCount() і SizeBytes()).

Не підписуйтеся на OnAreaChange, щоб оновити хендл. Подія спрацьовує, коли воркер хоста виявляє зміну локації, але нова сітка прохідності може ще не бути розібраною — і ви триматимете застарілий вказівник один-два кадри. Замість цього опитуйте щокадру в DrawUI:

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

Це дешево (один виклик ABI + одне порівняння вказівників). Дивіться Plugins/Radar/src/Radar.cpp для production-версії.

Інші аксесори рельєфу:

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. Події

Підпишіться на події, які емітує хост. Кожен Subscribe повертає Token, який пізніше можна передати в Unsubscribe. Деструктор EventsService (спрацьовує при вимкненні або вивантаженні плагіна) автоматично звільняє все, що лишилося — тому суворо вручну відписуватися не обов'язково, але це гарний тон.

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

Чотири види подій — AreaChange, Frame, GameAttached, GameDetached. Є також узагальнений Subscribe(EventKind, callback), якщо вам зручніше будувати таблицю диспетчеризації.

const_cast обов'язковий, тому що Events мутує свою внутрішню мапу токенів. Базовий клас повертає const Context*, щоб важче було випадково змінити інші сервіси.

Групування підписок

Якщо ваш плагін володіє кількома підписками, патерн з ExamplePlugin — це чистий спосіб тримати enable/disable симетричними: складіть токени й лічильники в одну структуру стану й маршрутизуйте все через одну пару 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;
}

Дивіться Plugins/ExamplePlugin/examples/ExampleEvents.h для повного патерну.


13. OverlayService — захоплення вводу та сплячі сутності

ctx()->Overlay надає два «прапорці запиту» на плагін, що змінюють поведінку оверлея в цілому. Хост зберігає стан кожного плагіна за його вказівником this і об'єднує їх через OR із власними вбудованими прапорцями (видимість головного меню, блокування AutoCraft, вбудований хостовий засіб вибору сутностей з мапи, прапорці кожного іншого плагіна). Автоматично скидаються при вимкненні/вивантаженні плагіна — плагін, що впав або має помилку, не може назавжди залишити оверлей у некоректному стані.

SetIncludeSleepingEntities

За замовчуванням EntitiesService.Enumerate і Snapshot.Entities приховують сутності з EntityState::Useless (фільтр «сплячих» хоста, який відкидає далеких неактивних монстрів / NPC / скрині). Це обмежує вартість снапшота за кадр — у типовій локації є сотні Useless-сутностей, які плагіну не потрібні.

Для UI вибору на мапі та переглядачів налагоджування, яким потрібен повний пул сутностей локації (щоб користувач міг клікнути на ще неактивну сутність), вимкніть фільтр:

ctx()->Overlay.SetIncludeSleepingEntities(true);
// Тепер ctx()->Entities.Enumerate бачить і Useless-сутності.
// Entity::IsSleeping позначає саме ті, що надійшли з окремої колекції
// SleepingEntities хоста (ортогонально до EntityState::Useless).

Вартість: приблизно +5–15% CPU снапшота за кадр під час увімкнення. Залишайте ВИМКНЕНИМ, якщо не потрібно.

SetWantsOverlayInput

Вікно оверлея зазвичай є наскрізним (WS_EX_TRANSPARENT): кожен клік миші проходить безпосередньо до гри під ним. Це правильне значення за замовчуванням для оверлеїв лише для читання (радар, смуги здоров'я, показники DPS) — гравець продовжує грати, не помічаючи оверлея.

Щойно плагін хоче, щоб користувач клікнув по чомусь всередині оверлея — підтвердив спливне вікно, вибрав сутність на мапі, перетягнув маркер — це значення за замовчуванням ламається. SetWantsOverlayInput(true) просить хост почати захоплювати кліки миші там, де ваші ImGui-вікна їх перекривають:

ctx()->Overlay.SetWantsOverlayInput(true);
// ...
// Коли завершено (користувач вибрав, закрив спливне вікно, натиснув Escape):
ctx()->Overlay.SetWantsOverlayInput(false);

Логіка хоста за кадр захоплює кліки ЛИШЕ там, де курсор знаходиться над видимим ImGui-вікном — будь-де ще наскрізний режим зберігається, тому гравець може рухатися/атакувати/підбирати предмети навколо вашого спливного вікна.

Область дії (важливо):

  • Кнопки миші (ЛКМ/ПКМ): ТАК — управляється цим прапорцем.
  • Позиція миші / наведення: ЗАВЖДИ працює незалежно. Підказки при наведенні не потребують цього прапорця.
  • Клавіатура: ЗАВЖДИ надходить до плагіна через WindowProc хоста, незалежно від цього прапорця. ImGui::IsKeyPressed(ImGuiKey_Escape) працює в будь-якому разі.

Застереження щодо фонового списку малювання — ПРОЧИТАЙТЕ ЦЕ для плагінів вибору на мапі

Якщо ви малюєте кліковні маркери через ImGui::GetBackgroundDrawList() (типово для радарних або великомапних оверлеїв), фоновий список малювання не має підтримки ImGui-вікна. Хостова перевірка попадання обходить ctx->Windows, нічого не знаходить під курсором і знову вмикає наскрізний режим — ваші маркери видимі, але на них не можна клікнути.

Виправлення: відкрийте справжнє ImGui-вікно, що охоплює область вашого засобу вибору, і розмістіть всередині ImGui::InvisibleButton. Саме вікно перевіряє хост; InvisibleButton дає вам ImGui::IsItemClicked() для визначення кліків. Схема:

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

// Малюйте маркери через ImGui::GetForegroundDrawList() (або 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) {
            // користувач вибрав цей POI
        }
    }
    return true;
});
ImGui::End();

Менші вікна, що охоплюють лише область вибору, працюють так само — хост не переймається розміром, лише тим, що якесь вікно знаходиться під курсором.

Практичний приклад — Додавання POI з мапи

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

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

        // Перемикання гарячою клавішею. Клавіатура надходить до плагіна незалежно від стану
        // захоплення, тому це працює, навіть коли оверлей є наскрізним.
        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 засобу вибору (дивіться розділ «Застереження щодо фонового
        //     списку малювання» вище для патерну ImGui::Begin + InvisibleButton).
    }

    void OnDisable() override {
        // Перестраховка. Хост також скидає прапорці при вимкненні, але явне
        // очищення підтримує узгодженість стану, якщо між OnDisable і
        // обробкою PluginManager спрацює синхронний кадр.
        ctx()->Overlay.SetIncludeSleepingEntities(false);
        ctx()->Overlay.SetWantsOverlayInput     (false);
    }
};

Життєвий цикл та агрегація

  • Обидва прапорці є ідемпотентними — подвійний виклик Set(true) є no-op; лічильник не подвоюється.
  • Обидва об'єднуються через OR із власним станом хоста та прапорцями кожного іншого плагіна. Декілька плагінів у режимі вибору одночасно чудово співіснують.
  • Хост автоматично скидає всі прапорці плагіна при його вимкненні (через вкладку Plugins, вимкненні після збою або завершенні роботи). Збій посеред кадру не залишить оверлей назавжди у режимі захоплення — але добре поведені плагіни все одно поєднують виклики on/off, щоб інші плагіни та гра лишались чутливими.
  • Затримка: виклик Set оновлює прапорець синхронно, але ефективна зміна поведінки проявляється на наступному кадрі хоста (введення оверлея) або наступному тіку воркера GameClient (сплячі сутності). Субкадрово, непомітно.
  • Усі методи безпечно викликати з будь-якого потоку.

14. Prices — ціни предметів, завантажені хостом

Хост завантажує ринкові ціни один раз за сесію з poe2scout у фоновому потоці й надає їх кожному плагіну через ctx()->Prices. Плагіни не отримують ціни самостійно — за вбудованим радаром, оверлеями хоста та всіма плагінами стоїть єдина спільна база даних цін, тому API викликається один раз, а не по одному разу на споживача.

  • Ліга цін обирається користувачем у Конфігурація → Налаштування (за замовчуванням Runes of Aldur) і зберігається на стороні хоста. Плагіни завжди зчитують обрану користувачем лігу; самостійно вибрати її не можна.
  • Завантаження є одноразовим з відкотом за категоріями (повтор через 1 → 5 → 10 → 20 → 30 → 60 хв при помилці, потім відмова до перезапуску додатку). Регулярного оновлення немає — ціни стабільні протягом усієї сесії.
  • Кожна ціна є деномінованою в Chaos. GetRates() надає конвертацію Divine / Exalted, якщо хочете відображати в цих одиницях.

Отримання ціни

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 — яке сховище poe2scout збіглося (currency, fragments, runes,
    // …, або категорія унікального предмета). Зручно для розгалуження currency vs. unique.
}

LookupPrice приймає відображувану назву предмета (назву валюти, унікальної або базового типу) і виконує нечітке зіставлення на стороні хоста по всіх завантажених категоріях. Збіг не знайдено → found == false, усі ціни нульові.

Поле PriceResult Тип Значення
found bool Ціну знайдено
chaos float Ціна в Chaos Orbs (канонічна одиниця)
divine float Та сама ціна в Divine Orbs
exalt float Та сама ціна в Exalted Orbs
category std::string Категорія poe2scout, що збіглася (currency / fragments / runes / … / категорія унікального)

Курси та стан завантаження

PluginSDK::PriceRates  r = ctx()->Prices.GetRates();   // divineInChaos, exaltedInChaos
PluginSDK::PriceStatus s = ctx()->Prices.GetStatus();
if (!s.loaded) {
    // Ще не готово (завантажується або всі категорії зазнали помилки).
    // s.catsOk / s.catsPending / s.catsFailed показують, наскільки далеко просунувся завантажувач.
}

GetStatus().loaded — це ворота для перевірки перед відображенням цін — стає true лише після того, як надійшли курси конвертації і принаймні перша категорія. До того часу відображайте стан «завантаження цін…» замість нулів.


15. Пристрої Runeshape

ctx()->Runeshape надає пристрої Expedition2Encounter («Runeshape»), які хост знайшов у поточній локації, разом із нагородою, яку давав би кожен рецепт. Хост сам обходить ланцюжок пристрою і виконує офлайн-зіставлення рецепта; ваш плагін просто читає результат. Саме це живить тег нагороди вбудованого радара і вікно Runeshape в NinjaPricer — сторонній плагін може відрисовувати ті самі дані.

for (const PluginSDK::Runeshape& rs : ctx()->Runeshape.Runeshapes()) {
    // rs.color надає кожному пристрою власний колір (для групування/підфарбовування).
    // rs.bestIndex — індекс найдорожчої нагороди (або -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: руна(и), що переносяться на наступний remnant
            ctx()->Log.Info(("  propagates: " + rw.propagatingRunes).c_str());
    }
}
Поле Runeshape Тип Значення
entityId uint64_t id сутності пристрою — передається в Rewards()
color uint32_t Запакований RGBA, стабільний на пристрій (для групування / підфарбовування)
isUnique bool Пристрій пропонує рецепт унікального предмета
holeCount int Кількість отворів під руни на якорі
anchorName std::string Назва якорної руни
rewardCount int Кількість слотів нагород
bestIndex int Індекс нагороди з найбільшим totalChaos, або -1
propagatingSlots std::vector<int> Індекс(и) слота отвору, чия руна переноситься на наступний remnant (carryover 0.5.4); зазвичай 1, іноді 2
Поле RuneshapeReward Тип Значення
name std::string Назва предмета-нагороди
count int Кількість, що видається
unitChaos float Ціна за одиницю в Chaos (з сервісу Prices)
totalChaos float unitChaos × count
priced bool Для цієї нагороди знайдено ціну
propagatingRunes std::string Руна(и) у пропагованому слоті(ах) цього рецепта — те, що переноситься при виконанні рецепта; напр. "Power" або "Cold, Time"; порожньо, якщо рецепт не покриває слот
propagatingCount int Кількість пропагованих рун для цієї нагороди
propagatingHasRare bool Хоча б одна пропагована руна є рідкісною («фіолетовою»/цінною)

Ціни нагород беруться з тієї самої бази ctx()->Prices, тому нагорода без ціни (priced == false) зазвичай означає, що ціни ще не завантажились, або предмет відсутній на poe2scout.

Поширення рун (0.5.4). Кожен remnant випадково обирає один слот руни, чия руна переноситься на наступний remnant (у грі — золота підсвітка з короною в списку Runeshape Combinations). Runeshape::propagatingSlots — це сирий список слотів; оскільки це позиція слота, переносна руна різниться між рецептами, тому RuneshapeReward::propagatingRunes вирішує це для кожної нагороди. На цьому побудовано жовту точку слота і мітку на нагороду в NinjaPricer.


16. Дані панелі атласу

ctx()->Atlas надає живу панель атласу ендгейму — вузли мап, їхню суміжність за якорями, поточний вибір Rite і сирі ваги придатності — читані на боці хоста через зсуви атласу з GameLibrary. Саме це живить вбудований оверлей атласу та референсний плагін ForetoldRewards. Усе відштовхується від GetPanel(): 0 означає, що UI атласу не побудовано (не в грі / панель закрита).

if (!ctx()->Atlas.GetPanel()) return;                     // atlas UI absent
for (const PluginSDK::AtlasNode& n : ctx()->Atlas.Nodes()) {
    // n.gridX / n.gridY are stable per map; n.marker / n.mapState carry UI state.
    std::string name = ctx()->Atlas.GetNodeName(n.uiAddress);  // resolve lazily
}
uint32_t seed = ctx()->Atlas.GetLineSeed();               // 0 = no Rite line
Метод Повертає Призначення
GetPanel() uintptr_t Адреса панелі атласу; 0 = панель відсутня
Nodes(detail) std::vector<AtlasNode> Усі вузли атласу (gridX/Y, uiAddress, marker, flags, biome, mapState); типовий detail пропускає імена — вирішуйте через GetNodeName()
Connections() std::vector<AtlasConnection> Суміжність за якорями (x/y + до 5 neighbors); залежить від в'юпорта
Selection(which) std::vector<AtlasGridPoint> which=0 — відкриті мапи лінії Rite, which=1 — вибрані якорі (в порядку вибору)
GetLineSeed() uint32_t Сід вибору нагород Rite; 0 = немає лінії Rite / панель відсутня
Weights() std::vector<AtlasWeight> Сирі рядки ваг придатності (key, value)
GetNodeName(uiAddress) std::string Відображуване ім'я вузла за його uiAddress ("", якщо не вирішено)

Переданий за значенням AtlasServiceAbi заморожено (після нього було додано інший член HostAbi), тому будь-яке майбутнє читання атласу має з'являтися як нова хвостова функція HostAbi — і ніколи як новий член AtlasServiceAbi.


17. Дані випробування Sekhema

ctx()->Sekhema надає дані мапи поверхів Trial of the Sekhemas — статичний граф кімнат, вибори поточного забігу та рядки content-FK кожної кімнати — плюс читання прапорців StateMachine, потрібні кімнатам випробування. Графові виклики приймають UI-адресу панелі випробування явно: почніть з GetPanel() (пряме вирішення за індексами дітей на боці хоста) або виконайте власний BFS по дереву UI з дешевим передфільтром ProbeFloor(). Саме це живить референсний плагін SekhemaHelper.

uintptr_t panel = ctx()->Sekhema.GetPanel();
if (!panel) return;                                       // not on a trial floor
PluginSDK::SekhemaFloor floor = ctx()->Sekhema.GetFloor(panel);
for (const PluginSDK::SekhemaRoom& r : ctx()->Sekhema.Rooms(panel)) {
    bool chosen = r.layer < (int)floor.choices.size() && floor.choices[r.layer] == r.index;
    // r.connections lists the room indices in the NEXT layer
}
Метод Повертає Призначення
GetPanel() uintptr_t Панель випробування, вирішена хостом; 0 = відсутня / режим контролера / не в грі
ProbeFloor(uiAddress) int Кількість шарів FloorData за адресою uiAddress, 0 = не поверх випробування (дешевий передфільтр для BFS)
GetFloor(panel) SekhemaFloor Заголовок поверху: layerCount, roomCounts, choices (вибраний індекс на шар, 0xFF=немає), counter
Rooms(panel) std::vector<SekhemaRoom> Статичний граф кімнат у порядку (layer, index); кожна має connections у наступний шар
Content(panel) std::vector<SekhemaContentEntry> Рядки content-FK по кімнатах, вирішені в rowId / rowName (диспетчеризуйте за tablePath)
GetRoomUsedFlag(sm) int Прапорець StateMachine «використана/закрита»: 1=використана, 0=активна, -1=не читається
GetStateMachineValue(sm, i, out) bool Одне значення shared-state (8 байтів на запис define_shared_state, у порядку оголошення)
GetUiStringId(uiAddress) std::string StringId UI-елемента як UTF-8 — числове поле, в яке рендерять значення листки HUD випробування (не Ui.GetText)

18. Збереження налаштувань

Конвенція: <plugin directory>/config/settings.json. Directory() повертає абсолютний UTF-8-шлях до папки вашого плагіна.

Для тривіальних налаштувань самостійно написаний JSON-райтер цілком підходить і тримає DLL самодостатнім. Дивіться Plugins/Radar/src/RadarSettings.h як робочий приклад. Скелет:

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

Для структурованих даних (вкладені об'єкти, масиви) додайте справжню JSON-бібліотеку в папку свого плагіна. Хост не нав'язує вибору.

SaveSettings викликається періодично (~5с) і при вимкненні; самостійно викликати його не потрібно.


19. Логування

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

Усі чотири рівні маршрутизуються до центрального логера хоста. Повідомлення з'являються у вкладці Logs хоста й у файлі логу на диску. Форматуйте текст самостійно перед викликом; хост не приймає varargs у стилі printf.

Внутрішньо зручні методи емітують рядки "Debug", "Info", "Warning" та "Error" (Warn мапиться у "Warning"). Bridge хоста виконує case-insensitive порівняння, тож плагін, що викликає Log("warn", "msg"), теж маршрутизується коректно — але зручні методи прозоріші.


20. Memory (power user)

Прямі примітиви читання пам'яті. Надавайте перевагу високорівневим сервісам скрізь, де можливо — вони знають оффсети, опрацьовують зміни ABI й безпечні щодо SEH. Прямі читання пам'яті доречні лише тоді, коли немає виклику вищого рівня, що покриває вашу потребу.

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

Якщо ви часто тягнетеся до цих викликів, запитайте себе, чи не належать потрібні вам дані до високорівневих сервісів.


21. Bridge / SEH-безпека

Кожен крос-DLL-виклик між хостом і плагіном виконується всередині блоку __try / __except на стороні хоста. Плагін, який поводиться неправильно — розіменовує застарілий вказівник, ділить на нуль або інакше падає всередині виклику SDK — отримує залогену помилку, а процес хоста не падає, гра продовжує працювати, і користувач може продовжувати користуватися іншими плагінами.

Це не означає, що плагіни можна писати неохайно. SEH ловить симптом, а не причину. Якщо ваш плагін падає кожного кадру — користувач бачить потік помилок у логах, і ваші дані фактично недоступні. Опрацьовуйте null-повернення з викликів SDK, перевіряйте прапорці Valid у даних компонентів і не розіменовуйте адреси uintptr_t напряму — пропускайте їх через виклики ComponentsService / Ui / Memory, які вже правильно обгортають RPM.

Хост може впоратись з багатим плагіном. Він не може впоратись із зависаючим DLL-плагіном — DrawSettings, що займає 100 мс, блокує весь UI-потік. Тримайте роботу на кадр дешевою.


22. Поширені пастки

Короткий перелік речей, з якими стикаються автори плагінів під час першої інтеграції. Більшість з них описано вище інлайн; зібрано тут як чек-лист.

  1. OnAreaChange спрацьовує до того, як сітка прохідності розібрана наново. Не оновлюйте WalkableGridHandle з події — опитуйте щокадру в DrawUI і робіть swap, коли змінюється Data(). (§11)
  2. Entity::Zone завжди None для локального гравця. Це класифікація за відстанню від гравця, тож гравець за визначенням на нульовій відстані. Не показуйте це в інформаційних блоках про гравця.
  3. Components.ReadMods() повертає лише підсумкові прапорці — без списків модів. Для списків модів за категоріями викликайте Inventory.ReadItemMods(entityAddr). (§8)
  4. Предмети, кинуті на землю, можуть не мати EntitySubtype. Якщо фільтруєте предмети у світі, надайте перевагу EntityType == Item || EntityType == Chest перед вужчою перевіркою підтипу.
  5. Directory() повертає абсолютний шлях. Не додавайте каталог EXE самостійно — отримаєте EXEDIR\EXEDIR\Plugins\X, і записи конфіга опиняться поза папкою плагіна.
  6. ctx() повертає const Context*. Методи, що мутують стан, як-от EventsService::Subscribe, потребують const_cast. Це навмисно — сервіси, які не мутують, мають бути неможливими для випадкової зміни.
  7. ImGui::SetCurrentContext — per-DLL. Викликайте його в кожній точці входу, де ви малюєте (OnEnable, DrawUI, DrawSettings), бо DLL плагіна за замовчуванням має власний стан ImGui.
  8. Зручні хелпери щоразу читають компонент заново. GetHealthPercent(addr) внутрішньо робить свіжий ReadLife(addr). Якщо у вас уже є структура Life з попереднього виклику, звертайтеся до її полів напряму.

23. Швидкий довідник по сервісах

Однорядковий підсумок кожного публічного методу кожного сервісу. Повні сигнатури типів і нотатки про використання — у текстових секціях вище.

GameService

Метод Повертає Призначення
GetSnapshot() Snapshot Повне per-frame представлення, включно з Entities
GetState() GameState Enum: InGame, Login, Loading, …
IsAttached() bool Процес гри прикріплено
IsInGame() bool State == InGame
IsForeground() bool Вікно гри має фокус
IsMenuVisible() bool Відкрите ESC-меню / налаштування
IsOverlayMode() bool Хост у режимі оверлею (click-through)
GetProcessId() DWORD PID гри
GetGameWindow() HWND Хендл вікна гри
GetScreenSize() ScreenSize {Width, Height} floats
GetGold() int Лічильник золота персонажа (0, коли не в грі)
GetAreaId() std::string Сирий id WorldArea поточної зони (несе номер поверху Sekhemas)
GetHiveblood(out) bool Ресурс Genesis-дерева (Hiveblood) → out; false на старіших хостах / не в грі

EntitiesService

Метод Повертає Призначення
Enumerate(cb) Відвідати кожну сусідню сутність (повернути false, щоб зупинити)
GetPlayer() Entity Сутність локального гравця
FindById(id) std::optional<Entity> Пошук за entity id
GetWorldItemInner(addr) std::optional<Entity> Внутрішня сутність предмета для контейнера WorldItem (предмети на землі)
Watch(id) Закріпити сутність, щоб її компоненти лишалися читабельними
Unwatch(id) Звільнити watch
IsWatched(id) bool Стан watch
GetWatchedComponents(id) std::optional<ComponentAddresses> Прочитати закріплені компоненти

ComponentsService

Метод Повертає Призначення
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime Структура компонента 21 ридер, по одному на тип компонента
EnumerateBuffs(addr) std::vector<Buff> Активні баффи на сутності
EnumerateActiveSkills(addr) std::vector<ActiveSkill> Скіли з компонента Actor
EnumerateStats(addr) std::vector<StatEntry> Стати з джерелами (предмети + баффи)
EnumerateItemMods(addr) std::vector<Mod> Моди, доступні з компонента Mods
EnumerateMonsterMods(ompAddr) std::vector<MonsterMod> Моди монстра з компонента ObjectMagicProperties (Components.OMP) — Id / Name / Metadata + Hash16 / Hash32; виявляйте при спавні, до накладання баффів
ReadGroundEffect(entityAddr) GroundEffect Тип наземного ефекту + радіус з сутності VisibleServerGroundEffect — передайте адресу СУТНОСТІ; зіставляйте за TypeId (ShockedGround/IgnitedGround/…). Розрізняє ефекти, що мають спільний шлях сутності
GetHealthPercent / GetEsPercent / GetManaPercent float Зручні відсоткові хелпери
IsAlive(addr) bool Health > 0
GetItemRarity(addr) int Рарність з компонента Mods
IsItemIdentified(addr) bool Прапорець ідентифіковано
GetStackCount(addr) int Поточний розмір стака
IsChestOpened(addr) bool Прапорець відкритої скрині
GetPlayerName(addr) std::string Ім'я гравця з компонента Player
GetWorldPosition(renderAddr, x, y, z) bool Зручний доступ до світових координат

InventoryService

Метод Повертає Призначення
Scan(inventoryId) Тригернути повторне сканування на стороні хоста (-1 = всі)
Get(inventoryId) Inventory Один інвентар, предмети вже заповнено
GetItems(inventoryId) std::vector<InventoryItem> Лише предмети
GetAll() std::vector<Inventory> Усі просканований інвентарі
GetName(inventoryId) const char* Відображуване ім'я ("Backpack", "Stash", …)
ReadItemRarity(addr) int Рарність окремої сутності (автоматичне розв'язання контейнерів WorldItem)
ReadItemStackCount(addr) int Стак окремої сутності (автоматичне розв'язання контейнерів WorldItem)
ReadItemBaseTypeName(addr) std::string Базовий тип, автоматичне розв'язання контейнерів WorldItem
ReadItemUniqueName(addr) std::string Унікальна назва, автоматичне розв'язання контейнерів WorldItem
ReadItemPath(addr) std::string Шлях Metadata/Items/..., автоматичне розв'язання контейнерів WorldItem
ReadItemMods(addr) ItemMods Підсумкові прапорці + 5 векторів модів за категоріями, автоматичне розв'язання контейнерів WorldItem
FormatStat(statKey, v0, v1) std::string Текст у стилі гри для ключа статистики + значення через форматувальник .csd хоста; порожній, поки описи не завантажено
ReadItemBaseStats(addr) ItemBaseStats Базові захисні значення (обчислений Energy Shield; базові Ward/Armour/Evasion); Valid false без компонента Armour; автоматично розв'язує контейнери WorldItem
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> Агреговані {statId, value} (Item Rarity вейстона 8205 / Pack Size 8206 / Monster Rarity 8207 / Monster Effectiveness 8208 / Waystone Drop Chance 8209); автоматично розв'язує контейнери WorldItem

UiService

Метод Повертає Призначення
Read(addr) UiElement Поля елемента (rect, прапорці, кількість дітей)
GetChildren(addr) std::vector<uintptr_t> Адреси дочірніх елементів
GetChildAt(addr, index) uintptr_t Одна дитина за індексом
FollowPath(root, indices, count) uintptr_t Пройти відомий шлях індексів
IsVisible(addr) bool Елемент на екрані
GetStringId(addr) std::string Стабільний ідентифікатор з боку гри
GetText(addr) std::string Відрендерений текст
ComputeScreenRect(addr, x, y, w, h) bool Підсумковий екранний прямокутник
GetGameUiRoot() uintptr_t Корінь in-game UI
GetUiRoot() uintptr_t Верхньорівневий корінь UI
GetCullValue() int Поріг відсікання UI у хоста
FindPanelByStringId(parent, stringId) uintptr_t Цільовий пошук нащадка

RenderService

Метод Повертає Призначення
WorldToScreen(wx, wy, wz, sx, sy) bool Перспективна проєкція
GridToLargeMap(gx, gy, worldZ, sx, sy) bool Проєкція на оверлей великої мапи
GridToMiniMap(gx, gy, worldZ, sx, sy) bool Проєкція на мінімапу
GetLargeMapTransform() MapTransform Попередньо перемножена трансформація для пакетної математики
GetMiniMapTransform() MapTransform Те саме, для мінімапи

TerrainService

Метод Повертає Призначення
GetWalkableGrid() WalkableGridHandle RAII-хендл до бітмапу прохідності 4 біти/тайл
GetHeightGrid() HeightGridHandle RAII-хендл до висот рельєфу по тайлах
IsWalkable(gx, gy) bool Предикат для одного тайла
GetTerrainHeight(gx, gy) float Z у світовому просторі
GetWorldToGridConvertor() float Коефіцієнт перетворення world → grid
EnumerateTgtLocations(cb) Відвідати кожен TGT-екземпляр у поточній локації

MemoryService

Метод Повертає Призначення
Read(addr, buf, size) bool Сирий RPM
ReadString(addr) std::string Narrow-рядок з нульовим завершенням
ReadWString(addr) std::wstring Wide-рядок з нульовим завершенням
ReadStdWString(addr) std::wstring Читає контейнер std::wstring з боку гри (обробляє SSO)
ReadStdVector(addr, elemSize, maxElems) std::vector<uint8_t> Сирі байти; інтерпретуйте як свій тип
GetBaseAddress() uintptr_t База ігрового модуля
GetModuleSize() uintptr_t Розмір ігрового модуля
GetPatternAddress(name) uintptr_t Пошук іменованого паттерну

LogService

Метод Призначення
Debug / Info / Warn / Error(msg) Емітувати на відповідному рівні
Log(level, msg) Кастомний рядок рівня

EventsService

Метод Повертає Призначення
Subscribe(kind, cb) Token Загальний диспетч
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token Однорядкові хелпери підписки
Unsubscribe(token) Ручне звільнення (деструктор все одно звільняє автоматично)

OverlayService

Метод Повертає Призначення
SetIncludeSleepingEntities(enable) Увімкнути отримання сутностей EntityState::Useless у EntitiesService.Enumerate
SetWantsOverlayInput(enable) Запросити в оверлея захоплення кліків миші замість режиму click-through

Обидва ідемпотентні, дія на рівні плагіна, OR-агрегація з хостом і іншими плагінами, автоматично скидаються при Disable/Unload. Дивіться розділ 13 для повного патерну (включаючи застереження щодо background-draw-list для плагінів вибору на мапі).

FlasksService

Метод Повертає Призначення
GetFlask(slot) std::optional<Flask> Колба life/mana за слотом пояса (0=life, 1=mana); nullopt за межами або не в грі
GetCharm(slot) std::optional<Charm> Чарм за слотом пояса (0..2); nullopt за межами або не в грі
AllFlasks() std::vector<Flask> Усі слоти колб включно з порожніми (записи FlaskSlotCount())
AllCharms() std::vector<Charm> Усі слоти чармів включно з порожніми (записи CharmSlotCount())
FlaskSlotCount() int32_t Кількість слотів колб (2 у POE2)
CharmSlotCount() int32_t Кількість слотів чармів (3 у POE2)

Дивіться розділ 8 ("Flasks & charms") для таблиць полів Flask / Charm та обмеження PerUseEffective.

PricesService

Метод Повертає Призначення
LookupPrice(name) PriceResult Нечіткий пошук ціни з боку хоста за відображуваним ім'ям (found, chaos, divine, exalt, category)
GetRates() PriceRates Курси конвертації Divine / Exalted → Chaos
GetStatus() PriceStatus Шлюз loaded + кількість за категоріями (catsOk / catsPending / catsFailed)

RuneshapeService

Метод Повертає Призначення
Runeshapes() std::vector<Runeshape> Усі вирішені пристрої Expedition2Encounter (id, колір, якір, bestIndex)
Rewards(entityId) std::vector<RuneshapeReward> Слоти нагород на пристрій, кожен оцінений через сервіс Prices

AtlasService

Метод Повертає Призначення
GetPanel() uintptr_t Адреса панелі атласу; 0 = відсутня
Nodes(detail) std::vector<AtlasNode> Усі вузли атласу (координати сітки, marker, mapState)
Connections() std::vector<AtlasConnection> Суміжність за якорями (залежить від в'юпорта)
Selection(which) std::vector<AtlasGridPoint> 0 = відкриті мапи лінії Rite, 1 = вибрані якорі
GetLineSeed() uint32_t Сід вибору нагород Rite (0 = немає)
Weights() std::vector<AtlasWeight> Сирі рядки ваг придатності (key, value)
GetNodeName(uiAddress) std::string Відображуване ім'я вузла ("", якщо не вирішено)

SekhemaService

Метод Повертає Призначення
GetPanel() uintptr_t Панель випробування, вирішена хостом; 0 = відсутня
ProbeFloor(uiAddress) int Кількість шарів FloorData (0 = не поверх випробування); дешевий передфільтр
GetFloor(panel) SekhemaFloor Заголовок поверху (layerCount, roomCounts, choices, counter)
Rooms(panel) std::vector<SekhemaRoom> Статичний граф кімнат у порядку (layer, index)
Content(panel) std::vector<SekhemaContentEntry> Рядки content-FK по кімнатах (rowId / rowName)
GetRoomUsedFlag(sm) int 1=використана, 0=активна, -1=не читається
GetStateMachineValue(sm, i, out) bool Одне значення shared-state (у порядку оголошення)
GetUiStringId(uiAddress) std::string StringId UI-елемента як UTF-8 (числове поле HUD)

24. Версіонування

PluginAbi.h визначає:

constexpr int PLUGIN_SDK_VERSION = 6;

Під час завантаження хост викликає plugin->GetSDKVersion() і порівнює зі своїм PLUGIN_SDK_VERSION. Розбіжність → хост логує попередження і відмовляється завантажувати плагін.

Хост також перевіряє HostAbi::version і HostAbi::size_bytes усередині PluginSDK_AttachHost (визначено inline у PluginSDK.h, коли задано PLUGIN_EXPORTS). Якщо якесь із цих полів не збігається з тим, на чому збирався плагін, ctx() непрацездатний. Аксесор базового класу HostCompatible() у цьому випадку повертає false, і будь-який плагін, що хоче поводитися ввічливо, має відмовитися від дій:

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

25. Приклади плагінів

Чотири плагіни в репозиторії розраховані на читання як документація:

  • Plugins/ExamplePlugin/ — оглядовий showcase з широкою поверхнею. Один плагін, що торкається майже кожного сервісу, організований як 11 sub-файлів examples/Example*.h (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) плюс підсумковий банер про покриття. Читайте його, коли хочете побачити, як сервіс використовується у контексті.

  • Plugins/Radar/ — сфокусований реальний приклад. ~200 рядків. Радар-оверлей, побудований виключно на публічному SDK — без оффсетів, без сирих читань пам'яті. Малює сітку прохідності плюс точки по сутностях через Render.GridToLargeMap. Читайте його, коли хочете побачити мінімальний код для конкретного результату.

  • Plugins/KillCount/ — трекер вбивств/скринь/смертей. SQLite + sprite-атлас + стан по локаціях. Показує, як упакувати персистентність, постачені дані-файли й оверлей в одну DLL.

  • Plugins/NinjaPricer/ — оверлей цін з poe.ninja. HTTP-запит (Exchange API) + сканування інвентаря + ціноутворення по предметах. Показує мережевий код, споживання сторонніх даних і ітерацію по інвентарю в реальному робочому процесі.


← Home

Clone this wiki locally