Skip to content

Plugin Development Guide KO

Lafko edited this page Jun 24, 2026 · 14 revisions

← Home


플러그인 개발 가이드

POEFixer 플러그인은 런타임에 Plugins/<PluginName>/<PluginName>.dll 경로에서 로드되는 네이티브 C++ DLL입니다. 실시간 게임 상태를 읽고, ImGui 오버레이를 그리고, 자체 설정을 영구 저장하고, 호스트 이벤트를 구독합니다.


1. 개요

플러그인 SDK는 3계층 아키텍처로 구성되어 있습니다.

Plugin DLL  ───►  PluginSDK.h  (header-only C++ wrapper, owns std::string/vector/function)
                         │
                         ▼  inline function-pointer calls only
                  HostAbi  (pure-C ABI, POD structs only)
                         │
                         ▼  SEH-wrapped on the host side
   Host bridge: plugin_manager/bridge/Bridge_<Service>.cpp  (10 files)
                         │
                         ▼
                   GameClient + GameLibrary
  • 플러그인 작성자는 정확히 하나의 헤더만 포함하면 됩니다. POEFixer/plugin_sdk/PluginSDK.h.
  • 해당 헤더는 PluginSDK:: 네임스페이스 내의 모든 것을 선언하며, 그 아래에서 PluginAbi.h의 C ABI를 가져옵니다. 후자의 존재를 언급할 수는 있지만, 직접 들여다볼 일은 거의 없습니다.
  • 모든 std::* 컨테이너는 플러그인 DLL 내부에 존재합니다. 호스트 경계를 넘는 것은 POD뿐입니다. 즉, 특정 툴체인 버전으로 빌드된 플러그인이 호스트의 STL과 얽힐 일이 없습니다 — 공유되는 타입은 정수, 부동소수점, 포인터, 작은 구조체뿐입니다.

SDK 헤더 위치는 다음과 같습니다.

  • POEFixer/plugin_sdk/PluginSDK.h — 플러그인 작성자가 사용하는 C++ 래퍼.
  • POEFixer/plugin_sdk/PluginAbi.h — 그 아래의 순수 C ABI.

저장소에 함께 제공되는 참조 플러그인(문서처럼 읽어보세요): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.


2. Hello-world 플러그인

로드되어 호스트 로그에 메시지를 출력하는 최소한의 플러그인입니다.

#define PLUGIN_EXPORTS
#include "POEFixer/plugin_sdk/PluginSDK.h"

class HelloPlugin : public PluginSDK::Plugin {
public:
    const char* GetName() const override { return "Hello"; }
    void OnEnable(bool) override { ctx()->Log.Info("Hello, world"); }
};

extern "C" PLUGIN_API PluginSDK::Plugin* CreatePlugin()  { return new HelloPlugin(); }
extern "C" PLUGIN_API void               DestroyPlugin(PluginSDK::Plugin* p) { delete p; }

Plugins/Hello/Hello.dll로 빌드하고, 호스트를 재시작한 다음, Plugins 탭에서 활성화합니다.


3. 프로젝트 설정

Plugins/ExamplePlugin/ExamplePlugin.vcxproj를 표준 템플릿으로 사용하세요. 필수 설정은 다음과 같습니다.

  • Configuration type: DynamicLibrary
  • Platform toolset: v143 (Visual Studio 2022)
  • Character set: Unicode
  • Language standard: stdcpp20
  • Runtime library: MultiThreadedDLL (Release) / MultiThreadedDebugDLL (Debug). 호스트와 반드시 일치해야 합니다.
  • Preprocessor definitions: PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS
  • Additional include directories: $(SolutionDir)POEFixer
  • Output directory: $(SolutionDir)x64\Release\Plugins\<YourPlugin>\
  • Target name: 폴더 이름과 일치해야 합니다 (Plugins/MyPlugin/MyPlugin.dll)

호스트는 Plugins/ 내의 각 하위 폴더를 스캔하여 <FolderName>.dll을 찾습니다. DLL은 세 가지 심볼을 내보내야 합니다.

  • CreatePlugin — 팩토리; PluginSDK::Plugin*을 반환합니다.
  • DestroyPlugin — 소멸자; PluginSDK::Plugin*을 받습니다.
  • PluginSDK_AttachHostContext를 연결합니다. PluginSDK.h 내부에 정의되어 있으며, PLUGIN_EXPORTS가 설정되면 자동으로 생성됩니다.

권장 소스 레이아웃:

Plugins/YourPlugin/
  YourPlugin.vcxproj
  YourPlugin.vcxproj.filters
  src/
    YourPlugin.cpp        // class YourPlugin : public PluginSDK::Plugin
    YourPluginSettings.h  // Save()/Load() POCO
  config/                 // runtime-created by SaveSettings()
    settings.json

Directory()는 호스트 EXE 디렉터리를 기준으로 한 절대 UTF-8 경로를 반환합니다 — EXE 경로를 직접 앞에 붙이지 마세요. 디렉터리 문자열은 PluginSDK::Plugin 내부에 값으로 소유되므로, 호스트의 컨테이너 재할당이나 리로드 주기에 영향을 받지 않고 수명이 보장됩니다.

ImGui를 그리려면 <ClCompile>에도 다음을 추가하세요(호스트도 이들을 링크하지만, 플러그인 측 ImGui는 DLL별로 분리되어 있습니다).

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

OnEnable에서 호스트의 ImGui 컨텍스트에 연결합니다.

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

4. 수명주기 훅

PluginSDK::Plugin은 가상 베이스 클래스입니다. 플러그인에서 다음 항목들을 오버라이드하세요(대략 호출 순서대로 정리됨).

