Skip to content

Plugin Development Guide KO

Lafko edited this page Apr 9, 2026 · 14 revisions

← Home


플러그인 개발 가이드

1. 시작하기

사전 요구 사항

  • MSVC v143 (Visual Studio 2022)
  • C++20 (/std:c++20)
  • x64 Release 빌드
  • 런타임 라이브러리: /MD (Multi-threaded DLL) — 호스트와 일치해야 합니다

프로젝트 설정

  1. Visual Studio에서 새 C++ DLL 프로젝트를 생성합니다
  2. 인클루드 경로를 POEFixer 소스 디렉토리로 설정합니다 (SDK 및 ImGui 헤더용)
  3. ImGui 소스 파일을 프로젝트에 추가합니다: imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp
  4. 플러그인 소스에 Plugin SDK 헤더를 포함합니다:
    #include "plugin_sdk/PluginAPI.h"
    #include "plugin_sdk/PluginContext.h"
    #include "imgui/imgui.h"
    또는 ExamplePlugin의 편의 헤더를 사용합니다:
    #include "sdk/PluginHelpers.h"  // Includes all SDK headers + MemoryReader + utilities
  5. 프로젝트의 전처리기 정의에 PLUGIN_EXPORTS_CRT_SECURE_NO_WARNINGS를 정의합니다

폴더 구조

Plugins/
  YourPlugin/
    YourPlugin.dll      <-- DLL 이름은 반드시 폴더 이름과 일치해야 합니다
    config/
      settings.txt      <-- 선택적 설정 파일
    data/
      ...               <-- 선택적 데이터 디렉토리 (데이터베이스, 캐시 등)

ExamplePlugin 프로젝트 구조

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 요소 탐색기

KillCount 플러그인 구조

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을 찾습니다

2. 플러그인 생명주기

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는 스레드 안전하지 않습니다

3. IPlugin 인터페이스 레퍼런스

모든 플러그인은 IPlugin 인터페이스(plugin_sdk/PluginAPI.h에 정의)를 구현해야 합니다:

void SetPluginDirectory(const char* dir)

  • 호출 시점: 생성 직후 한 번
  • 매개변수: "Plugins/YourPlugin"과 같은 상대 경로
  • 목적: 설정/리소스 로딩을 위해 이 경로를 저장합니다

void SetContext(PluginContext* context)

  • 호출 시점: SetPluginDirectory 이후 한 번
  • 매개변수: 호스트의 PluginContext에 대한 포인터 (플러그인 수명 동안 유효)
  • 목적: 이 포인터를 저장합니다 — 모든 게임 데이터에 대한 게이트웨이입니다
  • 중요: 여기에서 ImGui::SetCurrentContext(ctx->ImGuiContext)를 호출하세요

void OnEnable(bool isGameOpened)

  • 호출 시점: 사용자가 플러그인을 활성화할 때, 또는 이전에 활성화되었다면 시작 시
  • 매개변수: 게임 프로세스가 현재 연결되어 있으면 true
  • 목적: 설정 로드, 리소스 할당, 상태 초기화

void OnDisable()

  • 호출 시점: 사용자가 플러그인을 비활성화할 때
  • 목적: 리소스 해제, 백그라운드 작업 중지

void DrawUI()

  • 호출 시점: 매 프레임, 플러그인이 활성화된 경우에만
  • 목적: ImGui를 사용하여 오버레이 렌더링
  • 참고: 충돌을 피하기 위해 "MyWindow##MyPlugin"과 같은 고유 윈도우 ID를 사용하세요

void DrawSettings()

  • 호출 시점: 매 프레임, Plugins 설정 탭에서 (활성화된 경우에만)
  • 목적: ImGui를 사용하여 플러그인 구성 렌더링

void SaveSettings()

  • 호출 시점: 주기적으로 및 애플리케이션 종료 시
  • 목적: 디스크에 설정 저장 (예: Plugins/YourPlugin/config/settings.txt)

const char* GetName()

  • 반환값: Plugins 탭에 표시되는 이름 (예: "My Plugin")

