-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide RU
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - Сборка x64 Release
-
Библиотека времени выполнения:
/MD(Multi-threaded DLL) — должна совпадать с хостом
- Создайте новый проект C++ DLL в Visual Studio
- Укажите путь включения (include path) на директорию исходного кода POEFixer (для заголовков SDK и ImGui)
- Добавьте исходные файлы ImGui в ваш проект:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Подключите заголовки Plugin SDK в исходный код плагина:
Или используйте удобный заголовок из ExamplePlugin:
#include "plugin_sdk/PluginAPI.h" #include "plugin_sdk/PluginContext.h" #include "imgui/imgui.h"
#include "sdk/PluginHelpers.h" // Includes all SDK headers + MemoryReader + utilities
- Определите
PLUGIN_EXPORTSи_CRT_SECURE_NO_WARNINGSв определениях препроцессора вашего проекта
Plugins/
YourPlugin/
YourPlugin.dll <-- Имя DLL ДОЛЖНО совпадать с именем папки
config/
settings.txt <-- Необязательный файл настроек
data/
... <-- Необязательная директория данных (базы данных, кэши и т.д.)
ExamplePlugin демонстрирует рекомендуемую структуру проекта:
Plugins/ExamplePlugin/
ExamplePlugin.cpp <-- Главная точка входа плагина + экспорт фабричных функций
sdk/
PluginHelpers.h <-- MemoryReader, WideToNarrow, хелперы сущностей/редкости
examples/
ExampleBuffs.h <-- Список баффов с фильтрацией и полосами прогресса
ExampleEntities.h <-- Отладочный список сущностей с механизмом наблюдения, деревьями компонентов, JSON-дампом
ExampleInventory.h <-- ServerData, выбор инвентаря, сетка слотов, моды предметов с редкостью
ExampleMemory.h <-- Hex-просмотрщик, демо Read<T>, сканер паттернов
ExampleUiExplorer.h <-- Полный обозреватель UI-элементов с поиском, навигацией, подсветкой
Более полный пример плагина с SQLite3, атласом иконок и рендерингом оверлея:
Plugins/KillCount/
KillCount.cpp <-- Главная точка входа плагина, жизненный цикл IPlugin, UI настроек
KillCount.h <-- Объявление класса плагина
KillTracker.cpp/h <-- Движок подсчёта убийств/сундуков/смертей
OverlayRenderer.cpp/h <-- ImGui-оверлей с перетаскиванием для изменения позиции
IconAtlas.cpp/h <-- Загрузка текстур спрайт-листов (D3D11 + stb_image)
Database.cpp/h <-- Обёртка SQLite3 для хранения статистики
DisplaySettings.h <-- Структура настроек
sdk/
PluginHelpers.h <-- Скопирован из ExamplePlugin
lib/
sqlite3.c/h <-- SQLite3 amalgamation (компилируется как C)
sqlite3-vcpkg-config.h <-- Локальное переопределение для статической линковки
Имя файла DLL должно точно совпадать с именем папки:
- Папка:
Plugins/MyPlugin/→ DLL:MyPlugin.dll - Хост сканирует каждую подпапку в
Plugins/и ищет<FolderName>.dll
Load DLL (LoadLibrary)
→ CreatePlugin() -- Фабрика: создание экземпляра IPlugin
→ SetContext(ctx) -- Получение сервисов хоста
→ SetPluginDirectory(dir) -- Получение пути к папке плагина
→ GetSDKVersion() -- Проверка совместимости
→ GetName() -- Отображаемое имя для UI
→ [if enabled] OnEnable() -- Инициализация ресурсов
↓
Основной цикл (каждый кадр):
→ DrawUI() -- Рендеринг оверлея (только если включён)
→ DrawSettings() -- Рендеринг настроек во вкладке Plugins
→ WantsOverlay() -- Хост проверяет, нужен ли плагину режим оверлея
↓
Периодически / при завершении:
→ SaveSettings() -- Сохранение настроек
↓
→ OnDisable() -- Освобождение ресурсов
→ DestroyPlugin(plugin) -- Фабрика: удаление экземпляра IPlugin
→ FreeLibrary -- Выгрузка DLL
- Все методы
Draw*вызываются в основном/рендер-потоке -
GetSnapshot()и другие функции PluginContext являются потокобезопасными - НЕ создавайте потоки, вызывающие ImGui — ImGui не является потокобезопасным
Каждый плагин должен реализовать интерфейс IPlugin (определён в plugin_sdk/PluginAPI.h):
- Когда вызывается: Один раз, сразу после создания
-
Параметр: Относительный путь, например
"Plugins/YourPlugin" - Назначение: Сохраните этот путь для загрузки настроек/ресурсов
-
Когда вызывается: Один раз, после
SetPluginDirectory -
Параметр: Указатель на
PluginContextхоста (действителен в течение всего времени жизни плагина) - Назначение: Сохраните этот указатель — это ваш шлюз ко всем игровым данным
-
Важно: Вызовите
ImGui::SetCurrentContext(ctx->ImGuiContext)здесь
- Когда вызывается: Когда пользователь включает плагин или при запуске, если плагин был ранее включён
-
Параметр:
true, если игровой процесс в данный момент присоединён - Назначение: Загрузите настройки, выделите ресурсы, инициализируйте состояние
- Когда вызывается: Когда пользователь отключает плагин
- Назначение: Освободите ресурсы, остановите фоновую работу
- Когда вызывается: Каждый кадр, только когда плагин включён
- Назначение: Рендеринг оверлея с помощью ImGui
-
Примечание: Используйте уникальные идентификаторы окон, например
"MyWindow##MyPlugin", чтобы избежать конфликтов
- Когда вызывается: Каждый кадр, во вкладке настроек плагинов (только когда включён)
- Назначение: Рендеринг конфигурации плагина с помощью ImGui
- Когда вызывается: Периодически и при завершении работы приложения
-
Назначение: Сохраните настройки на диск (например,
Plugins/YourPlugin/config/settings.txt)
-
Возвращает: Отображаемое имя во вкладке Plugins (например,
"My Plugin")
-
Возвращает:
PLUGIN_SDK_VERSION(текущая версия: 5) - Назначение: Хост проверяет совместимость — значение должно совпадать
-
Возвращает:
true, если плагин хочет рендериться в режиме оверлея (прозрачный оверлей поверх игры) -
По умолчанию:
false— плагин рендерится только в обычном окне настроек -
Назначение: Когда любой плагин возвращает
true, хост переходит в режим оверлея, даже если встроенные функции этого не требуют
Ваша DLL должна экспортировать эти две функции на C:
extern "C" PLUGIN_API IPlugin* CreatePlugin() {
return new MyPlugin();
}
extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
delete plugin;
}Структура PluginContext (определена в plugin_sdk/PluginContext.h) предоставляет указатели на функции для доступа к игровым данным. Все типы находятся в пространстве имён PluginSDK.
Возвращает полный снимок состояния игры. Обновляется один раз за кадр. Содержит:
| Поле | Тип | Описание |
|---|---|---|
CurrentState |
GameStateTypes |
Текущее состояние игры |
CurrentAreaName |
string |
Название зоны (например, "The Riverways") |
CurrentAreaHash |
string |
Уникальный хеш экземпляра зоны |
CurrentAreaLevel |
uint8_t |
Уровень монстров текущей зоны |
IsTown |
bool |
True, если в городе |
IsHideout |
bool |
True, если в убежище |
IsPaused |
bool |
True, если игра на паузе |
IsSkillTreeVisible |
bool |
True, если открана панель дерева навыков |
WorldToGridConvertor |
float |
Коэффициент конвертации мир→сетка |
Player |
RadarEntity |
Данные сущности локального игрока |
Entities |
vector<RadarEntity> |
Все ближайшие сущности |
LargeMap / MiniMap
|
MapData |
Данные оверлея карты |
Vitals |
PlayerVitals |
HP/ES/MP игрока + баффы |
ScreenWidth / ScreenHeight
|
int |
Размеры окна игры |
ProcessId |
DWORD |
ID процесса игры |
GameWindow |
HWND |
Дескриптор окна игры |
GameWindowForeground |
bool |
True, если окно игры на переднем плане |
IsAttached |
bool |
True, если присоединён к процессу игры |
IsWindowValid |
bool |
True, если окно игры валидно |
LastUpdateTime |
uint64_t |
Временная метка последнего обновления данных |
AreaChangeCounter |
uint64_t |
Увеличивается при смене зоны |
Inventories |
vector<InventoryInfo> |
Содержимое инвентаря игрока |
CurrencyTotals |
map<string,int> |
Количество валюты по путям |
InventoryGrid |
InventoryGridInfo |
Информация о сетке инвентаря UI |
WorldToScreenMatrix |
XMFLOAT4X4 |
Матрица проекции 3D→2D |
Важно: Фильтрация сущностей Мёртвые сущности (с
EntityState == Useless) фильтруются из снимка до его передачи плагинам. Это означает, что вы никогда не увидите переход HP от живого к мёртвому. Если вам нужно отслеживать убийства, используйте обнаружение по исчезновению — отслеживайте ID сущностей по зонам и считайте их убитыми, когда они исчезают из списка сущностей, находясь в радиусеInnerCircleилиOuterCircle. Подробности см. в Разделе 8: Типовые рецепты.
Удобный ярлык для получения показателей здоровья игрока.
Возвращает перечисление текущего состояния игры.
True, если процесс игры присоединён и доступен для чтения.
True, если сейчас в игре (не загрузка, не экран входа).
True, если окно игры является активным (на переднем плане).
Возвращает ID процесса игры.
Считывает все модификаторы предмета.
Возвращает: 0=Обычный, 1=Магический, 2=Редкий, 3=Уникальный
Возвращает количество в стопке для валюты/стакируемых предметов.
Возвращает имя базового типа предмета.
Возвращает путь метаданных предмета.
Возвращает имя базового типа предмета (например, "Divine Orb", "Chaos Orb"). В отличие от ReadItemName, который возвращает путь метаданных, эта функция читает реальное имя базового типа из BaseItemTypeData.BaseTypeName.
Возвращает уникальное имя предмета из Words.dat (например, "Headhunter", "Brimstone Call"). Возвращает пустую строку для не-уникальных предметов.
Возвращает true, если хост в данный момент находится в режиме оверлея (прозрачный оверлей поверх окна игры). Используйте это для настройки рендеринга — например, рисование на игровом оверлее или в окне настроек.
Возвращает true, когда меню настроек хоста видимо (оверлей интерактивен). Когда меню скрыто, окно оверлея является прозрачным для кликов (WS_EX_TRANSPARENT), поэтому окна ImGui не могут получать ввод мыши.
Используйте это для реализации паттерна перетаскиваемого оверлея:
- Меню видимо: Показывайте ручку перетаскивания, позволяйте взаимодействие (вкладки, кнопки)
-
Меню скрыто: Убирайте ручку перетаскивания, добавляйте
ImGuiWindowFlags_NoInputsдля неинтерактивного окна
Полную реализацию см. в Разделе 6: Паттерн перетаскиваемого оверлея.
Прямой доступ к памяти игрового процесса. Все чтения безопасны (возвращают 0/пустое значение при ошибке).
Возвращает базовый адрес модуля игрового исполняемого файла. Возвращает 0, если не присоединён.
Возвращает размер игрового модуля в байтах. Возвращает 0, если не присоединён.
Считывает блок необработанных байтов из игрового процесса. Буфер buffer должен иметь как минимум size выделенных байтов. Возвращает true при успехе.
// Example: Read a 4-byte integer from game memory
uint32_t value = 0;
m_Context->ReadProcessMemory(address, &value, sizeof(value));
// Example: Read a struct
MyStruct data{};
m_Context->ReadProcessMemory(structAddress, &data, sizeof(data));Считывает строку ASCII с нулевым завершением из памяти игры (максимум 128 символов).
Считывает строку Unicode (wide) с нулевым завершением из памяти игры (максимум 128 wchar).
Получает разрешённый адрес по имени паттерна. Возвращает 0, если не найден.
Стандартные паттерны:
| Имя | Описание |
|---|---|
"Game States" |
Корень вектора GameStates |
"File Root" |
Реестр файлов |
"AreaChangeCounter" |
Счётчик переходов между зонами |
"Terrain Rotator Helper" |
Данные вращения |
"Terrain Rotation Selector" |
Селектор вращения |
"GameCullSize" |
Значение отсечения экрана |
Конвертирует позицию в мировом пространстве в экранные координаты. Возвращает true, если позиция видима на экране.
float screenX, screenY;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &screenX, &screenY)) {
ImGui::GetBackgroundDrawList()->AddText(ImVec2(screenX, screenY), IM_COL32_WHITE, "Label");
}Запрашивает у хоста сканирование инвентарей. Передайте -1 для сканирования всех инвентарей или конкретный ID инвентаря. Данные инвентаря в снимке заполняются после завершения сканирования (следующий кадр).
Примечание: Данные инвентаря не обновляются автоматически — вы должны вызывать эту функцию для запуска сканирования. Вызывайте её периодически (например, каждые 2 секунды), если вам нужны постоянно обновляемые данные инвентаря.
Возвращает указатель на данные сетки проходимости. Сетка представляет собой 2D-массив, где 0 = непроходимо, ненулевое значение = проходимо. Возвращает nullptr, если данные недоступны.
Возвращает высоту рельефа в позиции сетки. Возвращает 0, если позиция за пределами или данные недоступны.
Эти функции читают контейнеры стандартной библиотеки C++ непосредственно из памяти игры, повторяя методы Core::Process хоста.
Считывает StdVector (24-байтная структура: {First, Last, End}) из памяти игры. Возвращает буфер элементов, выделенный через malloc. Вызывающий должен освободить возвращённый указатель через free(). Возвращает nullptr при ошибке.
// Example: Read a vector of uint32_t
int count = 0;
void* data = m_Context->ReadStdVector(vectorAddr, sizeof(uint32_t), &count);
if (data && count > 0) {
uint32_t* values = static_cast<uint32_t*>(data);
for (int i = 0; i < count; i++) { /* values[i] */ }
free(data);
}Считывает StdList (16-байтная структура: {Head, Size}) из памяти игры. Обходит связный список и возвращает непрерывный буфер. Вызывающий должен вызвать free().
Считывает StdBucket из памяти игры (читает встроенный StdVector). Вызывающий должен вызвать free().
Обходит StdMap (16-байтная структура: {Head, Size}) и вызывает callback для каждой пары ключ-значение. Возвращает количество посещённых узлов.
// Example: Read a map<uint32_t, float>
struct MapResult { std::vector<std::pair<uint32_t, float>> entries; };
MapResult result;
m_Context->ReadStdMap(mapAddr, sizeof(uint32_t), sizeof(float),
[](const void* key, const void* value, void* userData) {
auto* r = static_cast<MapResult*>(userData);
uint32_t k; float v;
memcpy(&k, key, sizeof(k));
memcpy(&v, value, sizeof(v));
r->entries.push_back({k, v});
}, &result);Считывает StdWString (32-байтная структура с встроенным/динамическим буфером) из памяти игры.
Возвращает удобочитаемое имя для ID инвентаря (например, 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1").
SDK v4 предоставляет прямой доступ к отладочным данным хоста — компоненты сущностей, подробности инвентаря и дерево UI-элементов — соответствующие встроенным вкладкам Debug.
Возвращает список всех сущностей с отладочными метаданными (Id, Address, Path, Type, SubType, State, Rarity, Zone). Соответствует вкладке Debug→Entity List.
Начинает наблюдение за компонентами сущности. Рабочий поток хоста будет считывать полные данные компонентов этой сущности каждый кадр.
Прекращает наблюдение за компонентами сущности. Вызовите это, когда пользователь сворачивает узел дерева сущности, чтобы освободить ресурсы.
Возвращает полные данные компонентов для наблюдаемой сущности. Содержит подструктуры для всех 8 распознанных компонентов (Life, Render, Positioned, Targetable, Animated, Stats, Actor, Buffs) плюс список всех адресов компонентов.
// Example: Watch entity on expand, read components
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
if (ImGui::TreeNode(e.Path.c_str())) {
m_Context->WatchEntity(e.Id);
auto data = m_Context->GetWatchedEntityData(e.Id);
if (data.HasLife) {
ImGui::Text("HP: %d / %d ES: %d / %d",
data.Life.Health.Current, data.Life.Health.Total,
data.Life.EnergyShield.Current, data.Life.EnergyShield.Total);
}
ImGui::TreePop();
} else {
m_Context->UnwatchEntity(e.Id);
}
}Возвращает базовый адрес компонента ServerData.
Возвращает все ID и адреса инвентарей игрока (из ServerData).
Начинает наблюдение за инвентарём для детальной отладочной инспекции. Хост считывает занятость слотов, подробности предметов и модификаторы.
Возвращает полные данные для текущего наблюдаемого инвентаря: размеры сетки, занятость слотов, предметы с редкостью и модификаторами.
// Example: Inventory inspector
auto invList = m_Context->GetPlayerInventoryList();
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
for (auto& item : inv.Items) {
ImGui::Text("[%s] Rarity=%d Mods=%d", item.Path.c_str(), item.Rarity,
(int)(item.ImplicitMods.size() + item.ExplicitMods.size()));
}Возвращает адрес корневого элемента игрового UI (для навигации по дереву UI внутри игры).
Возвращает адрес корневого элемента UI верхнего уровня.
Возвращает текущее значение GameCullSize, используемое для расчёта масштаба UI. В сочетании с размерами экрана это позволяет точно вычислять позицию/размер элементов UI.
// Example: UI scale calculation (matching host logic)
int cullValue = m_Context->GetGameCullValue();
auto snapshot = m_Context->GetSnapshot();
// Scale for index 1 (width): screenWidth / (cullValue / baseWidth)
// Scale for index 2 (height): screenHeight / (cullValue / baseHeight)SDK v5 добавляет прямое чтение UI-элементов без механизма отладочного наблюдения. Навигация по дереву UI, проверка видимости, чтение текста и вычисление экранных прямоугольников.
Чтение основных свойств UI-элемента (позиция, масштаб, флаги видимости, количество дочерних, адрес родителя).
Возвращает адреса всех дочерних элементов.
Возвращает адрес дочернего элемента по индексу. Возвращает 0, если индекс вне диапазона.
Навигация по пути дочерних индексов от корневого элемента. Например, {5, 1, 2} обходит root -> child[5] -> child[1] -> child[2]. Возвращает 0, если любой шаг не удался.
Возвращает true, если элемент и все его предки видимы.
Возвращает строковый идентификатор элемента (например, "Stash", "Log In").
Вычисляет экранный прямоугольник элемента с полным рекурсивным масштабированием. Возвращает false, если адрес элемента невалиден.
Возвращает текстовое содержимое элемента.
SDK v5 предоставляет 21 типизированную функцию чтения компонентов. Каждая принимает адрес компонента (из EntityComponentCache) и возвращает структуру данных с флагом Valid.
| Функция | Возвращаемая структура | Компонент |
|---|---|---|
ReadLifeComponent(addr) |
PluginLifeData |
Life (здоровье/ES/мана) |
ReadRenderComponent(addr) |
PluginRenderData |
Render (мировая позиция, границы модели) |
ReadPositionedComponent(addr) |
PluginPositionedData |
Positioned (реакция, флаг дружественности) |
ReadTargetableComponent(addr) |
PluginTargetableData |
Targetable (флаги доступности для выделения) |
ReadChestComponent(addr) |
PluginChestData |
Chest (состояние открытия, качество) |
ReadShrineComponent(addr) |
PluginShrineData |
Shrine (флаг доступности) |
ReadStackComponent(addr) |
PluginStackData |
Stack (текущий и максимальный размер) |
ReadChargesComponent(addr) |
PluginChargesData |
Charges (текущие, максимальные, за использование) |
ReadPlayerComponent(addr) |
PluginPlayerData |
Player (имя, опыт, уровень, класс) |
ReadAnimatedComponent(addr) |
PluginAnimatedData |
Animated (путь анимации, ID) |
ReadTransitionableComponent(addr) |
PluginTransitionableData |
Transitionable (флаг) |
ReadTriggerableBlockageComponent(addr) |
PluginTriggerableBlockageData |
TriggerableBlockage (заблокирован, мин/макс) |
ReadMinimapIconComponent(addr) |
PluginMinimapIconData |
MinimapIcon (флаг иконки) |
ReadStateMachineComponent(addr) |
PluginStateMachineData |
StateMachine (текущие значения состояний) |
ReadBaseComponent(addr) |
PluginBaseData |
Base (размер ячейки предмета, флаги влияния) |
ReadModsComponent(addr) |
PluginModsData |
Mods (редкость, опознан, списки модов) |
ReadStatsComponent(addr) |
PluginStatsData |
Stats (пары ключ-значение характеристик) |
ReadBuffsComponent(addr) |
PluginBuffsData |
Buffs (список активных баффов) |
ReadActorComponent(addr) |
PluginActorData |
Actor (анимация, навыки, счётчики деплоя) |
ReadNpcComponent(addr) |
PluginNpcData |
NPC (флаг скрытости, путь иконки) |
ReadDiesAfterTimeComponent(addr) |
PluginDiesAfterTimeData |
DiesAfterTime (общее время) |
Встроенные вспомогательные методы PluginContext, оборачивающие функции чтения компонентов для типичных операций:
| Хелпер | Сигнатура | Описание |
|---|---|---|
GetHealthPercent |
float (uintptr_t lifeAddr) |
Процент HP (0-100) |
GetEsPercent |
float (uintptr_t lifeAddr) |
Процент энергетического щита (0-100) |
GetManaPercent |
float (uintptr_t lifeAddr) |
Процент маны (0-100) |
IsAlive |
bool (uintptr_t lifeAddr) |
True, если HP > 0 |
IsChestOpenedHelper |
bool (uintptr_t chestAddr) |
True, если сундук открыт |
GetWorldPosition |
bool (uintptr_t renderAddr, float* x, float* y, float* z) |
Извлечение мировой позиции из Render |
GetItemRarityFromMods |
int (uintptr_t modsAddr) |
Редкость предмета из компонента Mods |
IsItemIdentifiedHelper |
bool (uintptr_t modsAddr) |
True, если предмет опознан |
GetStackCountHelper |
int (uintptr_t stackAddr) |
Текущий размер стака |
GetPlayerNameHelper |
string (uintptr_t playerAddr) |
Имя персонажа игрока |
// Чтение здоровья сущности через чтение компонентов
auto snapshot = m_Context->GetSnapshot();
for (auto& entity : snapshot->Entities) {
if (entity.ComponentCache.HasLife()) {
auto life = m_Context->ReadLifeComponent(entity.ComponentCache.LifeAddr);
if (life.Valid) {
float hpPct = m_Context->GetHealthPercent(entity.ComponentCache.LifeAddr);
}
}
}
// Навигация по дереву UI
uintptr_t gameUi = m_Context->GetGameUiRootAddress();
const int path[] = {5, 1, 2};
uintptr_t btn = m_Context->ReadUiChildChain(gameUi, path, 3);
if (m_Context->IsUiElementVisible(btn)) {
float x, y, w, h;
m_Context->ComputeUiScreenRect(btn, &x, &y, &w, &h);
}Заголовок sdk/PluginHelpers.h (включён в ExamplePlugin) предоставляет типобезопасный класс MemoryReader, оборачивающий сырые функции PluginContext:
PluginSDK::MemoryReader mem(m_Context);
// Read a single struct
auto data = mem.Read<MyStruct>(address);
// Read an array
auto arr = mem.ReadArray<uint32_t>(address, count);
// Read native containers — returns std::vector<T>
auto vec = mem.ReadStdVector<uint32_t>(vectorAddr);
auto list = mem.ReadStdList<MyNode>(listAddr);
auto bucket = mem.ReadStdBucket<MyEntry>(bucketAddr);
auto map = mem.ReadStdMap<uint32_t, float>(mapAddr);
auto wstr = mem.ReadStdWString(wstringAddr);MemoryReader также предоставляет удобные обёртки для ReadString(), ReadUnicodeString(), GetBaseAddress(), GetModuleSize() и GetPatternAddress().
Дополнительные утилиты в PluginHelpers.h:
-
WideToNarrow(wstring)— безопасная конвертация wstring→string (с потерей не-ASCII) -
GetEntityTypeName(type)— перечисление в отображаемое имя (включая ExpeditionMarker/ExpeditionRemnant) -
GetNearbyZoneName(zone)— зона в отображаемое имя -
GetRarityName(rarity)/GetRarityColor(rarity)— хелперы отображения редкости
Запись в систему логирования хоста. Уровни: "Debug", "Info", "Warning", "Error"
Контекст ImGui хоста. Вызовите ImGui::SetCurrentContext() с этим значением в SetContext().
ID3D11Device* хоста. Приведите тип и используйте для загрузки текстур.
Все типы находятся в пространстве имён PluginSDK. Плагины обычно добавляют using namespace PluginSDK;.
Данные по каждой сущности, доступные в snapshot->Entities:
| Поле | Тип | Описание |
|---|---|---|
Id |
uint32_t |
Уникальный ID сущности |
Address |
uintptr_t |
Адрес в памяти (для вызовов API предметов) |
EntityDetailsAddress |
uintptr_t |
Адрес структуры деталей сущности |
RenderComponentAddress |
uintptr_t |
Адрес компонента Render (ярлык) |
IsValid |
bool |
Флаг валидности сущности |
entityType |
EntityTypes |
Категория сущности |
entitySubtype |
EntitySubtypes |
Подкатегория сущности |
entityState |
EntityStates |
Состояние сущности |
Rarity |
int |
0=Обычный, 1=Магический, 2=Редкий, 3=Уникальный |
Reaction |
uint8_t |
0=Враждебный, 1=Нейтральный, 2=Дружественный |
GridPositionX/Y |
float |
Позиция на сетке рельефа |
TerrainHeight |
float |
Высота рельефа в позиции сущности |
WorldX/Y/Z |
float |
Позиция в мировом пространстве |
ModelBoundsZ |
float |
Высота модели |
Path |
wstring |
Путь метаданных сущности |
PlayerName |
wstring |
Имя игрока (если это сущность-игрок) |
TgtPath |
string |
Путь цели (узкая строка) |
CurrentHP/MaxHP |
int |
Здоровье сущности |
CurrentES/MaxES |
int |
Энергетический щит сущности |
IsSleeping |
bool |
Флаг далёкой сущности |
IsChestOpened |
bool |
Состояние открытия сундука |
Zone |
NearbyZone |
Близость к игроку |
ComponentCache |
EntityComponentCache |
Адреса компонентов |
Важно: Мёртвые сущности (
EntityState::Useless) удаляются из снимка до его передачи плагинам. Вы никогда не увидите, как HP монстра падает до нуля — он просто исчезает из списка. Используйте полеZone, чтобы отличить убийства (сущность исчезла из InnerCircle/OuterCircle) от выхода сущностей за пределы дальности (сущность была в зоне Far).
Активный бафф/дебафф:
| Поле | Тип | Описание |
|---|---|---|
Name |
string |
Внутреннее имя баффа (например, "flask_effect_life") |
TimeLeft |
float |
Оставшееся время в секундах |
Charges |
short |
Количество стаков |
TotalTime |
float |
Общая длительность |
Состояние миникарты/большой карты:
| Поле | Тип | Описание |
|---|---|---|
CenterX/Y |
float |
Центр карты |
SizeX/Y |
float |
Размеры карты |
ShiftX/Y |
float |
Текущее смещение панорамирования |
DefaultShiftX/Y |
float |
Значения смещения по умолчанию |
Zoom |
float |
Уровень масштабирования |
Scale |
float |
Коэффициент масштаба карты |
IsVisible |
bool |
Карта в данный момент отображается |
| Поле | Тип | Описание |
|---|---|---|
Id |
int |
ID инвентаря |
TotalBoxesX/Y |
int |
Размеры сетки |
Ptr |
uintptr_t |
Адрес инвентаря в памяти |
Items |
vector<InventoryItemInfo> |
Предметы в инвентаре |
Поля предмета: Address, Name (путь метаданных), Path (то же, что Name), BaseTypeName (имя базового типа, например "Divine Orb"), UniqueName (уникальное имя предмета из Words.dat, например "Headhunter", пустое для не-уникальных), SlotX/Y, Width/Height, StackCount, IsCurrency
Возвращается ReadExtendedItemMods():
| Поле | Тип | Описание |
|---|---|---|
ImplicitMods |
vector<ItemModData> |
Неявные модификаторы |
ExplicitMods |
vector<ItemModData> |
Явные модификаторы |
EnchantMods |
vector<ItemModData> |
Модификаторы зачарования |
HellscapeMods |
vector<ItemModData> |
Модификаторы Hellscape |
CrucibleMods |
vector<ItemModData> |
Модификаторы горнила |
Rarity |
int |
0=Обычный, 1=Магический, 2=Редкий, 3=Уникальный |
| Поле | Тип | Описание |
|---|---|---|
Key |
string |
Ключ характеристики модификатора |
Values |
vector<float> |
Значения модификатора |
Адреса компонентов, кэшированные для каждой сущности (доступны через RadarEntity.ComponentCache):
| Поле | Тип | Has-метод |
|---|---|---|
RenderAddr |
uintptr_t |
HasRender() |
PositionedAddr |
uintptr_t |
HasPositioned() |
ChestAddr |
uintptr_t |
HasChest() |
PlayerAddr |
uintptr_t |
HasPlayer() |
ShrineAddr |
uintptr_t |
HasShrine() |
LifeAddr |
uintptr_t |
HasLife() |
TargetableAddr |
uintptr_t |
HasTargetable() |
OMPAddr |
uintptr_t |
HasOMP() |
NPCAddr |
uintptr_t |
HasNPC() |
TriggerableBlockageAddr |
uintptr_t |
HasTriggerableBlockage() |
DiesAfterTimeAddr |
uintptr_t |
HasDiesAfterTime() |
BuffsAddr |
uintptr_t |
HasBuffs() |
WorldItemAddr |
uintptr_t |
HasWorldItem() |
AreaTransitionAddr |
uintptr_t |
HasAreaTransition() |
MinimapIconAddr |
uintptr_t |
HasMinimapIcon() |
StatsAddr |
uintptr_t |
HasStats() |
ActorAddr |
uintptr_t |
HasActor() |
AnimatedAddr |
uintptr_t |
HasAnimated() |
BaseAddr |
uintptr_t |
HasBase() |
ChargesAddr |
uintptr_t |
HasCharges() |
ModsAddr |
uintptr_t |
HasMods() |
StackAddr |
uintptr_t |
HasStack() |
TransitionableAddr |
uintptr_t |
HasTransitionable() |
StateMachineAddr |
uintptr_t |
HasStateMachine() |
Метаданные сущности из GetEntityDebugList():
| Поле | Тип | Описание |
|---|---|---|
Id |
uint32_t |
ID сущности |
Address |
uintptr_t |
Адрес в памяти |
Path |
string |
Путь метаданных |
EntityType |
int |
Тип сущности (приведите к EntityTypes) |
EntitySubType |
int |
Подтип сущности (приведите к EntitySubtypes) |
EntityState |
int |
Состояние сущности (приведите к EntityStates) |
Rarity |
int |
0=Обычный, 1=Магический, 2=Редкий, 3=Уникальный |
Zone |
NearbyZone |
Близость к игроку |
ComponentAddresses |
vector<pair<string,uintptr_t>> |
Все пары имя компонента→адрес |
Полные данные компонентов из GetWatchedEntityData():
| Поле | Тип | Описание |
|---|---|---|
EntityId |
uint32_t |
Для какой сущности эти данные |
Valid |
bool |
Были ли данные успешно считаны |
HasLife / Life
|
bool / DebugLifeComp
|
Компонент Life — Life.Health, Life.EnergyShield, Life.Mana (каждый — DebugVital с .Current, .Total, .Regeneration, .ReservedFlat, .ReservedPercent) |
HasRender / Render
|
bool / DebugRenderComp
|
Позиция (WorldX/Y/Z, GridX/Y), TerrainHeight, ModelBounds (X/Y/Z) |
HasPositioned / Positioned
|
bool / DebugPositionedComp
|
Значение Reaction, флаг IsFriendly |
HasTargetable / Targetable
|
bool / DebugTargetableComp
|
IsTargetable, IsHighlightable, IsTargettedByPlayer, HiddenFromPlayer, MeetsQuestState, MeetsItemRequirements |
HasAnimated / Animated
|
bool / DebugAnimatedComp
|
Путь анимации (string), Id (uint32) |
HasStats / Stats
|
bool / DebugStatsComp
|
CurrentWeaponIndex, IsShapeshifted, StatsItems/StatsBuff (векторы пар ID характеристики→значение) |
HasActor / Actor
|
bool / DebugActorComp
|
AnimationId, AnimationName, ActiveSkills (вектор DebugActiveSkill), DeployedCounts[256] |
HasBuffs / Buffs
|
bool / vector<DebugBuff>
|
Активные баффы: Name, TotalTime, TimeLeft, Charges, FlaskSlot, Effectiveness, SourceEntityId |
Подробности инвентаря из GetWatchedInventoryData():
| Поле | Тип | Описание |
|---|---|---|
InventoryId |
int |
ID инвентаря (-1, если нет) |
Address |
uintptr_t |
Адрес инвентаря |
TotalBoxesX/Y |
int |
Размеры сетки |
ServerRequestCounter |
int |
Счётчик синхронизации с сервером |
GridScreenX / GridScreenY
|
float |
Позиция сетки UI на экране |
CellSize |
float |
Размер ячейки сетки в пикселях |
GridValid |
bool |
Валидны ли данные сетки UI |
SlotOccupied |
vector<bool> |
Занятость каждого слота |
Items |
vector<DebugInventoryItem> |
Предметы с путём, редкостью и модификаторами |
| Поле | Тип | Описание |
|---|---|---|
Address |
uintptr_t |
Адрес сущности предмета |
Path |
string |
Путь метаданных предмета |
BaseTypeName |
string |
Имя базового типа (например "Divine Orb") |
UniqueName |
string |
Уникальное имя предмета из Words.dat (пустое для не-уникальных) |
SlotX / SlotY
|
int |
Позиция в сетке инвентаря |
Rarity |
int |
0=Обычный, 1=Магический, 2=Редкий, 3=Уникальный |
ItemLevel |
int |
Уровень предмета |
RequiredLevel |
int |
Требуемый уровень персонажа |
IsIdentified |
bool |
Идентифицирован ли предмет |
IsCorrupted |
bool |
Осквернён ли предмет |
CraftedModCount |
int |
Количество крафтовых модификаторов |
ImplicitMods |
vector<DebugModInfo> |
Неявные модификаторы |
ExplicitMods |
vector<DebugModInfo> |
Явные модификаторы |
EnchantMods |
vector<DebugModInfo> |
Модификаторы зачарования |
HellscapeMods |
vector<DebugModInfo> |
Модификаторы Hellscape |
| Поле | Тип | Описание |
|---|---|---|
Name |
string |
Имя навыка |
UseStage |
int |
Текущая стадия использования |
CastType |
int |
Тип применения |
TotalUses |
int |
Общее количество использований |
TotalCooldownTimeInMs |
int |
Время перезарядки в миллисекундах |
CanBeUsed |
bool |
Может ли навык быть использован сейчас |
| Поле | Тип | Описание |
|---|---|---|
Name |
string |
Внутреннее имя баффа |
TotalTime |
float |
Общая длительность |
TimeLeft |
float |
Оставшееся время в секундах |
Charges |
short |
Количество стаков |
FlaskSlot |
short |
Индекс слота зелья |
Effectiveness |
short |
Эффективность баффа |
SourceEntityId |
uint32_t |
Сущность, наложившая этот бафф |
| Поле | Тип | Описание |
|---|---|---|
Name |
string |
Отображаемое имя модификатора |
StatKey |
string |
Идентификатор ключа характеристики |
AffixName |
string |
Имя аффикса |
GenerationType |
int |
1=Префикс, 2=Суффикс, 3=Неявный |
Value0 |
float |
Первое значение (NaN, если отсутствует) |
Value1 |
float |
Второе значение (NaN, если отсутствует) |
Свойства UI-элемента из ReadUiElement():
| Поле | Тип | Описание |
|---|---|---|
Valid |
bool |
Успешно ли выполнено чтение |
X / Y
|
float |
Позиция элемента |
Width / Height
|
float |
Размер элемента |
ScaleX / ScaleY
|
float |
Коэффициенты масштабирования |
IsVisible |
bool |
Флаг видимости |
IsEnabled |
bool |
Флаг активности |
ChildCount |
int |
Количество дочерних элементов |
ParentAddr |
uintptr_t |
Адрес родительского элемента |
SelfAddr |
uintptr_t |
Указатель на себя |
Все функции чтения компонентов возвращают структуры с полем Valid. Основные структуры:
PluginLifeData — Health, EnergyShield, Mana (каждое — PluginVitalData с Current, Total, Regeneration, ReservedFlat, ReservedPercent)
PluginRenderData — WorldX/Y/Z, GridX/Y, TerrainHeight, ModelBoundsX/Y/Z
PluginPositionedData — Reaction, IsFriendly, GridX/Y
PluginTargetableData — IsTargetable, IsHighlightable, IsTargettedByPlayer, HiddenFromPlayer, MeetsQuestState, MeetsItemRequirements
PluginChestData — IsOpened, IsLocked, Quality
PluginShrineData — IsAvailable
PluginStackData — CurrentSize, MaxSize
PluginChargesData — CurrentCharges, MaxCharges, ChargesPerUse
PluginPlayerData — Name, XP, Level, ClassId, ClassNameHash
PluginAnimatedData — AnimationPath, AnimationId, IsAnimating
PluginTransitionableData — Flag
PluginTriggerableBlockageData — IsBlocked, MinValue, MaxValue
PluginMinimapIconData — IsIcon
PluginStateMachineData — CurrentState1, CurrentState2, CurrentState3
PluginBaseData — CellSizeX, CellSizeY, IsElder, IsShaper, IsHunter, IsCrusader, IsRedeemer, IsWarlord
PluginModsData — Rarity, IsIdentified, ItemLevel, RequiredLevel, IsMirrored, IsSplit, IsCorrupted, ImplicitMods, ExplicitMods, EnchantMods, HellscapeMods, CrucibleMods (каждое — vector<PluginModData> с Key и Values)
PluginStatsData — Stats (vector<pair<int,int>> из ID характеристики в значение)
PluginBuffsData — Buffs (vector<PluginBuffData> с Name, TotalTime, TimeLeft, Charges, FlaskSlot, Effectiveness, SourceEntityId)
PluginActorData — AnimationId, AnimationName, ActiveSkills (vector<PluginActiveSkillData>), DeployedCounts[256]
PluginNpcData — IsHidden, IconPath
PluginDiesAfterTimeData — TotalTime
EntityTypes: Unidentified(0), Chest(1), NPC(2), Player(3), Shrine(4), Monster(5), DeliriumBomb(6), DeliriumSpawner(7), OtherImportantObjects(8), Item(9), Renderable(10), AreaTransition(11), ExpeditionMarker(12), ExpeditionRemnant(13)
EntitySubtypes: _Unidentified(0), _None(1), PlayerSelf(2), PlayerOther(3), ChestWithMagicRarity(4), ChestWithRareRarity(5), ExpeditionChest(6), BreachChest(7), Strongbox(8), SpecialNPC(9), POIMonster(10), PinnacleBoss(11), WorldItem(12), InventoryItem(13)
EntityStates: None(0), Useless(1), PlayerLeader(2), MonsterFriendly(3), PinnacleBossHidden(4)
NearbyZone: None(0), InnerCircle(1, ~60 единиц сетки), OuterCircle(2, ~120 единиц сетки), Far(3)
GameStateTypes: AreaLoadingState(0), ChangePasswordState(1), CreditsState(2), EscapeState(3), InGameState(4), PreGameState(5), LoginState(6), WaitingState(7), CreateCharacterState(8), SelectCharacterState(9), DeleteCharacterState(10), LoadingState(11), GameNotLoaded(12)
Хост и плагин используют один и тот же контекст ImGui. Вы должны вызвать:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));в вашем методе SetContext().
Всегда используйте уникальные ID окон, чтобы избежать конфликтов с хостом или другими плагинами:
ImGui::Begin("My Window##MyPluginName", &showWindow);- Окна, вкладки, деревья, таблицы, списки рисования
- Загрузка текстур через устройство D3D11
- Рендеринг оверлея через
ImGui::GetBackgroundDrawList() - Иконки FontAwesome 6 через
#include "imgui/IconsFontAwesome6.h"(например,ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)
Используйте WorldToScreen() для рисования надписей/фигур в позициях сущностей:
float sx, sy;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &sx, &sy)) {
auto* drawList = ImGui::GetBackgroundDrawList();
drawList->AddText(ImVec2(sx, sy - 20), IM_COL32(255, 255, 0, 255), "Monster");
drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, IM_COL32(255, 0, 0, 255));
}Переопределите WantsOverlay(), чтобы вернуть true для запроса хосту войти в режим оверлея:
bool WantsOverlay() override { return m_OverlayEnabled; }В режиме оверлея окно хоста прозрачно и расположено поверх игры. Ваши вызовы DrawUI() рендерятся непосредственно на экране игры.
Оверлей хоста использует WS_EX_TRANSPARENT для прозрачности окна для кликов, когда меню скрыто. Это означает, что окна ImGui не могут получать ввод мыши, если меню не видимо. Для создания перетаскиваемого окна оверлея (как встроенный Vitals Overlay) используйте этот двухрежимный паттерн:
#include "imgui/IconsFontAwesome6.h"
void MyPlugin::RenderOverlay() {
bool menuVisible = m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false;
if (menuVisible) {
// === DRAGGABLE MODE ===
// Window with background, drag hint, interactive controls
ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY), ImGuiCond_Appearing);
ImGui::SetNextWindowBgAlpha(m_Alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing;
ImGui::Begin("##MyOverlay", nullptr, flags);
// Drag hint (the entire window is draggable since there's no title bar)
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT " Drag to reposition");
ImGui::Spacing();
// ... your content here (tabs, text, icons, buttons — all interactive) ...
// Persist position when user drags the window
ImVec2 pos = ImGui::GetWindowPos();
if (pos.x != m_PosX || pos.y != m_PosY) {
m_PosX = pos.x;
m_PosY = pos.y;
// Save to settings on next SaveSettings() call
}
ImGui::End();
}
else {
// === NON-INTERACTIVE MODE ===
// Static overlay — no drag, no mouse interaction
ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY));
ImGui::SetNextWindowBgAlpha(m_Alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoInputs;
ImGui::Begin("##MyOverlay", nullptr, flags);
// ... your content here (display-only, no interactive controls) ...
ImGui::End();
}
}Ключевые моменты:
-
ImGuiCond_Appearingустанавливает позицию только при первом показе; затем ImGui отслеживает позицию перетаскивания -
NoTitleBar+ безNoMove= окно перетаскивается за любую пустую область (поведение ImGui по умолчанию) -
NoInputsв неинтерактивном режиме предотвращает перехват фокуса оверлеем черезWS_EX_TRANSPARENT - Всегда проверяйте указатель
IsMenuVisibleна null для обратной совместимости:m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false - Сохраняйте позицию в файл настроек, чтобы она сохранялась между сессиями
void OnEnable(bool isGameOpened) override {
LoadSettings(); // Load from Plugins/YourPlugin/config/settings.txt
}
void SaveSettings() override {
// Write to Plugins/YourPlugin/config/settings.txt
// The host calls this periodically and on shutdown
}Храните настройки в <PluginDirectory>/config/:
std::filesystem::path settingsPath =
std::filesystem::path(m_Directory) / "config" / "settings.txt";Для плагинов с множеством настроек хорошо подходит простой текстовый формат ключ=значение:
// Save
std::ofstream file(configDir / "settings.txt");
file << "ShowOverlay=" << (m_Settings.ShowOverlay ? 1 : 0) << "\n";
file << "WindowAlpha=" << m_Settings.WindowAlpha << "\n";
file << "PosX=" << m_Settings.PosX << "\n";
file << "PosY=" << m_Settings.PosY << "\n";
// Load
std::ifstream file(settingsPath);
std::string line;
while (std::getline(file, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "ShowOverlay") m_Settings.ShowOverlay = (val == "1");
else if (key == "WindowAlpha") m_Settings.WindowAlpha = std::stof(val);
else if (key == "PosX") m_Settings.PosX = std::stof(val);
else if (key == "PosY") m_Settings.PosY = std::stof(val);
}auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100auto snapshot = m_Context->GetSnapshot();
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster &&
e.Zone == NearbyZone::InnerCircle) {
// e.CurrentHP, e.Path, e.WorldX/Y/Z...
}
}auto vitals = m_Context->GetPlayerVitals();
for (auto& buff : vitals.Buffs) {
if (buff.Name == "flask_effect_life") {
// buff.TimeLeft, buff.Charges...
}
}auto snapshot = m_Context->GetSnapshot();
std::string area = snapshot->CurrentAreaName;
bool isTown = snapshot->IsTown;
int level = snapshot->CurrentAreaLevel;for (auto& e : snapshot->Entities) {
if (e.entityType != EntityTypes::Monster) continue;
float sx, sy;
if (m_Context->WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, &sx, &sy)) {
auto* dl = ImGui::GetBackgroundDrawList();
dl->AddText(ImVec2(sx, sy - 15), IM_COL32(255, 255, 0, 255), "Monster");
}
}// First, request an inventory scan (call periodically, e.g. every 2s)
m_Context->RequestInventoryScan(-1);
// Then read from snapshot (next frame)
auto snapshot = m_Context->GetSnapshot();
for (auto& inv : snapshot->Inventories) {
for (auto& item : inv.Items) {
auto mods = m_Context->ReadExtendedItemMods(item.Address);
// mods.ExplicitMods, mods.ImplicitMods...
}
}auto snapshot = m_Context->GetSnapshot();
int monsters = 0, items = 0;
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster) monsters++;
if (e.entityType == EntityTypes::Item) items++;
}static uint64_t lastAreaChange = 0;
auto snapshot = m_Context->GetSnapshot();
if (snapshot->AreaChangeCounter != lastAreaChange) {
lastAreaChange = snapshot->AreaChangeCounter;
// Area changed! Reset state...
}if (m_Context->GetCurrentState() == GameStateTypes::AreaLoadingState) {
// Currently loading...
}Мёртвые сущности фильтруются из снимка (EntityState::Useless), поэтому вы не можете обнаружить падение HP до 0. Вместо этого отслеживайте сущности по ID и обнаруживайте их исчезновение из ближних зон:
// In your tracker class:
struct TrackedEntity {
uint32_t Id;
int Rarity;
PluginSDK::NearbyZone Zone;
};
std::unordered_map<uint32_t, TrackedEntity> m_PrevEntities;
void DetectKills(const std::vector<PluginSDK::RadarEntity>& entities) {
std::unordered_set<uint32_t> currentIds;
// Update tracking map with current monsters
for (const auto& e : entities) {
if (e.entityType != EntityTypes::Monster) continue;
if (e.entityState == EntityStates::MonsterFriendly) continue;
currentIds.insert(e.Id);
m_PrevEntities[e.Id] = { e.Id, e.Rarity, e.Zone };
}
// Disappeared from InnerCircle/OuterCircle = killed
for (auto it = m_PrevEntities.begin(); it != m_PrevEntities.end(); ) {
if (currentIds.count(it->first) == 0) {
if (it->second.Zone == NearbyZone::InnerCircle ||
it->second.Zone == NearbyZone::OuterCircle) {
OnMonsterKilled(it->second.Rarity);
}
it = m_PrevEntities.erase(it);
} else {
++it;
}
}
}Почему это работает: Сущности в пределах ~120 единиц сетки, которые внезапно исчезают, почти наверняка были убиты (а не просто вышли за пределы дальности). Сущности в зоне Far естественно появляются и исчезают из списка сущностей — не считайте их.
Важно: Очищайте m_PrevEntities при смене зоны (изменился AreaChangeCounter), чтобы избежать ложных срабатываний.
// Read a struct from a known address
struct MyGameStruct { int field1; float field2; };
MyGameStruct data{};
if (m_Context->ReadProcessMemory(someAddress, &data, sizeof(data))) {
// data.field1, data.field2 are now populated
}
// Read a string from memory
std::string str = m_Context->ReadString(stringAddress);uintptr_t gameStatesAddr = m_Context->GetPatternAddress("Game States");
if (gameStatesAddr != 0) {
// Read data at the resolved pattern address
uint64_t value = 0;
m_Context->ReadProcessMemory(gameStatesAddr, &value, sizeof(value));
}int gridW = 0, gridH = 0;
const uint8_t* grid = m_Context->GetWalkableGrid(&gridW, &gridH);
if (grid && gridW > 0 && gridH > 0) {
int x = (int)snapshot->Player.GridPositionX;
int y = (int)snapshot->Player.GridPositionY;
if (x >= 0 && x < gridW && y >= 0 && y < gridH) {
bool walkable = grid[y * gridW + x] != 0;
}
}PluginSDK::MemoryReader mem(m_Context);
// Read a struct from a known address
struct GameData { int level; float health; };
auto data = mem.Read<GameData>(address);
// Read a StdVector of pointers
auto ptrs = mem.ReadStdVector<uintptr_t>(vectorAddr);
for (auto ptr : ptrs) { /* process each pointer */ }
// Read a StdMap<int, float>
auto entries = mem.ReadStdMap<int, float>(mapAddr);
for (auto& [key, value] : entries) { /* key, value */ }for (auto& inv : snapshot->Inventories) {
const char* name = m_Context->GetInventoryName(inv.Id);
// name is e.g. "MainInventory1", "Weapon1", "Currency1"
}for (auto& e : snapshot->Entities) {
auto& cc = e.ComponentCache;
if (cc.HasLife()) {
// cc.LifeAddr contains the Life component address
// Use MemoryReader to read component structs
}
if (cc.HasRender()) {
// cc.RenderAddr has the Render component address
}
}// Get all entities with debug info
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
bool open = ImGui::TreeNode(e.Path.c_str());
if (open) {
m_Context->WatchEntity(e.Id);
auto comp = m_Context->GetWatchedEntityData(e.Id);
if (comp.HasLife) {
ImGui::Text("HP: %d/%d ES: %d/%d MP: %d/%d",
comp.Life.Health.Current, comp.Life.Health.Total,
comp.Life.EnergyShield.Current, comp.Life.EnergyShield.Total,
comp.Life.Mana.Current, comp.Life.Mana.Total);
}
if (comp.HasActor) {
ImGui::Text("Animation: %s (%d) Skills: %d",
comp.Actor.AnimationName.c_str(), comp.Actor.AnimationId,
(int)comp.Actor.ActiveSkills.size());
}
ImGui::TreePop();
} else {
m_Context->UnwatchEntity(e.Id);
}
}auto invList = m_Context->GetPlayerInventoryList();
if (!invList.empty()) {
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
if (inv.InventoryId >= 0) {
ImGui::Text("Grid: %dx%d Items: %d",
inv.TotalBoxesX, inv.TotalBoxesY, (int)inv.Items.size());
for (auto& item : inv.Items) {
ImGui::Text("[R%d iLvl%d] %s (%s) Mods: %d/%d/%d/%d",
item.Rarity, item.ItemLevel,
item.BaseTypeName.c_str(), item.Path.c_str(),
(int)item.ImplicitMods.size(), (int)item.ExplicitMods.size(),
(int)item.EnchantMods.size(), (int)item.HellscapeMods.size());
}
}
}uintptr_t uiRoot = m_Context->GetGameUiRootAddress();
if (uiRoot) {
PluginSDK::MemoryReader mem(m_Context);
// Read children vector at offset 0x010
auto children = mem.ReadStdVector<uintptr_t>(uiRoot + 0x010);
for (auto childAddr : children) {
// Read StringId at offset 0x448
uintptr_t strPtr = mem.Read<uintptr_t>(childAddr + 0x448);
if (strPtr) {
std::string name = m_Context->ReadString(strPtr);
ImGui::Text("Child: %s (0x%llX)", name.c_str(), childAddr);
}
}
}auto mods = m_Context->ReadExtendedItemMods(item.Address);
ImGui::TextColored(
PluginSDK::GetRarityColor(mods.Rarity),
"Rarity: %s", PluginSDK::GetRarityName(mods.Rarity));
for (auto& mod : mods.ExplicitMods) {
ImGui::BulletText("%s", mod.Key.c_str());
}| Настройка | Значение |
|---|---|
| Configuration | Release |
| Platform | x64 |
| C++ Standard | /std:c++20 |
| Runtime Library |
/MD (Multi-threaded DLL) |
| Configuration Type | DLL |
- Ваш(и) файл(ы)
.cppплагина - Исходные файлы ImGui:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Путь включения к корню POEFixer (для заголовков SDK и ImGui)
- Необязательно: скопируйте
Plugins/ExamplePlugin/sdk/PluginHelpers.hдля обёрткиMemoryReaderи вспомогательных функций
Ваш .vcxproj должен содержать следующие дополнительные директории включения:
<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>Если вы используете локальные сторонние библиотеки (например, SQLite3 в подпапке lib/), добавьте $(ProjectDir)lib перед путём решения, чтобы локальные заголовки имели приоритет:
<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>Для использования SQLite3 в плагине необходимо компилировать исходный код amalgamation непосредственно в вашу DLL — Windows LoadLibrary не ищет зависимости в директории самой DLL, поэтому динамическая линковка sqlite3.dll завершится ошибкой 126.
Шаги:
- Скопируйте
sqlite3.cиsqlite3.hв директориюlib/вашего плагина - Создайте
lib/sqlite3-vcpkg-config.hдля переопределенияSQLITE_API(предотвращает ошибки__declspec(dllimport)):#ifndef SQLITE_API #define SQLITE_API #endif #define SQLITE_ENABLE_UNLOCK_NOTIFY 1 #define SQLITE_OS_WIN 1 #define SQLITE_ENABLE_COLUMN_METADATA 1
- Добавьте
sqlite3.cв ваш.vcxprojкак файл C с отключёнными предупреждениями:<ClCompile Include="lib\sqlite3.c"> <CompileAs>CompileAsC</CompileAs> <WarningLevel>TurnOffAllWarnings</WarningLevel> <SDLCheck>false</SDLCheck> <PreprocessorDefinitions>SQLITE_THREADSAFE=1;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile>
Для загрузки текстур из файлов изображений (PNG, JPG) подключите stb_image в одном .cpp файле:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"Затем используйте устройство D3D11 из m_Context->D3DDevice для создания GPU-текстур.
Установите выходную директорию:
$(SolutionDir)x64\Release\Plugins\YourPlugin\
Скопируйте собранную DLL в Plugins/YourPlugin/YourPlugin.dll рядом с основным исполняемым файлом.
- Соберите DLL плагина в режиме Debug
- Запустите основное приложение
- В Visual Studio: Debug → Attach to Process → выберите .exe хоста
- Установите точки останова в исходном коде плагина
- Отладчик остановится при вызове вашего кода
| Проблема | Решение |
|---|---|
| Plugin not loading | Проверьте, что имя DLL точно совпадает с именем папки |
| "SDK version mismatch" | Пересоберите плагин с последними заголовками SDK (текущая версия: 5) |
| LoadLibrary error 126 | У DLL есть неразрешённые зависимости. Для сторонних библиотек, таких как SQLite3, компилируйте их статически в DLL (см. Раздел 9). Используйте dumpbin /dependents YourPlugin.dll для проверки. |
| Crash on load | Проверьте несовпадение CRT — оба должны использовать /MD
|
| ImGui not rendering | Убедитесь, что ImGui::SetCurrentContext() вызван в SetContext()
|
| Data is empty/zero | Проверьте IsAttached() и IsInGame() перед чтением данных |
| Inventory is empty | Вызовите RequestInventoryScan(-1) — данные инвентаря загружаются по запросу |
| "Missing exports" error | Убедитесь, что CreatePlugin и DestroyPlugin экспортированы с extern "C"
|
| Plugin crashes host | Этого не должно происходить — все вызовы плагинов защищены SEH. Проверьте логи. |
| Stale data |
GetSnapshot() возвращает данные последнего кадра. Не кэшируйте указатель. |
| Memory read returns 0 | Убедитесь, что IsAttached() возвращает true и адрес валиден |
| WorldToScreen returns false | Позиция может быть за камерой или за пределами экрана |
| Overlay window not clickable | Хост использует WS_EX_TRANSPARENT, когда меню скрыто. Используйте IsMenuVisible() для показа интерактивных элементов только при активном меню. См. паттерн перетаскиваемого оверлея в Разделе 6. |
| Kill/death detection doesn't work | Мёртвые сущности удаляются из снимка. Используйте обнаружение по исчезновению вместо отслеживания перехода HP. См. Раздел 8. |
| C2491 "dllimport function" errors | Заголовки вашей сторонней библиотеки определяют __declspec(dllimport). Создайте локальный заголовок-переопределение, устанавливающий макрос API в пустое значение (см. пример SQLite3 в Разделе 9). |