메서드 호출 시점 일반적인 용도
const char* GetName() const 생성 직후 한 번 플러그인 표시 이름 반환
void OnEnable(bool isGameAttached) 사용자가 플러그인을 활성화할 때(또는 영구 저장된 경우 시작 시) 설정 로드, 이벤트 구독, ImGui 컨텍스트 연결
void DrawSettings() 플러그인 설정 패널이 열려 있는 동안 매 프레임 설정용 ImGui 컨트롤
void DrawUI() 플러그인이 활성화된 동안 매 프레임 ImGui 오버레이 그리기 (게임 오버레이용으로는 ImGui::GetBackgroundDrawList() 사용)
bool WantsOverlay() const 매 프레임 폴링 호스트가 오버레이(클릭-스루) 모드이길 원하면 true 반환
void SaveSettings() 주기적(~5초) 및 비활성화 시 설정을 디스크에 영구 저장
void OnDisable() 사용자가 비활성화하거나 호스트 종료 시 리소스 해제, 이벤트 구독 해제

GetName만 필수이며, 나머지는 안전한 기본값이 있습니다.

호스트는 또한 CreatePlugin 직후 GetSDKVersion()을 호출하여(베이스에 정의되어 있으며 오버라이드하지 마세요) 플러그인과 호스트의 일치 여부를 확인합니다. 불일치 시 플러그인이 거부됩니다.


5. Context

ctx()const PluginSDK::Context*를 반환하며, 10개의 서비스를 집합한 것입니다.

struct Context {
    GameService       Game;        // snapshot, state flags, screen size
    EntitiesService   Entities;    // enumerate, find-by-id, watch
    ComponentsService Components;  // 21 component readers + collection enumerators
    InventoryService  Inventory;   // scan + iterate + per-item helpers
    UiService         Ui;          // tree walk, FindPanelByStringId, screen-rect
    RenderService     Render;      // WorldToScreen + isometric map projection
    TerrainService    Terrain;     // walkable grid (RAII), height, TGT locations
    MemoryService     Memory;      // direct memory primitives (last resort)
    LogService        Log;         // Debug/Info/Warn/Error
    EventsService     Events;      // Subscribe / Unsubscribe / On<X>
    void* ImGuiContext;            // pass to ImGui::SetCurrentContext
    void* D3DDevice;               // ID3D11Device* for texture loading
};

각 서비스가 무엇에 사용되는지 한눈에 보기:

서비스 사용 목적
GameService Snapshot, state flags, screen + window info
EntitiesService Enumerate, find-by-id, watch lifecycle
ComponentsService 21 readers + 4 enumerators + ~10 convenience helpers
InventoryService Scan, enumerate, per-item mod reads
UiService Tree walk, FindPanelByStringId, ComputeScreenRect
RenderService WorldToScreen, GridTo{Large,Mini}Map, transforms
TerrainService Walkable + height grids (RAII), TGT locations
MemoryService RPM primitives — only when no higher-level call fits
LogService Debug / Info / Warn / Error
EventsService Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}

ctx()는 호스트가 OnEnable을 호출하는 순간부터 OnDisable이 반환될 때까지 유효합니다. 핫 리로드나 DLL 언로드 경계를 넘어 ctx()를 캐시하지 마세요.


6. 게임 상태 읽기

ctx()->Game.GetSnapshot()은 값 타입의 Snapshot을 반환합니다 — 현재 프레임의 완전한 불변 뷰입니다. GetSnapshot()은 반환 전에 abi->entities.enumerate를 순회하여 snap.Entities를 채우므로, 비용은 근처 엔티티 수에 비례합니다. 프레임당 한 번 호출하고 재사용하세요.

PluginSDK::Snapshot snap = ctx()->Game.GetSnapshot();
if (snap.State != PluginSDK::GameState::InGame) return;

ctx()->Log.Info(snap.CurrentAreaName.c_str());
if (snap.IsTown || snap.IsHideout) return;  // safe area

// snap.Vitals.HPPercent, snap.Vitals.MaxES, snap.Vitals.IsPaused
// snap.Player.GridPositionX, snap.Player.Path (wstring), snap.Player.Components
// snap.Entities is a std::vector<Entity> — every nearby entity, fully populated
// snap.LargeMap / snap.MiniMap — visibility + projection inputs
// snap.AreaChangeCounter — increments each portal transition

스냅샷이 직접 담고 있는 것(추가 서비스 호출이 필요 없음):

  • 상태 + 플래그: State, IsAttached, IsWindowValid, GameWindowForeground, IsTown, IsHideout, IsPaused, IsSkillTreeVisible.
  • 영역: CurrentAreaName, CurrentAreaHash, CurrentAreaLevel, AreaChangeCounter.
  • 월드: Player (전체 Entity), Entities (전체 std::vector<Entity>), Vitals, LargeMap, MiniMap, WorldToScreenMatrix[16].
  • 윈도우: ScreenWidth, ScreenHeight, ProcessId, GameWindow, LastUpdateTime, WorldToGridConvertor.

스냅샷에 없는 것 — 서비스를 통해 가져오세요: 인벤토리 내용물(InventoryService), 버프(ComponentsService::EnumerateBuffs), 아이템별 모드 목록(InventoryService::ReadItemMods), UI 패널(UiService).

전체 스냅샷이 필요하지 않을 때의 가벼운 헬퍼들:

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

7. 컴포넌트 읽기

엔티티는 entity.Components를 통해 컴포넌트를 노출합니다 — uintptr_t 주소들로 이루어진 ComponentAddresses 구조체입니다. 각 주소를 해당하는 ComponentsService::Read*에 전달하여 값 타입 스냅샷을 얻습니다.

for (const auto& e : snap.Entities) {
    if (!e.Components.HasLife()) continue;
    PluginSDK::Life life = ctx()->Components.ReadLife(e.Components.Life);
    if (life.Valid && life.Health.Current > 0) {
        ctx()->Log.Info("alive monster");
    }
}

21개의 컴포넌트 리더가 있습니다: ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime.

ComponentAddresses 자체는 24개의 슬롯을 가집니다: 위의 21개에 세 개의 마커(Buffs, WorldItem, AreaTransition)와 OMP(호스트 내부용)가 더해진 것입니다. Buffs는 존재 여부 마커입니다 — 실제 버프 목록은 EnumerateBuffs에서 가져옵니다. WorldItem / AreaTransition은 실제 컴포넌트가 아니라 엔티티 유형 마커입니다. 모든 슬롯에는 ComponentAddresses 상의 일치하는 HasX() 술어가 있습니다.