int GetSDKVersion()

  • 반환값: PLUGIN_SDK_VERSION (현재 4)
  • 목적: 호스트가 호환성을 확인 — 반드시 일치해야 합니다

bool WantsOverlay() (SDK v2)

  • 반환값: 플러그인이 오버레이 모드(게임 위의 투명 오버레이)에서 렌더링하려면 true
  • 기본값: false — 플러그인은 일반 설정 윈도우에서만 렌더링
  • 목적: 어떤 플러그인이든 true를 반환하면, 내장 기능이 필요로 하지 않아도 호스트가 오버레이 모드로 전환합니다

팩토리 내보내기

DLL은 이 두 C 함수를 내보내야 합니다:

extern "C" PLUGIN_API IPlugin* CreatePlugin() {
    return new MyPlugin();
}

extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
    delete plugin;
}

4. PluginContext API 레퍼런스

PluginContext 구조체(plugin_sdk/PluginContext.h에 정의)는 게임 데이터에 접근하기 위한 함수 포인터를 제공합니다. 모든 타입은 PluginSDK 네임스페이스에 있습니다.

게임 데이터 접근

GetSnapshot()shared_ptr<const PluginGameSnapshot>

게임 상태의 완전한 스냅샷을 반환합니다. 프레임당 한 번 업데이트됩니다. 포함 내용:

필드 타입 설명
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: 일반적인 레시피를 참조하세요.

GetPlayerVitals()PlayerVitals

플레이어 바이탈에 대한 편의 바로가기입니다.

GetCurrentState()GameStateTypes

현재 게임 상태 열거형을 반환합니다.

IsAttached()bool

게임 프로세스가 연결되어 있고 읽기 가능하면 true.

IsInGame()bool

현재 게임 중이면 true (로딩 중이 아니고, 로그인 화면이 아님).

IsGameForeground()bool

게임 윈도우가 포그라운드 윈도우이면 true.

GetProcessId()DWORD

게임 프로세스 ID를 반환합니다.

아이템 데이터 접근

ReadExtendedItemMods(entityAddress)ExtendedItemModInfo

아이템 엔티티의 모든 모드를 읽습니다.

ReadItemRarity(entityAddress)int

반환값: 0=일반, 1=매직, 2=레어, 3=유니크

ReadItemStackCount(entityAddress)int

화폐/스택 가능 아이템의 스택 수를 반환합니다.

ReadItemName(entityAddress)string

아이템의 기본 타입 이름을 반환합니다.

ReadItemPath(entityAddress)string

아이템의 메타데이터 경로를 반환합니다.

ReadItemBaseTypeName(entityAddress)string

아이템의 기본 타입 이름을 반환합니다 (예: "Divine Orb", "Chaos Orb"). 메타데이터 경로를 반환하는 ReadItemName과 달리, BaseItemTypeData.BaseTypeName에서 실제 기본 타입 이름을 읽습니다.

ReadItemUniqueName(entityAddress)string

Words.dat에서 유니크 아이템 이름을 반환합니다 (예: "Headhunter", "Brimstone Call"). 유니크가 아닌 아이템의 경우 빈 문자열을 반환합니다.

오버레이 모드 (SDK v2)

IsOverlayMode()bool

호스트가 현재 오버레이 모드(게임 윈도우 위의 투명 오버레이)인 경우 true를 반환합니다. 렌더링을 조정하는 데 사용하세요 — 예를 들어, 게임 오버레이에 그리기와 설정 윈도우에 그리기를 전환합니다.

UI 상태 (SDK v4)

IsMenuVisible()bool

호스트의 설정 메뉴가 표시되어 있을 때(오버레이가 대화형) true를 반환합니다. 메뉴가 숨겨져 있으면 오버레이 윈도우는 클릭 통과(WS_EX_TRANSPARENT)되므로 ImGui 윈도우가 마우스 입력을 받을 수 없습니다.

드래그 가능 오버레이 패턴을 구현하는 데 사용하세요:

  • 메뉴 표시: 드래그 핸들을 표시하고 상호작용(탭, 버튼)을 허용
  • 메뉴 숨김: 드래그 핸들을 제거하고 ImGuiWindowFlags_NoInputs를 추가하여 윈도우를 비대화형으로 설정

