-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide KO
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - x64 Release 빌드
-
런타임 라이브러리:
/MD(Multi-threaded DLL) — 호스트와 일치해야 합니다
- Visual Studio에서 새 C++ DLL 프로젝트를 생성합니다
- 인클루드 경로를 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 통합 (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() -- 리소스 초기화
↓
Main Loop (every frame):
→ DrawUI() -- 오버레이 렌더링 (활성화된 경우에만)
→ DrawSettings() -- Plugins 탭에서 설정 렌더링
→ WantsOverlay() -- 호스트가 플러그인의 오버레이 모드 요청 확인
↓
Periodically / on shutdown:
→ 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"과 같은 고유 윈도우 ID를 사용하세요
- 호출 시점: 매 프레임, Plugins 설정 탭에서 (활성화된 경우에만)
- 목적: 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));게임 메모리에서 null 종료 ASCII 문자열을 읽습니다 (최대 128자).
게임 메모리에서 null 종료 유니코드(와이드) 문자열을 읽습니다 (최대 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 = 이동 불가, 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)(Translation pending)
SDK v5 adds direct UI element reading without the debug watch mechanism. Navigate the UI tree, check visibility, read text, and compute screen rectangles.
| Function | Return | Purpose |
|---|---|---|
ReadUiElement(addr) |
UiElementData |
Read core UI element properties |
GetUiChildren(addr) |
vector<uintptr_t> |
Get all child element addresses |
GetUiChildAt(addr, index) |
uintptr_t |
Get single child by index |
ReadUiChildChain(root, indices, count) |
uintptr_t |
Navigate child index path from root |
IsUiElementVisible(addr) |
bool |
Check visibility (including ancestors) |
GetUiStringId(addr) |
string |
Get element's string identifier |
ComputeUiScreenRect(addr, outX, outY, outW, outH) |
bool |
Compute screen rectangle with recursive scaling |
GetUiText(addr) |
string |
Get element's display text |
21 typed component reader functions. Each takes a component address from EntityComponentCache and returns a struct with Valid flag.
| Function | Return Struct | Component |
|---|---|---|
ReadLifeComponent(addr) |
PluginLifeData |
Life (HP/ES/Mana) |
ReadRenderComponent(addr) |
PluginRenderData |
Render (position, bounds) |
ReadPositionedComponent(addr) |
PluginPositionedData |
Positioned (reaction) |
ReadTargetableComponent(addr) |
PluginTargetableData |
Targetable (flags) |
ReadChestComponent(addr) |
PluginChestData |
Chest (opened, quality) |
ReadShrineComponent(addr) |
PluginShrineData |
Shrine (available) |
ReadStackComponent(addr) |
PluginStackData |
Stack (size) |
ReadChargesComponent(addr) |
PluginChargesData |
Charges |
ReadPlayerComponent(addr) |
PluginPlayerData |
Player (name, level) |
ReadAnimatedComponent(addr) |
PluginAnimatedData |
Animated (animation) |
ReadTransitionableComponent(addr) |
PluginTransitionableData |
Transitionable |
ReadTriggerableBlockageComponent(addr) |
PluginTriggerableBlockageData |
TriggerableBlockage |
ReadMinimapIconComponent(addr) |
PluginMinimapIconData |
MinimapIcon |
ReadStateMachineComponent(addr) |
PluginStateMachineData |
StateMachine |
ReadBaseComponent(addr) |
PluginBaseData |
Base (cell size, influence) |
ReadModsComponent(addr) |
PluginModsData |
Mods (rarity, mod lists) |
ReadStatsComponent(addr) |
PluginStatsData |
Stats (key-value pairs) |
ReadBuffsComponent(addr) |
PluginBuffsData |
Buffs (active buffs) |
ReadActorComponent(addr) |
PluginActorData |
Actor (skills, deploy) |
ReadNpcComponent(addr) |
PluginNpcData |
NPC (hidden, icon) |
ReadDiesAfterTimeComponent(addr) |
PluginDiesAfterTimeData |
DiesAfterTime |
| Helper | Signature | Description |
|---|---|---|
GetHealthPercent |
float (uintptr_t lifeAddr) |
HP percentage (0-100) |
GetEsPercent |
float (uintptr_t lifeAddr) |
Energy Shield percentage |
GetManaPercent |
float (uintptr_t lifeAddr) |
Mana percentage |
IsAlive |
bool (uintptr_t lifeAddr) |
True if HP > 0 |
IsChestOpenedHelper |
bool (uintptr_t chestAddr) |
True if chest opened |
GetWorldPosition |
bool (uintptr_t renderAddr, float*, float*, float*) |
Extract world position |
GetItemRarityFromMods |
int (uintptr_t modsAddr) |
Item rarity from Mods |
IsItemIdentifiedHelper |
bool (uintptr_t modsAddr) |
True if identified |
GetStackCountHelper |
int (uintptr_t stackAddr) |
Stack count |
GetPlayerNameHelper |
string (uintptr_t playerAddr) |
Player name |
// Read entity health via component reader
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);
}
}
}
// Navigate UI tree
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에 포함)는 원시 PluginContext 함수를 래핑하는 타입 안전한 MemoryReader 클래스를 제공합니다:
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 컨텍스트. SetContext()에서 ImGui::SetCurrentContext()를 호출하세요.
호스트의 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가 0으로 떨어지는 것을 볼 수 없습니다 — 단순히 목록에서 사라집니다.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> |
Crucible 모드 |
Rarity |
int |
0=일반, 1=매직, 2=레어, 3=유니크 |
| 필드 | 타입 | 설명 |
|---|---|---|
Key |
string |
모드 스탯 키 |
Values |
vector<float> |
모드 롤 값 |
엔티티별로 캐시된 컴포넌트 주소 (RadarEntity.ComponentCache를 통해 사용 가능):
| 필드 | 타입 | 존재 확인 메서드 |
|---|---|---|
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
|
Animation Path (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=Prefix, 2=Suffix, 3=Implicit |
Value0 |
float |
첫 번째 값 (없으면 NaN) |
Value1 |
float |
두 번째 값 (없으면 NaN) |
(Translation pending)
| Field | Type | Description |
|---|---|---|
Valid |
bool |
Whether the read succeeded |
X / Y
|
float |
Element position |
Width / Height
|
float |
Element size |
ScaleX / ScaleY
|
float |
Scale factors |
IsVisible |
bool |
Visibility flag |
IsEnabled |
bool |
Enabled flag |
ChildCount |
int |
Number of children |
ParentAddr |
uintptr_t |
Parent element address |
SelfAddr |
uintptr_t |
Self pointer |
All component reader functions return structs with a Valid field. See the English guide for full field listings of all 22 structs (PluginLifeData, PluginRenderData, PluginPositionedData, PluginTargetableData, PluginChestData, PluginShrineData, PluginStackData, PluginChargesData, PluginPlayerData, PluginAnimatedData, PluginTransitionableData, PluginTriggerableBlockageData, PluginMinimapIconData, PluginStateMachineData, PluginBaseData, PluginModsData, PluginStatsData, PluginBuffsData, PluginActorData, PluginNpcData, PluginDiesAfterTimeData).
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 컨텍스트를 공유합니다. SetContext() 메서드에서 다음을 반드시 호출해야 합니다:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));호스트나 다른 플러그인과의 충돌을 피하기 위해 항상 고유한 윈도우 ID를 사용하세요:
ImGui::Begin("My Window##MyPluginName", &showWindow);- 윈도우, 탭, 트리, 테이블, 드로우 리스트
- D3D11 디바이스를 통한 텍스처 로딩
-
ImGui::GetBackgroundDrawList()를 통한 오버레이 렌더링 -
#include "imgui/IconsFontAwesome6.h"를 통한 FontAwesome 6 아이콘 (예: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) {
// === 드래그 모드 ===
// 배경, 드래그 힌트, 대화형 컨트롤이 있는 윈도우
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 {
// === 비대화형 모드 ===
// 정적 오버레이 — 드래그 없음, 마우스 상호작용 없음
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 존의 엔티티는 엔티티 목록에 자연스럽게 나타났다 사라집니다 — 이것은 카운트하지 마세요.
중요: 지역 변경 시(AreaChangeCounter가 변경됨) m_PrevEntities를 초기화하여 오탐을 방지하세요.
// 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 헤더용)
- 선택 사항:
MemoryReader래퍼와 유틸리티 함수를 위해Plugins/ExamplePlugin/sdk/PluginHelpers.h를 복사
.vcxproj에 다음 추가 인클루드 디렉토리가 필요합니다:
<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>로컬 서드파티 라이브러리(예: lib/ 하위 폴더의 SQLite3)를 사용하는 경우, 솔루션 경로 앞에 $(ProjectDir)lib를 추가하여 로컬 헤더가 우선하도록 합니다:
<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>플러그인에서 SQLite3를 사용하려면 통합 소스를 DLL에 직접 컴파일해야 합니다 — Windows LoadLibrary는 DLL 자체 디렉토리에서 종속성을 검색하지 않으므로 sqlite3.dll의 동적 링크는 오류 126으로 실패합니다.
단계:
-
sqlite3.c와sqlite3.h를 플러그인의lib/디렉토리에 복사합니다 -
SQLITE_API를 오버라이드하는lib/sqlite3-vcpkg-config.h를 만듭니다 (__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)에서 텍스처를 로드하려면 하나의 .cpp 파일에 stb_image를 포함합니다:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"그런 다음 m_Context->D3DDevice의 D3D11 디바이스를 사용하여 GPU 텍스처를 만듭니다.
출력 디렉토리를 설정합니다:
$(SolutionDir)x64\Release\Plugins\YourPlugin\
빌드된 DLL을 메인 실행 파일 옆의 Plugins/YourPlugin/YourPlugin.dll로 복사합니다.
- 플러그인 DLL을 Debug 모드로 빌드합니다
- 호스트 애플리케이션을 시작합니다
- Visual Studio에서: Debug → Attach to Process → 호스트 .exe 선택
- 플러그인 소스에 브레이크포인트를 설정합니다
- 코드가 호출될 때 디버거가 중단됩니다
| 문제 | 해결 방법 |
|---|---|
| 플러그인이 로드되지 않음 | DLL 이름이 폴더 이름과 정확히 일치하는지 확인 |
| "SDK version mismatch" | 최신 SDK 헤더로 플러그인을 다시 빌드 (현재 버전: 5) |
| LoadLibrary 오류 126 | DLL에 해결되지 않은 종속성이 있습니다. SQLite3와 같은 서드파티 라이브러리는 DLL에 정적으로 컴파일하세요 (섹션 9 참조). dumpbin /dependents YourPlugin.dll로 확인하세요. |
| 로드 시 크래시 | CRT 불일치 확인 — 둘 다 /MD를 사용해야 합니다 |
| ImGui가 렌더링되지 않음 |
SetContext()에서 ImGui::SetCurrentContext()가 호출되는지 확인 |
| 데이터가 비어있거나 0 | 데이터를 읽기 전에 IsAttached()와 IsInGame()을 확인 |
| 인벤토리가 비어있음 |
RequestInventoryScan(-1)을 호출하세요 — 인벤토리 데이터는 요청 시 제공됩니다 |
| "Missing exports" 오류 |
CreatePlugin과 DestroyPlugin이 extern "C"로 내보내졌는지 확인 |
| 플러그인이 호스트를 크래시시킴 | 이것은 발생해서는 안 됩니다 — 모든 플러그인 호출은 SEH로 보호됩니다. 로그를 확인하세요. |
| 오래된 데이터 |
GetSnapshot()은 최신 프레임의 데이터를 반환합니다. 포인터를 캐시하지 마세요. |
| 메모리 읽기가 0을 반환 |
IsAttached()가 true이고 주소가 유효한지 확인 |
| WorldToScreen이 false를 반환 | 위치가 카메라 뒤쪽이거나 화면 밖일 수 있습니다 |
| 오버레이 윈도우가 클릭되지 않음 | 메뉴가 숨겨져 있을 때 호스트는 WS_EX_TRANSPARENT를 사용합니다. IsMenuVisible()을 사용하여 메뉴가 활성일 때만 대화형 컨트롤을 표시하세요. 섹션 6의 드래그 가능 오버레이 패턴을 참조. |
| 킬/사망 감지가 작동하지 않음 | 죽은 엔티티는 스냅샷에서 제거됩니다. HP 전환 대신 소멸 기반 감지를 사용하세요. 섹션 8 참조. |
| C2491 "dllimport function" 오류 | 서드파티 라이브러리 헤더가 __declspec(dllimport)를 정의합니다. API 매크로를 빈 값으로 설정하는 로컬 오버라이드 헤더를 만드세요 (섹션 9의 SQLite3 예제 참조). |