가변 크기 데이터를 가지는 컴포넌트용 컬렉션 스타일 리더:

auto buffs  = ctx()->Components.EnumerateBuffs(e.Components.Buffs);            // std::vector<Buff>
auto skills = ctx()->Components.EnumerateActiveSkills(e.Components.Actor);     // std::vector<ActiveSkill>
auto stats  = ctx()->Components.EnumerateStats(e.Components.Stats);            // std::vector<StatEntry>
auto mods   = ctx()->Components.EnumerateItemMods(e.Components.Mods);          // std::vector<Mod>

편의용 헬퍼(원샷 — 내부적으로 Read*를 호출합니다):

float hpPct  = ctx()->Components.GetHealthPercent(e.Components.Life);
bool  alive  = ctx()->Components.IsAlive(e.Components.Life);
float esPct  = ctx()->Components.GetEsPercent(e.Components.Life);
float mpPct  = ctx()->Components.GetManaPercent(e.Components.Life);
int   rarity = ctx()->Components.GetItemRarity(e.Components.Mods);
bool  ident  = ctx()->Components.IsItemIdentified(e.Components.Mods);
int   stack  = ctx()->Components.GetStackCount(e.Components.Stack);
bool  open   = ctx()->Components.IsChestOpened(e.Components.Chest);
std::string name = ctx()->Components.GetPlayerName(e.Components.Player);
float wx, wy, wz;
if (ctx()->Components.GetWorldPosition(e.Components.Render, wx, wy, wz)) { ... }

반환되는 모든 구조체에 Valid 플래그가 있어 예외 없이 "컴포넌트 주소가 0이었거나 / 읽기 실패" 상황을 처리할 수 있습니다. 이미 상위 구조체(Life, Mods, …)를 가지고 있다면 헬퍼를 다시 호출하지 말고 그 필드에 직접 접근하세요 — 헬퍼는 매번 컴포넌트를 다시 읽습니다.

엔티티 필드

모든 Entity(snap.Playersnap.Entities의 멤버 포함)는 동일한 필드 집합을 가집니다.

그룹 필드
식별 Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
분류 EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far)
위치 GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
빠른 vitals CurrentHP, MaxHP, CurrentES, MaxES (합계만 필요한 경우 ReadLife를 피할 수 있음)
문자열 Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, asset path)
상태 IsSleeping, IsChestOpened
컴포넌트 Components (ComponentAddresses sub-struct)

특정 엔티티 감시

프레임을 가로질러 한 엔티티(예: 플레이어가 여는 상자)를 추적해야 하고, 매 프레임 전체 엔티티 목록을 스캔하고 싶지 않다면 watch를 등록하세요.

ctx()->Entities.Watch(entityId);
// ...later:
if (auto opt = ctx()->Entities.GetWatchedComponents(entityId)) {
    PluginSDK::ComponentAddresses comps = *opt;
    PluginSDK::Life l = ctx()->Components.ReadLife(comps.Life);
}
bool active = ctx()->Entities.IsWatched(entityId);
ctx()->Entities.Unwatch(entityId);

FindById(id)는 원샷 조회를 위해 std::optional<Entity>를 반환하며, GetPlayer()는 항상 로컬 플레이어를 반환합니다.

바닥 아이템 (WorldItem 컨테이너)

바닥에 떨어진 아이템은 snap.EntitiesMetadata/MiscellaneousObjects/WorldItem 경로의 EntityType::Item 엔티티로 나타납니다. 이들은 컨테이너 엔티티입니다 — Mods / Base / Stack / Sockets을 직접 가지고 있지 않습니다. 실제 아이템 엔티티는 한 단계의 간접 참조 뒤에 있습니다.

내부 아이템 엔티티를 일반 Entity 스냅샷으로 얻으려면 Entities.GetWorldItemInner를 사용하세요:

for (const auto& e : snap.Entities) {
    if (e.EntityType != PluginSDK::EntityType::Item) continue;
    auto inner = ctx()->Entities.GetWorldItemInner(e.Address);
    if (!inner) continue;  // mid-spawn, retry next frame
    // inner->Path        — "Metadata/Items/Armours/Gloves/..."
    // inner->Components  — Mods / Base / Stack / Sockets / etc.
    PluginSDK::Mods mods = ctx()->Components.ReadMods(inner->Components.Mods);
    int iLvl   = mods.ItemLevel;
    int rarity = mods.Rarity;
}

GetWorldItemInner는 실제 WorldItem 컨테이너에서만 성공합니다 — 인벤토리 아이템 주소로 호출하면 std::nullopt를 반환합니다. 인벤토리 아이템과 동일한 데이터 형태를 원한다면(수동으로 컴포넌트를 탐색하지 않고), 다음 섹션의 Inventory.ReadItem* 계열이 WorldItem 컨테이너를 투명하게 자동 해결합니다.


8. 인벤토리

ctx()->Inventory.Scan(inventoryId)는 호스트 측 재스캔을 트리거합니다. 모든 인벤토리를 스캔하려면 -1을 사용하세요.

ctx()->Inventory.Scan(-1);
std::vector<PluginSDK::Inventory> all = ctx()->Inventory.GetAll();

for (const auto& inv : all) {
    const char* name = ctx()->Inventory.GetName(inv.InventoryId);
    ctx()->Log.Info(name);
    for (const auto& item : inv.Items) {
        ctx()->Log.Info(item.BaseTypeName.c_str());
        // item.SlotX, item.SlotY, item.Width, item.Height (grid metrics)
        // item.Rarity, item.ItemLevel, item.RequiredLevel, item.CraftedModCount
        // item.IsIdentified, item.IsCorrupted, item.IsCurrency
        // item.Path (Metadata/Items/...), item.BaseTypeName, item.UniqueName
        // item.Address — entity address for direct lookups below
    }
}