전체 구현은 섹션 6: 드래그 가능 오버레이 패턴을 참조하세요.

메모리 읽기 (SDK v2)

게임 프로세스 메모리에 대한 직접 접근. 모든 읽기는 안전합니다 (실패 시 0/빈 값 반환).

GetBaseAddress()uintptr_t

게임 실행 모듈의 기본 주소를 반환합니다. 연결되지 않은 경우 0을 반환합니다.

GetModuleSize()uintptr_t

게임 모듈의 바이트 크기를 반환합니다. 연결되지 않은 경우 0을 반환합니다.

ReadProcessMemory(address, buffer, size)bool

게임 프로세스에서 원시 바이트 블록을 읽습니다. 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));

ReadString(address)string

게임 메모리에서 null 종료 ASCII 문자열을 읽습니다 (최대 128자).

ReadUnicodeString(address)wstring

게임 메모리에서 null 종료 유니코드(와이드) 문자열을 읽습니다 (최대 128 wchar).

GetPatternAddress(patternName)uintptr_t

이름으로 해석된 패턴 스캔 주소를 가져옵니다. 찾지 못하면 0을 반환합니다.

표준 패턴:

이름 설명
"Game States" GameStates 벡터 루트
"File Root" 파일 레지스트리
"AreaChangeCounter" 지역 전환 카운터
"Terrain Rotator Helper" 회전 데이터
"Terrain Rotation Selector" 회전 선택자
"GameCullSize" 화면 컬 값

월드-투-스크린 투영 (SDK v2)

WorldToScreen(worldX, worldY, worldZ, outX, outY)bool

월드 공간 위치를 화면 좌표로 변환합니다. 위치가 화면에 보이면 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");
}

인벤토리 (SDK v2)

RequestInventoryScan(inventoryId)

호스트에 인벤토리 스캔을 요청합니다. 모든 인벤토리를 스캔하려면 -1을 전달하거나, 특정 인벤토리 ID를 전달합니다. 스냅샷의 인벤토리 데이터는 스캔 완료 후(다음 프레임)에 채워집니다.

참고: 인벤토리 데이터는 자동으로 갱신되지 않습니다 — 스캔을 트리거하려면 이 함수를 호출해야 합니다. 지속적인 인벤토리 데이터가 필요하면 주기적으로(예: 2초마다) 호출하세요.

지형 데이터 (SDK v2)

GetWalkableGrid(outWidth, outHeight)const uint8_t*

이동 가능 그리드 데이터에 대한 포인터를 반환합니다. 그리드는 2D 배열로 0 = 이동 불가, 0이 아닌 값 = 이동 가능입니다. 데이터를 사용할 수 없으면 nullptr을 반환합니다.

GetTerrainHeight(gridX, gridY)float

그리드 위치의 지형 높이를 반환합니다. 범위를 벗어나거나 데이터를 사용할 수 없으면 0을 반환합니다.

네이티브 컨테이너 읽기 (SDK v3)

이 함수들은 게임 메모리에서 C++ 표준 라이브러리 컨테이너를 직접 읽으며, 호스트의 Core::Process 메서드를 미러링합니다.

ReadStdVector(containerAddress, elementSize, outCount)void*

게임 메모리에서 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);
}

ReadStdList(containerAddress, elementSize, outCount)void*

게임 메모리에서 StdList (16바이트 구조체: {Head, Size})를 읽습니다. 연결 리스트를 순회하고 연속 버퍼를 반환합니다. 호출자는 free()해야 합니다.

ReadStdBucket(containerAddress, elementSize, outCount)void*

게임 메모리에서 StdBucket을 읽습니다 (내장된 StdVector를 읽음). 호출자는 free()해야 합니다.

ReadStdMap(containerAddress, keySize, valueSize, callback, userData)int

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

ReadStdWString(containerAddress)wstring

게임 메모리에서 StdWString (인라인/힙 버퍼가 있는 32바이트 구조체)을 읽습니다.

GetInventoryName(inventoryId)const char*

