-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide UA
Плагіни POEFixer — це нативні C++ DLL, які завантажуються під час виконання з Plugins/<PluginName>/<PluginName>.dll. Вони читають поточний стан гри, малюють ImGui-оверлеї, зберігають власні налаштування й підписуються на події хоста.
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/.
Мінімальний плагін, що завантажується і пише повідомлення у лог хоста:
#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.
Як канонічний шаблон використайте 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));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, щоб упевнитися, що плагін і хост узгоджені. Розбіжність → плагіну відмовлено.
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.
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.Сутності експонують свої компоненти через 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 (включно зі 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() завжди повертає локального гравця.
Предмети, кинуті на землю, з'являються у 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.
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);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.
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елемента переїхав (0x448→0x4C0), і міст хоста було виправлено відповідно. (Для числового StringId, у який field-листки HUD випробування рендерять свої значення — це інше поле, ніжGetText()—SekhemaHelperчитаєctx()->Sekhema.GetUiStringId().)
Три проєкційні хелпери, дві системи координат.
Перспектива (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 для робочого радара, побудованого виключно на цих викликах.
Сітка прохідності — це бітмап з 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
});Підпишіться на події, які емітує хост. Кожен 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 для повного патерну.
ctx()->Overlay надає два «прапорці запиту» на плагін, що змінюють поведінку оверлея в цілому. Хост зберігає стан кожного плагіна за його вказівником this і об'єднує їх через OR із власними вбудованими прапорцями (видимість головного меню, блокування AutoCraft, вбудований хостовий засіб вибору сутностей з мапи, прапорці кожного іншого плагіна). Автоматично скидаються при вимкненні/вивантаженні плагіна — плагін, що впав або має помилку, не може назавжди залишити оверлей у некоректному стані.
За замовчуванням 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 снапшота за кадр під час увімкнення. Залишайте ВИМКНЕНИМ, якщо не потрібно.
Вікно оверлея зазвичай є наскрізним (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();Менші вікна, що охоплюють лише область вибору, працюють так само — хост не переймається розміром, лише тим, що якесь вікно знаходиться під курсором.
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 (сплячі сутності). Субкадрово, непомітно.
- Усі методи безпечно викликати з будь-якого потоку.
Хост завантажує ринкові ціни один раз за сесію з 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 лише після того, як надійшли курси конвертації і принаймні перша категорія. До того часу відображайте стан «завантаження цін…» замість нулів.
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.
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.
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) |
Конвенція: <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с) і при вимкненні; самостійно викликати його не потрібно.
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"), теж маршрутизується коректно — але зручні методи прозоріші.
Прямі примітиви читання пам'яті. Надавайте перевагу високорівневим сервісам скрізь, де можливо — вони знають оффсети, опрацьовують зміни 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Якщо ви часто тягнетеся до цих викликів, запитайте себе, чи не належать потрібні вам дані до високорівневих сервісів.
Кожен крос-DLL-виклик між хостом і плагіном виконується всередині блоку __try / __except на стороні хоста. Плагін, який поводиться неправильно — розіменовує застарілий вказівник, ділить на нуль або інакше падає всередині виклику SDK — отримує залогену помилку, а процес хоста не падає, гра продовжує працювати, і користувач може продовжувати користуватися іншими плагінами.
Це не означає, що плагіни можна писати неохайно. SEH ловить симптом, а не причину. Якщо ваш плагін падає кожного кадру — користувач бачить потік помилок у логах, і ваші дані фактично недоступні. Опрацьовуйте null-повернення з викликів SDK, перевіряйте прапорці Valid у даних компонентів і не розіменовуйте адреси uintptr_t напряму — пропускайте їх через виклики ComponentsService / Ui / Memory, які вже правильно обгортають RPM.
Хост може впоратись з багатим плагіном. Він не може впоратись із зависаючим DLL-плагіном — DrawSettings, що займає 100 мс, блокує весь UI-потік. Тримайте роботу на кадр дешевою.
Короткий перелік речей, з якими стикаються автори плагінів під час першої інтеграції. Більшість з них описано вище інлайн; зібрано тут як чек-лист.
-
OnAreaChangeспрацьовує до того, як сітка прохідності розібрана наново. Не оновлюйтеWalkableGridHandleз події — опитуйте щокадру вDrawUIі робіть swap, коли змінюєтьсяData(). (§11) -
Entity::ZoneзавждиNoneдля локального гравця. Це класифікація за відстанню від гравця, тож гравець за визначенням на нульовій відстані. Не показуйте це в інформаційних блоках про гравця. -
Components.ReadMods()повертає лише підсумкові прапорці — без списків модів. Для списків модів за категоріями викликайтеInventory.ReadItemMods(entityAddr). (§8) -
Предмети, кинуті на землю, можуть не мати
EntitySubtype. Якщо фільтруєте предмети у світі, надайте перевагуEntityType == Item || EntityType == Chestперед вужчою перевіркою підтипу. -
Directory()повертає абсолютний шлях. Не додавайте каталог EXE самостійно — отримаєтеEXEDIR\EXEDIR\Plugins\X, і записи конфіга опиняться поза папкою плагіна. -
ctx()повертаєconst Context*. Методи, що мутують стан, як-отEventsService::Subscribe, потребуютьconst_cast. Це навмисно — сервіси, які не мутують, мають бути неможливими для випадкової зміни. -
ImGui::SetCurrentContext— per-DLL. Викликайте його в кожній точці входу, де ви малюєте (OnEnable,DrawUI,DrawSettings), бо DLL плагіна за замовчуванням має власний стан ImGui. -
Зручні хелпери щоразу читають компонент заново.
GetHealthPercent(addr)внутрішньо робить свіжийReadLife(addr). Якщо у вас уже є структураLifeз попереднього виклику, звертайтеся до її полів напряму.
Однорядковий підсумок кожного публічного методу кожного сервісу. Повні сигнатури типів і нотатки про використання — у текстових секціях вище.
| Метод | Повертає | Призначення |
|---|---|---|
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 на старіших хостах / не в грі |
| Метод | Повертає | Призначення |
|---|---|---|
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> |
Прочитати закріплені компоненти |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
Зручний доступ до світових координат |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
Цільовий пошук нащадка |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
Те саме, для мінімапи |
| Метод | Повертає | Призначення |
|---|---|---|
GetWalkableGrid() |
WalkableGridHandle |
RAII-хендл до бітмапу прохідності 4 біти/тайл |
GetHeightGrid() |
HeightGridHandle |
RAII-хендл до висот рельєфу по тайлах |
IsWalkable(gx, gy) |
bool |
Предикат для одного тайла |
GetTerrainHeight(gx, gy) |
float |
Z у світовому просторі |
GetWorldToGridConvertor() |
float |
Коефіцієнт перетворення world → grid |
EnumerateTgtLocations(cb) |
— | Відвідати кожен TGT-екземпляр у поточній локації |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
Пошук іменованого паттерну |
| Метод | Призначення |
|---|---|
Debug / Info / Warn / Error(msg) |
Емітувати на відповідному рівні |
Log(level, msg) |
Кастомний рядок рівня |
| Метод | Повертає | Призначення |
|---|---|---|
Subscribe(kind, cb) |
Token |
Загальний диспетч |
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) |
Token |
Однорядкові хелпери підписки |
Unsubscribe(token) |
— | Ручне звільнення (деструктор все одно звільняє автоматично) |
| Метод | Повертає | Призначення |
|---|---|---|
SetIncludeSleepingEntities(enable) |
— | Увімкнути отримання сутностей EntityState::Useless у EntitiesService.Enumerate
|
SetWantsOverlayInput(enable) |
— | Запросити в оверлея захоплення кліків миші замість режиму click-through |
Обидва ідемпотентні, дія на рівні плагіна, OR-агрегація з хостом і іншими плагінами, автоматично скидаються при Disable/Unload. Дивіться розділ 13 для повного патерну (включаючи застереження щодо background-draw-list для плагінів вибору на мапі).
| Метод | Повертає | Призначення |
|---|---|---|
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.
| Метод | Повертає | Призначення |
|---|---|---|
LookupPrice(name) |
PriceResult |
Нечіткий пошук ціни з боку хоста за відображуваним ім'ям (found, chaos, divine, exalt, category) |
GetRates() |
PriceRates |
Курси конвертації Divine / Exalted → Chaos |
GetStatus() |
PriceStatus |
Шлюз loaded + кількість за категоріями (catsOk / catsPending / catsFailed) |
| Метод | Повертає | Призначення |
|---|---|---|
Runeshapes() |
std::vector<Runeshape> |
Усі вирішені пристрої Expedition2Encounter (id, колір, якір, bestIndex) |
Rewards(entityId) |
std::vector<RuneshapeReward> |
Слоти нагород на пристрій, кожен оцінений через сервіс Prices |
| Метод | Повертає | Призначення |
|---|---|---|
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 |
Відображуване ім'я вузла ("", якщо не вирішено) |
| Метод | Повертає | Призначення |
|---|---|---|
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) |
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;
}
// ...
}Чотири плагіни в репозиторії розраховані на читання як документація:
-
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) + сканування інвентаря + ціноутворення по предметах. Показує мережевий код, споживання сторонніх даних і ітерацію по інвентарю в реальному робочому процесі.