Inventory는 또한 인벤토리가 화면에 그려지는 위치를 설명하는 Grid 구조체를 노출합니다.

if (inv.Grid.Valid) {
    float originX = inv.Grid.GridScreenX;
    float originY = inv.Grid.GridScreenY;
    float cell    = inv.Grid.CellSize;
    // Slot (x, y) screen-space top-left = (originX + x*cell, originY + y*cell)
}

id로 단일 인벤토리를 가져오려면(이미 채워진 Items와 함께 동일한 구조체를 반환):

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

또는 래핑 구조체 없이 아이템 벡터만 원한다면:

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

아이템 모드: 두 API, 두 범위

ComponentsService::ReadMods(addr)는 요약 플래그만 반환합니다(IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). 종류별 모드 목록은 포함되지 않습니다.

전체 그림(요약 + 모드 목록)을 위해서는 InventoryService::ReadItemMods(entityAddr)를 사용하세요.

PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(item.Address);
if (!im.Valid) return;
// Same summary fields as the Mods component, plus:
for (const auto& m : im.ImplicitMods)  { ... }   // std::vector<Mod>
for (const auto& m : im.ExplicitMods)  { ... }
for (const auto& m : im.EnchantMods)   { ... }
for (const auto& m : im.HellscapeMods) { ... }
for (const auto& m : im.CrucibleMods)  { ... }

이미 아이템 주소를 보유한 경우(재스캔보다 저렴한) 기타 엔티티별 직접 읽기:

int          rarity = ctx()->Inventory.ReadItemRarity(item.Address);
int          stack  = ctx()->Inventory.ReadItemStackCount(item.Address);
std::string  base   = ctx()->Inventory.ReadItemBaseTypeName(item.Address);
std::string  uniq   = ctx()->Inventory.ReadItemUniqueName(item.Address);
std::string  path   = ctx()->Inventory.ReadItemPath(item.Address);

인벤토리 API를 통한 바닥 아이템. 위의 일곱 가지 Inventory.ReadItem* 읽기 (그리고 ReadItemMods)는 인벤토리 아이템 주소와 WorldItem 컨테이너 주소를 모두 받습니다. 컨테이너 주소는 읽기 전에 자동으로 내부 아이템으로 해결되므로, 동일한 플러그인 코드 경로가 가방 안의 아이템과 바닥의 아이템 모두에 대해 작동합니다:

// `addr` may be either an inventory item address or a WorldItem container.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int          rarity   = ctx()->Inventory.ReadItemRarity(addr);
std::string  baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);

내부 아이템의 컴포넌트 주소가 직접 필요한 경우 (예: ctx()->Components.ReadStack(...)을 호출하거나 소켓을 탐색하는 경우), 대신 섹션 7의 Entities.GetWorldItemInner를 사용하세요.

게임 내 스타일 모드 텍스트 + 기본 / 집계 스탯 (v6, 2026-06-24). 스탯 키를 게임 내 툴팁에 표시되는 것과 동일한 텍스트로 포맷하고, 아이템의 기본 방어 수치와 집계된 지도/결계석 속성을 읽습니다.

// 게임이 표시하는 방식으로 모드를 렌더링합니다 ("19% increased Monster Damage").
for (const auto& m : im.ExplicitMods) {
    std::string text = ctx()->Inventory.FormatStat(m.StatKey, m.Value0, m.Value1);
    if (!text.empty()) { /* `text`를 그립니다 */ }
}

// 아이템 기본 방어 수치. EnergyShield는 게임 내 (계산된) 수치이며;
// Ward/Armour/Evasion은 아이템의 기본 수치입니다. Armour 컴포넌트가 없는 아이템
// (통화, 젬, 장신구, 결계석 등)에서는 Valid == false입니다.
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// 스탯 id로 키가 지정된 집계 스탯 — 예: 결계석의 아이템 희귀도 (8205),
// 무리 크기 (8206), 몬스터 희귀도 (8207), 몬스터 효과도 (8208),
// 결계석 드롭 확률 (8209).
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // statId를 라벨로 직접 매핑하세요; 값은 부호 있는 백분율입니다
}

FormatStat은 호스트의 .csd 스탯 설명 세트를 사용합니다(처음 사용 시 다운로드됨). 해당 데이터가 준비되기 전까지는 빈 문자열을 반환하므로 — 원시 Mod 필드로 폴백하세요. ReadItemBaseStats / ReadItemAggregatedStats는 모두 인벤토리 아이템 주소와 WorldItem 컨테이너 주소를 받습니다.


9. UI 트리

게임의 UI 트리는 uintptr_t 요소 주소로 노출됩니다. 루트에서 시작하여 자식을 순회하고 요소 필드를 읽습니다.

StringId로 알려진 패널을 찾는 깔끔한 방법:

uintptr_t gameUiRoot = ctx()->Ui.GetGameUiRoot();
uintptr_t invPanel   = ctx()->Ui.FindPanelByStringId(gameUiRoot, "Inventory");
if (invPanel && ctx()->Ui.IsVisible(invPanel)) {
    // panel is on-screen
}

StringId를 모를 때의 수동 트리 순회:

uintptr_t root = ctx()->Ui.GetUiRoot();
PluginSDK::UiElement e = ctx()->Ui.Read(root);
ctx()->Log.Info(("children=" + std::to_string(e.ChildCount)).c_str());

for (uintptr_t child : ctx()->Ui.GetChildren(root)) {
    std::string sid = ctx()->Ui.GetStringId(child);
    if (sid == "InventoriesPanel") { /* found it */ }
}

// Or use a known index path:
int path[] = { 5, 1, 2, 0 };
uintptr_t logInButton = ctx()->Ui.FollowPath(root, path, 4);

// Compute screen-space rect (post-scale, post-transform):
float x, y, w, h;
if (ctx()->Ui.ComputeScreenRect(invPanel, x, y, w, h)) {
    // draw an overlay box at (x,y,w,h)
}

// Get displayed text:
std::string label = ctx()->Ui.GetText(child);