인벤토리 ID의 사람이 읽을 수 있는 이름을 반환합니다 (예: 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1").

디버그 데이터 접근 (SDK v4)

SDK v4는 호스트의 디버그 데이터에 대한 직접 접근을 제공합니다 — 엔티티 컴포넌트, 인벤토리 세부 정보, UI 요소 트리 — 내장 Debug 탭에 대응합니다.

엔티티 디버그 목록

GetEntityDebugList()vector<DebugEntityInfo>

디버그 메타데이터(Id, Address, Path, Type, SubType, State, Rarity, Zone)를 포함한 모든 엔티티 목록을 반환합니다. Debug→Entity List 탭에 대응합니다.

WatchEntity(entityId)

엔티티의 컴포넌트 감시를 시작합니다. 호스트의 워커 스레드가 매 프레임 이 엔티티의 전체 컴포넌트 데이터를 읽습니다.

UnwatchEntity(entityId)

엔티티의 컴포넌트 감시를 중지합니다. 사용자가 엔티티 트리 노드를 접었을 때 리소스를 해제하기 위해 호출하세요.

GetWatchedEntityData(entityId)DebugEntityComponents

감시 중인 엔티티의 전체 컴포넌트 데이터를 반환합니다. 인식된 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);
    }
}

인벤토리 디버그

GetServerDataAddress()uintptr_t

ServerData 컴포넌트 기본 주소를 반환합니다.

GetPlayerInventoryList()vector<pair<int, uintptr_t>>

모든 플레이어 인벤토리 ID와 주소(ServerData에서)를 반환합니다.

WatchInventory(inventoryId)

상세 디버그 검사를 위해 인벤토리 감시를 시작합니다. 호스트가 슬롯 점유 상태, 아이템 세부 정보, 모드를 읽습니다.

GetWatchedInventoryData()DebugInventoryData

현재 감시 중인 인벤토리의 전체 데이터를 반환합니다: 그리드 크기, 슬롯 점유 상태, 레어도와 모드가 포함된 아이템.

// 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 요소 트리

GetGameUiRootAddress()uintptr_t

루트 게임 UI 요소 주소를 반환합니다 (게임 내 UI 트리 탐색용).

GetUiRootAddress()uintptr_t

최상위 UI 루트 주소를 반환합니다.

GetGameCullValue()int

현재 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)

PluginHelpers.h — 편의 래퍼 (SDK v3)

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

MemoryReaderReadString(), ReadUnicodeString(), GetBaseAddress(), GetModuleSize(), GetPatternAddress()의 편의 래퍼도 제공합니다.

PluginHelpers.h의 추가 유틸리티:

  • WideToNarrow(wstring) — 안전한 wstring→string 변환 (ASCII 손실 있음)
  • GetEntityTypeName(type) — 열거형에서 표시 이름으로 (ExpeditionMarker/ExpeditionRemnant 포함)
  • GetNearbyZoneName(zone) — 존에서 표시 이름으로
  • GetRarityName(rarity) / GetRarityColor(rarity) — 레어도 표시 헬퍼

호스트 서비스

Log(level, message)

호스트의 로그 시스템에 기록합니다. 레벨: "Debug", "Info", "Warning", "Error"

ImGuiContext (void*)

호스트의 ImGui 컨텍스트. SetContext()에서 ImGui::SetCurrentContext()를 호출하세요.

D3DDevice (void*)

호스트의 ID3D11Device*. 캐스팅하여 텍스처 로딩에 사용하세요.


5. 데이터 구조 레퍼런스

모든 타입은 PluginSDK 네임스페이스에 있습니다. 플러그인은 일반적으로 using namespace PluginSDK;를 추가합니다.

RadarEntity

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 존에 있었음)를 구분하세요.

Buff

활성 버프/디버프:

필드 타입 설명
Name string 내부 버프 이름 (예: "flask_effect_life")
TimeLeft float 남은 시간(초)
Charges short 스택 수
TotalTime float 총 지속 시간

MapData

미니맵/대형맵 상태:

필드 타입 설명
CenterX/Y float 맵 중심
SizeX/Y float 맵 크기
ShiftX/Y float 현재 패닝 오프셋
DefaultShiftX/Y float 기본 시프트 값
Zoom float 줌 레벨
Scale float 맵 스케일 팩터
IsVisible bool 맵이 현재 표시 중

InventoryInfo / InventoryItemInfo

필드 타입 설명
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

ExtendedItemModInfo

ReadExtendedItemMods() 반환값:

필드 타입 설명
ImplicitMods vector<ItemModData> 암시적 모드
ExplicitMods vector<ItemModData> 명시적 모드
EnchantMods vector<ItemModData> 인챈트 모드
HellscapeMods vector<ItemModData> Hellscape 모드
CrucibleMods vector<ItemModData> Crucible 모드
Rarity int 0=일반, 1=매직, 2=레어, 3=유니크

ItemModData

필드 타입 설명
Key string 모드 스탯 키
Values vector<float> 모드 롤 값

EntityComponentCache

엔티티별로 캐시된 컴포넌트 주소 (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()

DebugEntityInfo (SDK v4)

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>> 모든 컴포넌트 이름→주소 쌍

DebugEntityComponents (SDK v4)

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

DebugInventoryData (SDK v4)

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> 경로, 레어도, 모드가 포함된 아이템

DebugInventoryItem (SDK v4)

필드 타입 설명
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 모드

DebugActiveSkill (SDK v4)

필드 타입 설명
Name string 스킬 이름
UseStage int 현재 사용 단계
CastType int 시전 타입
TotalUses int 총 사용 횟수
TotalCooldownTimeInMs int 쿨다운(밀리초)
CanBeUsed bool 스킬을 현재 사용할 수 있는지

DebugBuff (SDK v4)

필드 타입 설명
Name string 내부 버프 이름
TotalTime float 총 지속 시간
TimeLeft float 남은 시간(초)
Charges short 스택 수
FlaskSlot short 플라스크 슬롯 인덱스
Effectiveness short 버프 효과
SourceEntityId uint32_t 이 버프를 적용한 엔티티

DebugModInfo (SDK v4)

필드 타입 설명
Name string 모드 표시 이름
StatKey string 스탯 키 식별자
AffixName string 접사 이름
GenerationType int 1=Prefix, 2=Suffix, 3=Implicit
Value0 float 첫 번째 값 (없으면 NaN)
Value1 float 두 번째 값 (없으면 NaN)

열거형

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)


6. 플러그인에서의 ImGui 사용

공유 컨텍스트

호스트와 플러그인은 동일한 ImGui 컨텍스트를 공유합니다. SetContext() 메서드에서 다음을 반드시 호출해야 합니다:

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

윈도우 ID

호스트나 다른 플러그인과의 충돌을 피하기 위해 항상 고유한 윈도우 ID를 사용하세요:

ImGui::Begin("My Window##MyPluginName", &showWindow);

사용 가능한 기능

  • 윈도우, 탭, 트리, 테이블, 드로우 리스트
  • D3D11 디바이스를 통한 텍스처 로딩
  • ImGui::GetBackgroundDrawList()를 통한 오버레이 렌더링
  • #include "imgui/IconsFontAwesome6.h"를 통한 FontAwesome 6 아이콘 (예: ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)

오버레이 렌더링 (SDK v2)

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() 호출은 게임 화면에 직접 렌더링됩니다.

드래그 가능 오버레이 패턴 (SDK v4)

호스트 오버레이는 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 기본 동작)
  • 비대화형 모드의 NoInputsWS_EX_TRANSPARENT를 통한 포커스 탈취를 방지합니다
  • 하위 호환성을 위해 항상 IsMenuVisible 포인터의 null 체크를 수행하세요: m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false
  • 세션 간에 위치가 유지되도록 설정 파일에 위치를 저장하세요

7. 설정 저장

권장 패턴

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

8. 일반적인 레시피

플레이어 HP 백분율 가져오기

auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100

내부 원 내의 모든 몬스터 나열

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

엔티티 월드 위치에 텍스트 그리기 (SDK v2)

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를 초기화하여 오탐을 방지하세요.

원시 게임 메모리 읽기 (SDK v2)

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