int cull = ctx()->Ui.GetCullValue();  // host's UI cull threshold

StringId 값은 게임 측에서 안정적인 식별자이며, 존재한다면 하드코딩된 경로보다 선호하세요.


10. 렌더링 및 투영

세 가지 투영 헬퍼, 두 가지 좌표계.

원근 (3D world → screen) — 게임이 월드에 사물을 그리는 데 사용하는 동일한 투영. 이름표, 디버그 마커, 타겟 표시기에 적합합니다.

float sx, sy;
if (ctx()->Render.WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, sx, sy)) {
    ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, IM_COL32(255,0,0,255));
}

아이소메트릭 (grid → minimap) — 대형 또는 미니맵에 그려지는 레이더 스타일 오버레이용. 표시되는 맵의 줌, 팬, 회전을 존중합니다.

if (!snap.LargeMap.IsVisible) return;
for (const auto& e : snap.Entities) {
    float sx, sy;
    if (ctx()->Render.GridToLargeMap(e.GridPositionX, e.GridPositionY, e.TerrainHeight, sx, sy)) {
        ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, color);
    }
}
// Mirror: ctx()->Render.GridToMiniMap(gx, gy, worldZ, sx, sy)

일괄 수학을 위해(엔티티당 함수 호출 생략) 트랜스폼을 한 번 가져와 인라인으로 투영합니다.

PluginSDK::MapTransform t = ctx()->Render.GetLargeMapTransform();
if (t.IsVisible) {
    // dx = gx - t.PlayerGridX; dy = gy - t.PlayerGridY;
    // sx = t.CenterX + (dx - dy) * t.ScaleX;
    // sy = t.CenterY + (worldZ * worldToGrid - (dx + dy)) * t.ScaleY;
}
// And ctx()->Render.GetMiniMapTransform() for the minimap.

이 호출들로만 구축된 레이더의 작동 예시는 Plugins/Radar/src/Radar.cpp를 참고하세요.


11. 지형 및 보행 가능 그리드

보행 가능 그리드는 플레이어가 밟을 수 있는 지형 셀을 나타내는 타일당 4비트 비트맵입니다. 호스트는 각 영역 변경 시 이를 업데이트하며, 플러그인은 (RAII를 통해) 플러그인이 해제할 때까지 유지되는 안정적인 핸들을 받습니다.

PluginSDK::WalkableGridHandle h = ctx()->Terrain.GetWalkableGrid();
if (h.Valid()) {
    const uint8_t* data       = h.Data();
    const int      w          = h.Width();
    const int      height     = h.Height();
    const size_t   sizeBytes  = h.SizeBytes();   // (w * height) / 2
    // POE2 packs two cells per byte:
    //   gx & 1 == 0  →  low nibble  (data[gy * (w/2) + gx/2] & 0x0F)
    //   gx & 1 == 1  →  high nibble ((data[gy * (w/2) + gx/2] >> 4) & 0x0F)
    // Non-zero nibble = walkable.
    //
    // Always bound your byte index against sizeBytes — the host enforces an
    // atomic snapshot, but defense-in-depth has caught at least one real bug.
}
// h is RAII — destructor releases the host reference automatically.

HeightGridHandle은 동일한 형태를 따르지만 타일당 하나의 float를 보유합니다(Data()const float*이며, ElementCount()SizeBytes()가 추가됨).

핸들을 새로 고치기 위해 OnAreaChange를 구독하지 마세요. 이벤트는 호스트 워커가 영역 변경을 감지할 때 발생하지만, 새 보행 가능 그리드가 아직 파싱되지 않았을 수 있습니다 — 한두 프레임 동안 오래된 포인터를 보유하게 됩니다. 대신 DrawUI에서 프레임마다 폴링하세요.

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

저렴한 작업입니다(ABI 호출 1개 + 포인터 비교 1개). 프로덕션 버전은 Plugins/Radar/src/Radar.cpp를 참고하세요.

기타 지형 접근자:

bool  ok        = ctx()->Terrain.IsWalkable(gx, gy);
float worldZ    = ctx()->Terrain.GetTerrainHeight(gx, gy);
float worldToG  = ctx()->Terrain.GetWorldToGridConvertor();

ctx()->Terrain.EnumerateTgtLocations([](const PluginSDK::TgtLocation& loc) {
    // loc.Path, loc.TileX, loc.TileY, loc.X, loc.Y
    return true;  // continue
});

12. 이벤트

호스트가 발생시키는 이벤트를 구독합니다. 각 Subscribe는 나중에 Unsubscribe로 전달할 수 있는 Token을 반환합니다. EventsService 소멸자(플러그인이 비활성화되거나 언로드될 때 호출됨)는 미해제 상태로 남은 모든 것을 자동으로 해제합니다 — 따라서 수동으로 구독 해제할 필요는 엄밀히 없지만, 그렇게 하는 것이 예의입니다.

class MyPlugin : public PluginSDK::Plugin {
    PluginSDK::EventsService::Token m_areaTok{};
    PluginSDK::EventsService::Token m_frameTok{};

public:
    void OnEnable(bool) override {
        auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);

        m_areaTok = ev.OnAreaChange([this]{
            ctx()->Log.Info("area changed");
        });

        m_frameTok = ev.OnFrame([this]{
            // called every frame; keep work cheap
        });

        ev.OnGameAttached([this]{ ctx()->Log.Info("game attached"); });
        ev.OnGameDetached([this]{ ctx()->Log.Info("game detached"); });
    }

    void OnDisable() override {
        auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);
        ev.Unsubscribe(m_areaTok);
        ev.Unsubscribe(m_frameTok);
    }
};

네 가지 이벤트 종류는 AreaChange, Frame, GameAttached, GameDetached입니다. 디스패치 테이블을 구축하고 싶다면 일반 Subscribe(EventKind, callback)도 있습니다.

const_cast가 필요한 이유는 Events가 내부 토큰 맵을 변경하기 때문입니다. 베이스 클래스는 다른 서비스를 실수로 변경하지 못하도록 const Context*를 반환합니다.

구독 그룹화

플러그인이 여러 구독을 소유한다면, ExamplePlugin 패턴은 활성화/비활성화 대칭을 깔끔하게 유지하는 좋은 방법입니다 — 토큰과 카운터를 단일 상태 구조체로 묶고 모든 것을 하나의 SubscribeAll/UnsubscribeAll 쌍을 통해 라우팅하세요.

struct EventsDemoState {
    std::atomic<int> frameCount{0}, areaChangeCount{0};
    PluginSDK::EventsService::Token frameTok{}, areaTok{};
    bool subscribed = false;
};

void SubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
    auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
    s.frameTok = ev.OnFrame      ([&s]{ s.frameCount.fetch_add(1); });
    s.areaTok  = ev.OnAreaChange ([&s]{ s.areaChangeCount.fetch_add(1); });
    s.subscribed = true;
}

void UnsubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
    auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
    ev.Unsubscribe(s.frameTok);
    ev.Unsubscribe(s.areaTok);
    s.frameTok = {}; s.areaTok = {};
    s.subscribed = false;
}

전체 패턴은 Plugins/ExamplePlugin/examples/ExampleEvents.h를 참고하세요.


13. 설정 영구 저장

규칙: <plugin directory>/config/settings.json. Directory()는 플러그인 폴더의 절대 UTF-8 경로를 반환합니다.

사소한 설정의 경우, 직접 작성한 JSON writer가 잘 작동하며 DLL을 자체 완결적으로 유지합니다. 작동 예시는 Plugins/Radar/src/RadarSettings.h를 참고하세요. 스켈레톤:

struct MySettings {
    bool  DrawEnabled = true;
    float Opacity     = 0.9f;

    void Save(const std::string& directory) const {
        std::filesystem::path p =
            std::filesystem::path(directory) / "config" / "settings.json";
        std::error_code ec;
        std::filesystem::create_directories(p.parent_path(), ec);
        std::ofstream out(p);
        if (!out.is_open()) return;
        out << "{\n";
        out << "  \"DrawEnabled\":" << (DrawEnabled ? "true" : "false") << ",\n";
        out << "  \"Opacity\":"     << Opacity << "\n";
        out << "}\n";
    }

    void Load(const std::string& directory) {
        std::filesystem::path p =
            std::filesystem::path(directory) / "config" / "settings.json";
        if (!std::filesystem::exists(p)) return;
        // ... parse ...
    }
};

// In your plugin:
void OnEnable(bool) override   { m_settings.Load(Directory()); }
void SaveSettings() override   { m_settings.Save(Directory()); }

구조화된 데이터(중첩 객체, 배열)의 경우 플러그인 폴더에 실제 JSON 라이브러리를 벤더링하세요. 호스트는 선택을 강제하지 않습니다.

SaveSettings는 주기적(~5초) 및 비활성화 시 호출됩니다; 직접 호출할 필요는 없습니다.


14. 로깅

ctx()->Log.Debug("verbose detail");
ctx()->Log.Info ("normal status");
ctx()->Log.Warn ("something unexpected");
ctx()->Log.Error("operation failed");
ctx()->Log.Log  ("custom-level", "message");

네 가지 레벨 모두 호스트의 중앙 로거로 라우팅됩니다. 메시지는 호스트의 Logs 탭과 디스크의 로그 파일에 표시됩니다. 호출 전에 직접 포맷하세요; 호스트는 printf 스타일 가변 인자를 받지 않습니다.

내부적으로 편의 메서드는 "Debug", "Info", "Warning", "Error" 문자열을 내보냅니다(Warn"Warning"에 매핑됨). 호스트 브리지는 대소문자를 구분하지 않고 매칭하므로 Log("warn", "msg")를 호출하는 플러그인도 올바르게 라우팅됩니다 — 하지만 편의 메서드가 더 명확합니다.


15. 메모리 (파워 유저용)

직접 메모리 프리미티브입니다. 가능하면 상위 레벨 서비스를 우선 사용하세요 — 이들은 오프셋을 이해하고, ABI 변경을 처리하며, SEH 안전합니다. 직접 메모리 읽기는 필요한 것에 대한 상위 레벨 호출이 없을 때만 적절합니다.

// Read a fixed-size value:
uint64_t value = 0;
ctx()->Memory.Read(addr, &value, sizeof(value));

// Read game strings (null-terminated, narrow or wide):
std::string  s  = ctx()->Memory.ReadString (strAddr);
std::wstring ws = ctx()->Memory.ReadWString(wstrAddr);

// Read a std::wstring container in the game's memory (handles SSO):
std::wstring inner = ctx()->Memory.ReadStdWString(containerAddr);

// Read a std::vector<T>; returns raw bytes you reinterpret_cast:
std::vector<uint8_t> raw = ctx()->Memory.ReadStdVector(vecAddr, sizeof(MyT), /*maxElems=*/1024);
const MyT* items = reinterpret_cast<const MyT*>(raw.data());
size_t count = raw.size() / sizeof(MyT);

// Module info:
uintptr_t base = ctx()->Memory.GetBaseAddress();
uintptr_t sz   = ctx()->Memory.GetModuleSize();
uintptr_t pat  = ctx()->Memory.GetPatternAddress("GameStates");  // resolves a named pattern

이들에 자주 손을 뻗는 자신을 발견한다면, 필요한 데이터가 상위 레벨 서비스에 속해야 하는지 자문하세요.


16. 브리지 / SEH 안전성

호스트와 플러그인 간의 모든 DLL 간 호출은 호스트 측에서 __try / __except 블록 내에서 실행됩니다. SDK 호출 내에서 오래된 포인터를 역참조하거나, 0으로 나누거나, 그 외 다른 방식으로 폴트를 일으키는 잘못된 플러그인은 로그된 오류를 받습니다 — 호스트 프로세스는 충돌하지 않으며, 게임은 계속 실행되고, 사용자는 다른 플러그인을 계속 사용할 수 있습니다.