패턴 스캔 결과 사용 (SDK v2)

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

이동 가능한 지형 확인 (SDK v2)

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

MemoryReader로 타입이 있는 메모리 읽기 (SDK v3)

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

인벤토리 이름 가져오기 (SDK v3)

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

디버그 감시를 통한 엔티티 컴포넌트 검사 (SDK v4)

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

슬롯 그리드가 있는 인벤토리 검사 (SDK v4)

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

UI 요소 트리 탐색 (SDK v4)

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

표시 헬퍼를 사용한 아이템 모드 읽기 (SDK v3)

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

9. 빌드 및 배포

빌드 설정

설정
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 (정적 링크)

플러그인에서 SQLite3를 사용하려면 통합 소스를 DLL에 직접 컴파일해야 합니다 — Windows LoadLibrary는 DLL 자체 디렉토리에서 종속성을 검색하지 않으므로 sqlite3.dll의 동적 링크는 오류 126으로 실패합니다.

단계:

  1. sqlite3.csqlite3.h를 플러그인의 lib/ 디렉토리에 복사합니다
  2. 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
  3. 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>

stb_image (텍스처 로딩)

이미지 파일(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로 복사합니다.

디버깅

  1. 플러그인 DLL을 Debug 모드로 빌드합니다
  2. 호스트 애플리케이션을 시작합니다
  3. Visual Studio에서: Debug → Attach to Process → 호스트 .exe 선택
  4. 플러그인 소스에 브레이크포인트를 설정합니다
  5. 코드가 호출될 때 디버거가 중단됩니다

10. 문제 해결

문제 해결 방법
플러그인이 로드되지 않음 DLL 이름이 폴더 이름과 정확히 일치하는지 확인
"SDK version mismatch" 최신 SDK 헤더로 플러그인을 다시 빌드 (현재 버전: 4)
LoadLibrary 오류 126 DLL에 해결되지 않은 종속성이 있습니다. SQLite3와 같은 서드파티 라이브러리는 DLL에 정적으로 컴파일하세요 (섹션 9 참조). dumpbin /dependents YourPlugin.dll로 확인하세요.
로드 시 크래시 CRT 불일치 확인 — 둘 다 /MD를 사용해야 합니다
ImGui가 렌더링되지 않음 SetContext()에서 ImGui::SetCurrentContext()가 호출되는지 확인
데이터가 비어있거나 0 데이터를 읽기 전에 IsAttached()IsInGame()을 확인
인벤토리가 비어있음 RequestInventoryScan(-1)을 호출하세요 — 인벤토리 데이터는 요청 시 제공됩니다
"Missing exports" 오류 CreatePluginDestroyPluginextern "C"로 내보내졌는지 확인
플러그인이 호스트를 크래시시킴 이것은 발생해서는 안 됩니다 — 모든 플러그인 호출은 SEH로 보호됩니다. 로그를 확인하세요.
오래된 데이터 GetSnapshot()은 최신 프레임의 데이터를 반환합니다. 포인터를 캐시하지 마세요.
메모리 읽기가 0을 반환 IsAttached()가 true이고 주소가 유효한지 확인
WorldToScreen이 false를 반환 위치가 카메라 뒤쪽이거나 화면 밖일 수 있습니다
오버레이 윈도우가 클릭되지 않음 메뉴가 숨겨져 있을 때 호스트는 WS_EX_TRANSPARENT를 사용합니다. IsMenuVisible()을 사용하여 메뉴가 활성일 때만 대화형 컨트롤을 표시하세요. 섹션 6의 드래그 가능 오버레이 패턴을 참조.
킬/사망 감지가 작동하지 않음 죽은 엔티티는 스냅샷에서 제거됩니다. HP 전환 대신 소멸 기반 감지를 사용하세요. 섹션 8 참조.
C2491 "dllimport function" 오류 서드파티 라이브러리 헤더가 __declspec(dllimport)를 정의합니다. API 매크로를 빈 값으로 설정하는 로컬 오버라이드 헤더를 만드세요 (섹션 9의 SQLite3 예제 참조).

← Home

Clone this wiki locally