그렇다고 플러그인이 부주의해도 된다는 의미는 아닙니다. SEH는 증상을 잡지, 원인을 잡지 않습니다. 플러그인이 매 프레임 폴트를 일으키면 사용자는 오류 로그의 홍수를 보게 되고, 데이터는 사실상 사용 불가능합니다. SDK 호출에서 null 반환을 처리하고, 컴포넌트 데이터의 Valid 플래그를 확인하고, uintptr_t 주소를 직접 역참조하지 마세요 — 이미 RPM을 적절히 감싸는 ComponentsService / Ui / Memory 호출을 통해 전달하세요.

호스트는 버그가 있는 플러그인은 다룰 수 있습니다. 하지만 멈춘 플러그인 DLL은 다룰 수 없습니다 — 100ms가 걸리는 DrawSettings는 전체 UI 스레드를 차단합니다. 프레임당 작업을 가볍게 유지하세요.


17. 흔한 함정

플러그인 작성자가 처음 통합할 때 부딪히는 짧은 목록입니다. 대부분 위에서 인라인으로 문서화되어 있지만, 여기에 체크리스트로 모았습니다.

  1. OnAreaChange는 보행 가능 그리드가 재파싱되기 전에 발생합니다. 이벤트에서 WalkableGridHandle을 새로 고치지 마세요 — DrawUI에서 프레임마다 폴링하고 Data()가 변경되면 교체하세요. (§11)
  2. Entity::Zone은 로컬 플레이어에 대해 항상 None입니다. 이는 플레이어로부터의 거리 분류이므로, 정의상 플레이어는 거리 0에 있습니다. 플레이어 정보 표시에 노출하지 마세요.
  3. Components.ReadMods()는 요약 플래그만 반환합니다 — 모드 목록 없음. 종류별 모드 목록은 Inventory.ReadItemMods(entityAddr)를 호출하세요. (§8)
  4. 땅에 드롭된 아이템은 EntitySubtype이 없을 수 있습니다. 월드의 아이템을 필터링한다면, 더 좁은 subtype 체크보다 EntityType == Item || EntityType == Chest를 선호하세요.
  5. Directory()는 절대 경로를 반환합니다. EXE 디렉터리를 직접 앞에 붙이지 마세요 — EXEDIR\EXEDIR\Plugins\X가 되어 설정 파일이 플러그인 폴더 밖에 저장됩니다.
  6. ctx()const Context*를 반환합니다. EventsService::Subscribe 같은 변경 메서드는 const_cast가 필요합니다. 이는 의도적입니다 — 변경하지 않는 서비스는 실수로 변경할 수 없어야 합니다.
  7. ImGui::SetCurrentContext는 DLL별로 호출해야 합니다. 플러그인 DLL은 기본적으로 자체 ImGui 상태를 가지므로, 그리기를 하는 모든 진입점(OnEnable, DrawUI, DrawSettings)에서 호출하세요.
  8. 편의 헬퍼는 호출할 때마다 컴포넌트를 다시 읽습니다. GetHealthPercent(addr)는 내부적으로 새로운 ReadLife(addr)를 수행합니다. 이전 호출에서 이미 Life 구조체를 가지고 있다면, 그 필드에 직접 접근하세요.

18. 서비스 빠른 참조

모든 서비스의 모든 공개 메서드에 대한 한 줄 요약입니다. 전체 타입 시그니처와 사용 노트는 위의 산문 섹션을 참고하세요.

GameService

메서드 반환 목적
GetSnapshot() Snapshot Full per-frame view, including Entities
GetState() GameState Enum: InGame, Login, Loading, …
IsAttached() bool Game process attached
IsInGame() bool State == InGame
IsForeground() bool Game window has focus
IsMenuVisible() bool ESC menu / settings open
IsOverlayMode() bool Host is in overlay (click-through)
GetProcessId() DWORD Game PID
GetGameWindow() HWND Game window handle
GetScreenSize() ScreenSize {Width, Height} floats

EntitiesService

메서드 반환 목적
Enumerate(cb) Visit every nearby entity (return false to stop)
GetPlayer() Entity The local player entity
FindById(id) std::optional<Entity> Lookup by entity id
GetWorldItemInner(addr) std::optional<Entity> Inner item entity for a WorldItem container (ground items)
Watch(id) Pin an entity so its components stay readable
Unwatch(id) Release a watch
IsWatched(id) bool Watch state
GetWatchedComponents(id) std::optional<ComponentAddresses> Read pinned components

ComponentsService

메서드 반환 목적
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime Component struct 21 readers, one per component type
EnumerateBuffs(addr) std::vector<Buff> Active buffs on the entity
EnumerateActiveSkills(addr) std::vector<ActiveSkill> Skills from an Actor component
EnumerateStats(addr) std::vector<StatEntry> Items + buffs sourced stats
EnumerateItemMods(addr) std::vector<Mod> Mods reachable from a Mods component
GetHealthPercent / GetEsPercent / GetManaPercent float Convenience % helpers
IsAlive(addr) bool Health > 0
GetItemRarity(addr) int Rarity from a Mods component
IsItemIdentified(addr) bool Identified flag
GetStackCount(addr) int Current stack count
IsChestOpened(addr) bool Chest open flag
GetPlayerName(addr) std::string Player name from a Player component
GetWorldPosition(renderAddr, x, y, z) bool Convenience accessor for world coords

InventoryService

메서드 반환 목적
Scan(inventoryId) Trigger a host-side rescan (-1 = all)
Get(inventoryId) Inventory One inventory, items already populated
GetItems(inventoryId) std::vector<InventoryItem> Items only
GetAll() std::vector<Inventory> All scanned inventories
GetName(inventoryId) const char* Display name ("Backpack", "Stash", …)
ReadItemRarity(addr) int Per-entity rarity (auto-resolves WorldItem containers)
ReadItemStackCount(addr) int Per-entity stack (auto-resolves WorldItem containers)
ReadItemBaseTypeName(addr) std::string Base type, auto-resolves WorldItem containers
ReadItemUniqueName(addr) std::string Unique name, auto-resolves WorldItem containers
ReadItemPath(addr) std::string Metadata/Items/... path, auto-resolves WorldItem containers
ReadItemMods(addr) ItemMods Summary flags + 5 per-kind mod vectors, auto-resolves WorldItem containers
FormatStat(statKey, v0, v1) std::string 호스트 .csd 포매터를 통해 스탯 키 + 값(들)의 게임 내 스타일 텍스트로 변환; 설명이 로드되기 전까지는 빈 문자열 반환
ReadItemBaseStats(addr) ItemBaseStats 기본 방어 수치 (계산된 에너지 실드; 기본 Ward/Armour/Evasion); Armour 컴포넌트 없이는 Valid false; WorldItem 컨테이너 자동 해결
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> 집계된 {statId, value} (결계석 아이템 희귀도 8205 / 무리 크기 8206 / 몬스터 희귀도 8207 / 몬스터 효과도 8208 / 결계석 드롭 확률 8209); WorldItem 컨테이너 자동 해결

UiService

메서드 반환 목적
Read(addr) UiElement Element fields (rect, flags, children count)
GetChildren(addr) std::vector<uintptr_t> Child element addresses
GetChildAt(addr, index) uintptr_t Single child by index
FollowPath(root, indices, count) uintptr_t Walk a known index path
IsVisible(addr) bool Element is on-screen
GetStringId(addr) std::string Game-side stable identifier
GetText(addr) std::string Rendered text
ComputeScreenRect(addr, x, y, w, h) bool Final screen-space rect
GetGameUiRoot() uintptr_t Root of the in-game UI
GetUiRoot() uintptr_t Top-level UI root
GetCullValue() int Host's UI cull threshold
FindPanelByStringId(parent, stringId) uintptr_t Targeted descendant lookup

RenderService

메서드 반환 목적
WorldToScreen(wx, wy, wz, sx, sy) bool Perspective projection
GridToLargeMap(gx, gy, worldZ, sx, sy) bool Project to the large map overlay
GridToMiniMap(gx, gy, worldZ, sx, sy) bool Project to the minimap
GetLargeMapTransform() MapTransform Pre-multiplied transform for batched math
GetMiniMapTransform() MapTransform Same, for the minimap

TerrainService

메서드 반환 목적
GetWalkableGrid() WalkableGridHandle RAII handle to the 4-bit-per-tile walkable bitmap
GetHeightGrid() HeightGridHandle RAII handle to the per-tile terrain heights
IsWalkable(gx, gy) bool Single-tile predicate
GetTerrainHeight(gx, gy) float World-space Z
GetWorldToGridConvertor() float World → grid conversion factor
EnumerateTgtLocations(cb) Visit every TGT instance in the current area

MemoryService

메서드 반환 목적
Read(addr, buf, size) bool Raw RPM
ReadString(addr) std::string Null-terminated narrow string
ReadWString(addr) std::wstring Null-terminated wide string
ReadStdWString(addr) std::wstring Reads a game-side std::wstring container (handles SSO)
ReadStdVector(addr, elemSize, maxElems) std::vector<uint8_t> Raw bytes; reinterpret as your type
GetBaseAddress() uintptr_t Game module base
GetModuleSize() uintptr_t Game module size
GetPatternAddress(name) uintptr_t Named pattern lookup

LogService

메서드 목적
Debug / Info / Warn / Error(msg) Emit at the corresponding level
Log(level, msg) Custom level string

EventsService

메서드 반환 목적
Subscribe(kind, cb) Token Generic dispatch
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token One-line subscribe helpers
Unsubscribe(token) Manual release (destructor auto-releases anyway)

19. 버전 관리

PluginAbi.h는 다음을 정의합니다.

constexpr int PLUGIN_SDK_VERSION = 6;

로드 시 호스트는 plugin->GetSDKVersion()을 호출하여 자신의 PLUGIN_SDK_VERSION과 비교합니다. 불일치 시 호스트는 경고를 로그하고 플러그인 로드를 거부합니다.

호스트는 또한 PluginSDK_AttachHost 내부에서 HostAbi::versionHostAbi::size_bytes를 확인합니다(PLUGIN_EXPORTS가 설정된 경우 PluginSDK.h에서 인라인으로 정의됨). 두 필드 중 하나라도 플러그인이 빌드된 것과 일치하지 않으면, ctx()는 동작하지 않습니다. 베이스 클래스 접근자 HostCompatible()은 이 경우 false를 반환하며, 예의 바른 플러그인이라면 동작을 거부해야 합니다.

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

20. 샘플 플러그인

저장소의 네 가지 플러그인은 문서처럼 읽을 수 있도록 설계되었습니다.

  • Plugins/ExamplePlugin/ — 광범위 표면 쇼케이스. 거의 모든 서비스를 다루는 단일 플러그인으로, 11개의 examples/Example*.h 하위 파일(Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log)과 커버리지 요약 배너로 구성되어 있습니다. 서비스가 문맥 속에서 어떻게 사용되는지 보고 싶을 때 읽어보세요.

  • Plugins/Radar/ — 집중된 실제 예시. ~200줄. 공개 SDK만으로 구축된 레이더 오버레이 — 오프셋 없음, 원시 메모리 읽기 없음. Render.GridToLargeMap을 통해 보행 가능 맵과 엔티티별 점을 렌더링합니다. 특정 결과를 위한 최소 코드를 보고 싶을 때 읽어보세요.

  • Plugins/KillCount/ — 처치/상자/사망 추적기. SQLite + 스프라이트 아틀라스 + 영역별 상태. 영구 저장, 벤더링된 데이터 파일, 오버레이를 모두 하나의 DLL에 담는 방법을 보여줍니다.

  • Plugins/NinjaPricer/ — poe.ninja 가격 오버레이. HTTP fetch (Exchange API) + 인벤토리 스캔 + 아이템별 가격. 실제 워크플로우에서 네트워크 코드, 서드 파티 데이터 수집, 인벤토리 순회를 보여줍니다.


← Home

Clone this wiki locally