Skip to content

Plugin Development Guide TH

Lafko edited this page Apr 9, 2026 · 15 revisions

← Home


คู่มือการพัฒนาปลั๊กอิน

1. เริ่มต้นใช้งาน

ข้อกำหนดเบื้องต้น

  • MSVC v143 (Visual Studio 2022)
  • C++20 (/std:c++20)
  • บิลด์ x64 Release
  • Runtime Library: /MD (Multi-threaded DLL) -- ต้องตรงกับ host

การตั้งค่าโปรเจกต์

  1. สร้างโปรเจกต์ C++ DLL ใหม่ใน Visual Studio
  2. ตั้งค่า include path ให้ชี้ไปยังไดเรกทอรีซอร์สของ POEFixer (สำหรับ SDK header และ ImGui header)
  3. เพิ่มไฟล์ซอร์ส ImGui เข้าในโปรเจกต์ของคุณ: imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp
  4. รวม Plugin SDK header ในซอร์สของปลั๊กอิน:
    #include "plugin_sdk/PluginAPI.h"
    #include "plugin_sdk/PluginContext.h"
    #include "imgui/imgui.h"
    หรือใช้ header สะดวกจาก ExamplePlugin:
    #include "sdk/PluginHelpers.h"  // Includes all SDK headers + MemoryReader + utilities
  5. กำหนด PLUGIN_EXPORTS และ _CRT_SECURE_NO_WARNINGS ใน Preprocessor Definitions ของโปรเจกต์

โครงสร้างโฟลเดอร์

Plugins/
  YourPlugin/
    YourPlugin.dll      <-- ชื่อ DLL ต้องตรงกับชื่อโฟลเดอร์
    config/
      settings.txt      <-- ไฟล์การตั้งค่าเสริม
    data/
      ...               <-- ไดเรกทอรีข้อมูลเสริม (ฐานข้อมูล, แคช, ฯลฯ)

โครงสร้างโปรเจกต์ ExamplePlugin

ExamplePlugin แสดงเลย์เอาต์โปรเจกต์ที่แนะนำ:

Plugins/ExamplePlugin/
  ExamplePlugin.cpp        <-- จุดเข้าหลักของปลั๊กอิน + ฟังก์ชัน factory ที่ export
  sdk/
    PluginHelpers.h        <-- MemoryReader, WideToNarrow, ตัวช่วย entity/rarity
  examples/
    ExampleBuffs.h         <-- รายการ buff พร้อมการกรองและแถบความคืบหน้า
    ExampleEntities.h      <-- รายการ debug ของ entity พร้อมกลไกการเฝ้าดู, ต้นไม้คอมโพเนนต์, JSON dump
    ExampleInventory.h     <-- ServerData, ตัวเลือกคลัง, ตารางช่อง, mod ของไอเท็มพร้อมระดับความหายาก
    ExampleMemory.h        <-- ตัวแสดงฐานสิบหก, สาธิต Read<T>, สแกนเนอร์แพตเทิร์น
    ExampleUiExplorer.h    <-- ตัวสำรวจ UI element แบบเต็มรูปแบบพร้อมการค้นหา, การนำทาง, การเน้น

โครงสร้างปลั๊กอิน KillCount

ตัวอย่างปลั๊กอินที่สมบูรณ์กว่าพร้อม SQLite3, atlas ไอคอน และการเรนเดอร์ overlay:

Plugins/KillCount/
  KillCount.cpp            <-- จุดเข้าหลักของปลั๊กอิน, วงจรชีวิต IPlugin, UI การตั้งค่า
  KillCount.h              <-- การประกาศคลาสของปลั๊กอิน
  KillTracker.cpp/h        <-- เอ็นจิ้นนับ kill/หีบ/การตาย
  OverlayRenderer.cpp/h    <-- ImGui overlay พร้อมรูปแบบลากเพื่อจัดตำแหน่ง
  IconAtlas.cpp/h           <-- โหลด sprite sheet เท็กซ์เจอร์ (D3D11 + stb_image)
  Database.cpp/h           <-- Wrapper SQLite3 สำหรับสถิติถาวร
  DisplaySettings.h        <-- โครงสร้างการตั้งค่า
  sdk/
    PluginHelpers.h        <-- คัดลอกจาก ExamplePlugin
  lib/
    sqlite3.c/h            <-- SQLite3 amalgamation (คอมไพล์เป็น C)
    sqlite3-vcpkg-config.h <-- override ท้องถิ่นสำหรับ static linking

หลักการตั้งชื่อ

ชื่อไฟล์ DLL ต้องตรงกับชื่อโฟลเดอร์อย่างแน่นอน:

  • โฟลเดอร์: Plugins/MyPlugin/ -> DLL: MyPlugin.dll
  • Host จะสแกนแต่ละโฟลเดอร์ย่อยใน Plugins/ และค้นหา <ชื่อโฟลเดอร์>.dll

2. วงจรชีวิตของปลั๊กอิน

Load DLL (LoadLibrary)
  -> CreatePlugin()           -- Factory: สร้างอินสแตนซ์ IPlugin ของคุณ
  -> SetContext(ctx)           -- รับบริการจาก host
  -> SetPluginDirectory(dir)   -- รับพาธโฟลเดอร์ของคุณ
  -> GetSDKVersion()           -- ตรวจสอบความเข้ากันได้
  -> GetName()                 -- ชื่อแสดงผลสำหรับ UI
  -> [if enabled] OnEnable()  -- เริ่มต้นทรัพยากร
  |
  ลูปหลัก (ทุกเฟรม):
    -> DrawUI()                -- เรนเดอร์ overlay ของคุณ (เมื่อเปิดใช้งานเท่านั้น)
    -> DrawSettings()          -- เรนเดอร์การตั้งค่าในแท็บ Plugins
    -> WantsOverlay()          -- Host ตรวจสอบว่าปลั๊กอินต้องการโหมด overlay หรือไม่
  |
  เป็นระยะ / เมื่อปิด:
    -> SaveSettings()          -- บันทึกการตั้งค่าของคุณ
  |
  -> OnDisable()               -- ทำความสะอาดทรัพยากร
  -> DestroyPlugin(plugin)     -- Factory: ลบ IPlugin ของคุณ
  -> FreeLibrary               -- ยกเลิกการโหลด DLL

เธรด

  • เมธอด Draw* ทั้งหมดถูกเรียกบนเธรดหลัก/เรนเดอร์
  • GetSnapshot() และฟังก์ชัน PluginContext อื่น ๆ เป็น thread-safe
  • อย่าสร้างเธรดที่เรียก ImGui -- ImGui ไม่ใช่ thread-safe

3. ข้อมูลอ้างอิง IPlugin Interface

ปลั๊กอินทุกตัวต้อง implement interface IPlugin (กำหนดใน plugin_sdk/PluginAPI.h):

void SetPluginDirectory(const char* dir)

  • เมื่อถูกเรียก: ครั้งเดียว ทันทีหลังการสร้าง
  • พารามิเตอร์: พาธสัมพัทธ์เช่น "Plugins/YourPlugin"
  • วัตถุประสงค์: เก็บพาธนี้สำหรับโหลดการตั้งค่า/ทรัพยากร

void SetContext(PluginContext* context)

  • เมื่อถูกเรียก: ครั้งเดียว หลังจาก SetPluginDirectory
  • พารามิเตอร์: ตัวชี้ไปยัง PluginContext ของ host (ใช้ได้ตลอดอายุปลั๊กอิน)
  • วัตถุประสงค์: เก็บตัวชี้นี้ -- มันเป็นประตูสู่ข้อมูลเกมทั้งหมดของคุณ
  • สำคัญ: เรียก ImGui::SetCurrentContext(ctx->ImGuiContext) ที่นี่

void OnEnable(bool isGameOpened)

  • เมื่อถูกเรียก: เมื่อผู้ใช้เปิดใช้งานปลั๊กอิน หรือเมื่อเริ่มต้นถ้าเปิดใช้งานก่อนหน้านี้
  • พารามิเตอร์: true ถ้าโปรเซสเกมเชื่อมต่ออยู่ในขณะนี้
  • วัตถุประสงค์: โหลดการตั้งค่า, จัดสรรทรัพยากร, เริ่มต้นสถานะ

void OnDisable()

  • เมื่อถูกเรียก: เมื่อผู้ใช้ปิดใช้งานปลั๊กอิน
  • วัตถุประสงค์: ปล่อยทรัพยากร, หยุดงานพื้นหลัง

void DrawUI()

  • เมื่อถูกเรียก: ทุกเฟรม เมื่อปลั๊กอินเปิดใช้งานเท่านั้น
  • วัตถุประสงค์: เรนเดอร์ overlay ของคุณโดยใช้ ImGui
  • หมายเหตุ: ใช้ ID หน้าต่างที่ไม่ซ้ำกันเช่น "MyWindow##MyPlugin" เพื่อหลีกเลี่ยงการชน

void DrawSettings()

  • เมื่อถูกเรียก: ทุกเฟรม ในแท็บการตั้งค่า Plugins (เมื่อเปิดใช้งานเท่านั้น)
  • วัตถุประสงค์: เรนเดอร์การกำหนดค่าปลั๊กอินโดยใช้ ImGui

void SaveSettings()

  • เมื่อถูกเรียก: เป็นระยะและเมื่อปิดแอปพลิเคชัน
  • วัตถุประสงค์: บันทึกการตั้งค่าลงดิสก์ (เช่น Plugins/YourPlugin/config/settings.txt)

const char* GetName()

  • คืนค่า: ชื่อที่แสดงในแท็บ Plugins (เช่น "My Plugin")

int GetSDKVersion()

  • คืนค่า: PLUGIN_SDK_VERSION (ปัจจุบันคือ 4)
  • วัตถุประสงค์: Host ตรวจสอบค่านี้สำหรับความเข้ากันได้ -- ต้องตรงกัน

bool WantsOverlay() (SDK v2)

  • คืนค่า: true ถ้าปลั๊กอินต้องการเรนเดอร์ในโหมด overlay (overlay โปร่งใสทับเกม)
  • ค่าเริ่มต้น: false -- ปลั๊กอินเรนเดอร์เฉพาะในหน้าต่างการตั้งค่าปกติ
  • วัตถุประสงค์: เมื่อปลั๊กอินใดคืนค่า true host จะเข้าโหมด overlay แม้ว่าจะไม่มีฟีเจอร์ในตัวที่ต้องการ

การ export ของ Factory

DLL ของคุณต้อง export ฟังก์ชัน C สองตัวนี้:

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

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

4. ข้อมูลอ้างอิง API PluginContext

struct PluginContext (กำหนดใน plugin_sdk/PluginContext.h) ให้ function pointer สำหรับเข้าถึงข้อมูลเกม ทุกประเภทอยู่ใน namespace PluginSDK

การเข้าถึงข้อมูลเกม

GetSnapshot() -> shared_ptr<const PluginGameSnapshot>

คืนค่า snapshot สมบูรณ์ของสถานะเกม อัปเดตหนึ่งครั้งต่อเฟรม ประกอบด้วย:

ฟิลด์ ประเภท คำอธิบาย
CurrentState GameStateTypes สถานะปัจจุบันของเกม
CurrentAreaName string ชื่อพื้นที่ (เช่น "The Riverways")
CurrentAreaHash string แฮชเฉพาะของอินสแตนซ์พื้นที่
CurrentAreaLevel uint8_t ระดับมอนสเตอร์ของพื้นที่ปัจจุบัน
IsTown bool จริงถ้าอยู่ในเมือง
IsHideout bool จริงถ้าอยู่ในที่ซ่อน
IsPaused bool จริงถ้าเกมหยุดชั่วคราว
IsSkillTreeVisible bool จริงถ้าแผงต้นไม้ทักษะเปิดอยู่
WorldToGridConvertor float ตัวแปลงค่าโลก->กริด
Player RadarEntity ข้อมูล entity ของผู้เล่นท้องถิ่น
Entities vector<RadarEntity> entity ใกล้เคียงทั้งหมด
LargeMap / MiniMap MapData ข้อมูล overlay ของแผนที่
Vitals PlayerVitals HP/ES/MP ของผู้เล่น + buff
ScreenWidth / ScreenHeight int ขนาดหน้าต่างเกม
ProcessId DWORD ID โปรเซสเกม
GameWindow HWND handle หน้าต่างเกม
GameWindowForeground bool จริงถ้าเกมอยู่เบื้องหน้า
IsAttached bool จริงถ้าเชื่อมต่อกับโปรเซสเกมแล้ว
IsWindowValid bool จริงถ้าหน้าต่างเกมถูกต้อง
LastUpdateTime uint64_t timestamp ของการอัปเดตล่าสุด
AreaChangeCounter uint64_t เพิ่มขึ้นเมื่อเปลี่ยนพื้นที่
Inventories vector<InventoryInfo> เนื้อหาคลังของผู้เล่น
CurrencyTotals map<string,int> จำนวนเงินตราตาม path
InventoryGrid InventoryGridInfo ข้อมูลกริดคลัง
WorldToScreenMatrix XMFLOAT4X4 เมทริกซ์ฉาย 3D->2D

สำคัญ: การกรอง Entity Entity ที่ตายแล้ว (ที่มี EntityState == Useless) ถูกกรอง ออก จาก snapshot ก่อนที่ปลั๊กอินจะรับ ซึ่งหมายความว่าคุณจะไม่มีทางสังเกตเห็นการเปลี่ยนแปลง HP จากมีชีวิตเป็นตาย หากคุณต้องการตรวจจับการ kill ให้ใช้การตรวจจับแบบหายไปแทน -- ติดตาม ID entity ตามโซนและนับว่าถูกฆ่าเมื่อหายไปจากรายการ entity ขณะอยู่ในระยะ InnerCircle หรือ OuterCircle ดู ส่วนที่ 8: สูตรทั่วไป สำหรับรายละเอียด

GetPlayerVitals() -> PlayerVitals

ทางลัดสะดวกสำหรับสัญญาณชีพของผู้เล่น

GetCurrentState() -> GameStateTypes

คืนค่า enum สถานะปัจจุบันของเกม

IsAttached() -> bool

จริงถ้าโปรเซสเกมเชื่อมต่อแล้วและอ่านได้

IsInGame() -> bool

จริงถ้าอยู่ในเกมปัจจุบัน (ไม่โหลด, ไม่อยู่หน้าจอเข้าสู่ระบบ)

IsGameForeground() -> bool

จริงถ้าหน้าต่างเกมเป็นหน้าต่างเบื้องหน้า

GetProcessId() -> DWORD

คืนค่า ID โปรเซสเกม

การเข้าถึงข้อมูลไอเท็ม

ReadExtendedItemMods(entityAddress) -> ExtendedItemModInfo

อ่าน mod ทั้งหมดจาก entity ไอเท็ม

ReadItemRarity(entityAddress) -> int

คืนค่า: 0=ปกติ, 1=เวทมนตร์, 2=หายาก, 3=ไม่ซ้ำ

ReadItemStackCount(entityAddress) -> int

คืนค่าจำนวนกองสำหรับไอเท็มเงินตรา/ซ้อนได้

ReadItemName(entityAddress) -> string

คืนค่าชื่อประเภทฐานของไอเท็ม

ReadItemPath(entityAddress) -> string

คืนค่า path เมตาดาต้าของไอเท็ม

ReadItemBaseTypeName(entityAddress) -> string

คืนค่าชื่อประเภทพื้นฐานของไอเท็ม (เช่น "Divine Orb", "Chaos Orb") ต่างจาก ReadItemName ที่คืนค่า path เมตาดาต้า ฟังก์ชันนี้อ่านชื่อประเภทพื้นฐานจริงจาก BaseItemTypeData.BaseTypeName

ReadItemUniqueName(entityAddress) -> string

คืนค่าชื่อไอเท็มไม่ซ้ำจาก Words.dat (เช่น "Headhunter", "Brimstone Call") คืนค่าสตริงว่างสำหรับไอเท็มที่ไม่ใช่ไม่ซ้ำ

โหมด overlay (SDK v2)

IsOverlayMode() -> bool

คืนค่าจริงถ้า host อยู่ในโหมด overlay ปัจจุบัน (overlay โปร่งใสทับหน้าต่างเกม) ใช้สิ่งนี้เพื่อปรับการเรนเดอร์ -- เช่น วาดบน game overlay เทียบกับวาดในหน้าต่างการตั้งค่า

สถานะ UI (SDK v4)

IsMenuVisible() -> bool

คืนค่าจริงเมื่อเมนูการตั้งค่าของ host มองเห็นได้ (overlay เป็นแบบโต้ตอบ) เมื่อเมนูซ่อนอยู่ หน้าต่าง overlay จะโปร่งใสต่อการคลิก (WS_EX_TRANSPARENT) ดังนั้นหน้าต่าง ImGui ไม่สามารถรับ input จากเมาส์ได้

ใช้สิ่งนี้เพื่อ implement รูปแบบ overlay ลากได้:

  • เมนูมองเห็นได้: แสดงตัวจับลาก, อนุญาตการโต้ตอบ (แท็บ, ปุ่ม)
  • เมนูซ่อน: ลบตัวจับลาก, เพิ่ม ImGuiWindowFlags_NoInputs เพื่อทำให้หน้าต่างไม่โต้ตอบ

ดู ส่วนที่ 6: รูปแบบ overlay ลากได้ สำหรับการ implement เต็มรูปแบบ

การอ่านหน่วยความจำ (SDK v2)

เข้าถึงหน่วยความจำโปรเซสเกมโดยตรง การอ่านทั้งหมดปลอดภัย (คืนค่า 0/ว่างเมื่อล้มเหลว)

GetBaseAddress() -> uintptr_t

คืนค่าที่อยู่ฐานของโมดูลเกม คืนค่า 0 ถ้าไม่ได้เชื่อมต่อ

GetModuleSize() -> uintptr_t

คืนค่าขนาดโมดูลเกมเป็นไบต์ คืนค่า 0 ถ้าไม่ได้เชื่อมต่อ

ReadProcessMemory(address, buffer, size) -> bool

อ่านบล็อกไบต์ดิบจากโปรเซสเกม buffer ต้องมีอย่างน้อย size ไบต์ที่จัดสรรไว้ คืนค่าจริงเมื่อสำเร็จ

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

อ่านสตริง ASCII ที่จบด้วย null จากหน่วยความจำเกม (สูงสุด 128 ตัวอักษร)

ReadUnicodeString(address) -> wstring

อ่านสตริง Unicode (wide) ที่จบด้วย 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

แปลงตำแหน่งในพื้นที่โลกเป็นพิกัดหน้าจอ คืนค่าจริงถ้าตำแหน่งมองเห็นได้บนหน้าจอ

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)

ขอให้ host สแกนคลัง ส่ง -1 เพื่อสแกนคลังทั้งหมด หรือ ID คลังเฉพาะ ข้อมูลคลังใน snapshot จะถูกเติมหลังจากสแกนเสร็จ (เฟรมถัดไป)

หมายเหตุ: ข้อมูลคลังไม่ได้รีเฟรชอัตโนมัติ -- คุณต้องเรียกฟังก์ชันนี้เพื่อเริ่มสแกน เรียกเป็นระยะ (เช่น ทุก 2 วินาที) ถ้าคุณต้องการข้อมูลคลังต่อเนื่อง

ข้อมูลภูมิประเทศ (SDK v2)

GetWalkableGrid(outWidth, outHeight) -> const uint8_t*

คืนค่าตัวชี้ไปยังข้อมูลกริดที่เดินได้ กริดเป็นอาร์เรย์ 2 มิติที่ 0 = เดินไม่ได้, ไม่ใช่ศูนย์ = เดินได้ คืนค่า nullptr ถ้าข้อมูลไม่พร้อมใช้งาน

GetTerrainHeight(gridX, gridY) -> float

คืนค่าความสูงภูมิประเทศที่ตำแหน่งกริด คืนค่า 0 ถ้าอยู่นอกขอบเขตหรือข้อมูลไม่พร้อม

การอ่านคอนเทนเนอร์เนทีฟ (SDK v3)

ฟังก์ชันเหล่านี้อ่านคอนเทนเนอร์ของไลบรารีมาตรฐาน C++ โดยตรงจากหน่วยความจำเกม สะท้อนเมธอด Core::Process ของ host

ReadStdVector(containerAddress, elementSize, outCount) -> void*

อ่าน StdVector (struct 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 (struct 16 ไบต์: {Head, Size}) จากหน่วยความจำเกม สำรวจ linked list และคืนค่าบัฟเฟอร์ต่อเนื่อง ผู้เรียกต้อง free()

ReadStdBucket(containerAddress, elementSize, outCount) -> void*

อ่าน StdBucket จากหน่วยความจำเกม (อ่าน StdVector ที่ฝังอยู่) ผู้เรียกต้อง free()

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

สำรวจ StdMap (struct 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 (struct 32 ไบต์พร้อมบัฟเฟอร์ inline/heap) จากหน่วยความจำเกม

GetInventoryName(inventoryId) -> const char*

คืนค่าชื่อที่อ่านได้สำหรับ ID คลัง (เช่น 1 -> "MainInventory1", 3 -> "Weapon1", 64 -> "Currency1")

การเข้าถึงข้อมูลดีบัก (SDK v4)

SDK v4 ให้การเข้าถึงโดยตรงไปยังข้อมูลดีบักของ host -- คอมโพเนนต์ entity, รายละเอียดคลัง และต้นไม้ element UI -- ตรงกับแท็บดีบักในตัว

รายการดีบัก Entity

GetEntityDebugList() -> vector<DebugEntityInfo>

คืนค่ารายการ entity ทั้งหมดพร้อมเมตาดาต้าดีบัก (Id, Address, Path, Type, SubType, State, Rarity, Zone) สิ่งนี้สะท้อนแท็บ Debug->Entity List

WatchEntity(entityId)

เริ่มเฝ้าดูคอมโพเนนต์ของ entity เธรดทำงานของ host จะอ่านข้อมูลคอมโพเนนต์เต็มรูปแบบสำหรับ entity นี้ทุกเฟรม

UnwatchEntity(entityId)

หยุดเฝ้าดูคอมโพเนนต์ของ entity เรียกสิ่งนี้เมื่อผู้ใช้ยุบโหนดต้นไม้ entity เพื่อปล่อยทรัพยากร

GetWatchedEntityData(entityId) -> DebugEntityComponents

คืนค่าข้อมูลคอมโพเนนต์เต็มรูปแบบสำหรับ entity ที่เฝ้าดู ประกอบด้วย sub-struct สำหรับคอมโพเนนต์ที่รู้จัก 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)

เริ่มเฝ้าดูคลังสำหรับการตรวจสอบดีบักโดยละเอียด Host อ่านการครอบครองช่อง, รายละเอียดไอเท็ม และ mod

GetWatchedInventoryData() -> DebugInventoryData

คืนค่าข้อมูลเต็มรูปแบบสำหรับคลังที่เฝ้าดูปัจจุบัน: ขนาดกริด, การครอบครองช่อง, ไอเท็มพร้อมระดับความหายากและ mod

// 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 Element

GetGameUiRootAddress() -> uintptr_t

คืนค่าที่อยู่ root UI element ของเกม (สำหรับการนำทางต้นไม้ UI ในเกม)

GetUiRootAddress() -> uintptr_t

คืนค่าที่อยู่ root UI ระดับบนสุด

GetGameCullValue() -> int

คืนค่า GameCullSize ปัจจุบัน ใช้สำหรับคำนวณสเกล UI เมื่อรวมกับขนาดหน้าจอ จะช่วยให้คำนวณตำแหน่ง/ขนาดของ UI element ได้แม่นยำ

// 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 -- Wrapper สะดวก (SDK v3)

header sdk/PluginHelpers.h (รวมมากับ ExamplePlugin) ให้คลาส MemoryReader ที่ type-safe ซึ่งห่อหุ้มฟังก์ชันดิบของ PluginContext:

PluginSDK::MemoryReader mem(m_Context);

// Read a single struct
auto data = mem.Read<MyStruct>(address);

// Read an array
auto arr = mem.ReadArray<uint32_t>(address, count);

// Read native containers — returns std::vector<T>
auto vec = mem.ReadStdVector<uint32_t>(vectorAddr);
auto list = mem.ReadStdList<MyNode>(listAddr);
auto bucket = mem.ReadStdBucket<MyEntry>(bucketAddr);
auto map = mem.ReadStdMap<uint32_t, float>(mapAddr);
auto wstr = mem.ReadStdWString(wstringAddr);

MemoryReader ยังให้ wrapper สะดวกสำหรับ ReadString(), ReadUnicodeString(), GetBaseAddress(), GetModuleSize() และ GetPatternAddress()

ยูทิลิตี้เพิ่มเติมใน PluginHelpers.h:

  • WideToNarrow(wstring) -- แปลง wstring เป็น string อย่างปลอดภัย (สูญเสีย ASCII)
  • GetEntityTypeName(type) -- enum เป็นชื่อแสดงผล (รวม ExpeditionMarker/ExpeditionRemnant)
  • GetNearbyZoneName(zone) -- โซนเป็นชื่อแสดงผล
  • GetRarityName(rarity) / GetRarityColor(rarity) -- ตัวช่วยแสดงผลระดับความหายาก

บริการ Host

Log(level, message)

เขียนไปยังระบบบันทึกของ host ระดับ: "Debug", "Info", "Warning", "Error"

ImGuiContext (void*)

บริบท ImGui ของ host เรียก ImGui::SetCurrentContext() ด้วยค่านี้ใน SetContext()

D3DDevice (void*)

ID3D11Device* ของ host แคสต์และใช้สำหรับโหลดเท็กซ์เจอร์


5. ข้อมูลอ้างอิงโครงสร้างข้อมูล

ทุกประเภทอยู่ใน namespace PluginSDK ปลั๊กอินมักเพิ่ม using namespace PluginSDK;

RadarEntity

ข้อมูลต่อ entity ที่มีอยู่ใน snapshot->Entities:

ฟิลด์ ประเภท คำอธิบาย
Id uint32_t ID เฉพาะของ entity
Address uintptr_t ที่อยู่หน่วยความจำ (สำหรับเรียก API ไอเท็ม)
EntityDetailsAddress uintptr_t ที่อยู่ struct รายละเอียด entity
RenderComponentAddress uintptr_t ที่อยู่คอมโพเนนต์ Render (ทางลัด)
IsValid bool แฟล็กความถูกต้องของ entity
entityType EntityTypes หมวดหมู่ entity
entitySubtype EntitySubtypes หมวดหมู่ย่อยของ entity
entityState EntityStates สถานะ entity
Rarity int 0=ปกติ, 1=เวทมนตร์, 2=หายาก, 3=ไม่ซ้ำ
Reaction uint8_t 0=เป็นศัตรู, 1=เป็นกลาง, 2=เป็นมิตร
GridPositionX/Y float ตำแหน่งบนกริดภูมิประเทศ
TerrainHeight float ความสูงภูมิประเทศที่ตำแหน่ง entity
WorldX/Y/Z float ตำแหน่งในพื้นที่โลก
ModelBoundsZ float ความสูงของโมเดล
Path wstring path เมตาดาต้าของ entity
PlayerName wstring ชื่อผู้เล่น (ถ้าเป็น entity ผู้เล่น)
TgtPath string path เป้าหมาย (สตริงแคบ)
CurrentHP/MaxHP int สุขภาพของ entity
CurrentES/MaxES int โล่พลังงานของ entity
IsSleeping bool แฟล็ก entity ที่อยู่ไกล
IsChestOpened bool สถานะหีบเปิด
Zone NearbyZone ความใกล้เคียงกับผู้เล่น
ComponentCache EntityComponentCache ที่อยู่คอมโพเนนต์

สำคัญ: Entity ที่ตายแล้ว (EntityState::Useless) ถูกลบออกจาก snapshot ก่อนถึงปลั๊กอิน คุณจะไม่เห็น HP ของมอนสเตอร์ลดเหลือศูนย์ -- มันเพียงหายไปจากรายการ ใช้ฟิลด์ Zone เพื่อแยกแยะ kill (entity หายไปจาก InnerCircle/OuterCircle) จาก entity ที่ออกนอกระยะ (entity อยู่ในโซน Far)

Buff

Buff/debuff ที่ใช้งานอยู่:

ฟิลด์ ประเภท คำอธิบาย
Name string ชื่อภายในของ buff (เช่น "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 เมตาดาต้า), Path (เหมือน Name), BaseTypeName (ชื่อประเภทพื้นฐาน, เช่น "Divine Orb"), UniqueName (ชื่อไอเท็มไม่ซ้ำจาก Words.dat, เช่น "Headhunter", ว่างสำหรับไอเท็มที่ไม่ใช่ไม่ซ้ำ), SlotX/Y, Width/Height, StackCount, IsCurrency

ExtendedItemModInfo

คืนค่าโดย ReadExtendedItemMods():

ฟิลด์ ประเภท คำอธิบาย
ImplicitMods vector<ItemModData> mod โดยปริยาย
ExplicitMods vector<ItemModData> mod โดยชัดเจน
EnchantMods vector<ItemModData> mod เสน่ห์
HellscapeMods vector<ItemModData> mod Hellscape
CrucibleMods vector<ItemModData> mod Crucible
Rarity int 0=ปกติ, 1=เวทมนตร์, 2=หายาก, 3=ไม่ซ้ำ

ItemModData

ฟิลด์ ประเภท คำอธิบาย
Key string คีย์ stat ของ mod
Values vector<float> ค่า mod

EntityComponentCache

ที่อยู่คอมโพเนนต์ที่แคชไว้ต่อ entity (พร้อมใช้งานผ่าน RadarEntity.ComponentCache):

ฟิลด์ ประเภท เมธอด-Has
RenderAddr uintptr_t HasRender()
PositionedAddr uintptr_t HasPositioned()
ChestAddr uintptr_t HasChest()
PlayerAddr uintptr_t HasPlayer()
ShrineAddr uintptr_t HasShrine()
LifeAddr uintptr_t HasLife()
TargetableAddr uintptr_t HasTargetable()
OMPAddr uintptr_t HasOMP()
NPCAddr uintptr_t HasNPC()
TriggerableBlockageAddr uintptr_t HasTriggerableBlockage()
DiesAfterTimeAddr uintptr_t HasDiesAfterTime()
BuffsAddr uintptr_t HasBuffs()
WorldItemAddr uintptr_t HasWorldItem()
AreaTransitionAddr uintptr_t HasAreaTransition()
MinimapIconAddr uintptr_t HasMinimapIcon()
StatsAddr uintptr_t HasStats()

DebugEntityInfo (SDK v4)

เมตาดาต้า entity จาก GetEntityDebugList():

ฟิลด์ ประเภท คำอธิบาย
Id uint32_t ID entity
Address uintptr_t ที่อยู่หน่วยความจำ
Path string path เมตาดาต้า
EntityType int ประเภท entity (แคสต์เป็น EntityTypes)
EntitySubType int ประเภทย่อยของ entity (แคสต์เป็น EntitySubtypes)
EntityState int สถานะ entity (แคสต์เป็น EntityStates)
Rarity int 0=ปกติ, 1=เวทมนตร์, 2=หายาก, 3=ไม่ซ้ำ
Zone NearbyZone ความใกล้เคียงกับผู้เล่น
ComponentAddresses vector<pair<string,uintptr_t>> คู่ชื่อ->ที่อยู่ของคอมโพเนนต์ทั้งหมด

DebugEntityComponents (SDK v4)

ข้อมูลคอมโพเนนต์เต็มรูปแบบจาก GetWatchedEntityData():

ฟิลด์ ประเภท คำอธิบาย
EntityId uint32_t ข้อมูลนี้เป็นของ entity ตัวไหน
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 Path แอนิเมชัน (string), Id (uint32)
HasStats / Stats bool / DebugStatsComp CurrentWeaponIndex, IsShapeshifted, StatsItems/StatsBuff (เวกเตอร์ของคู่ stat ID->ค่า)
HasActor / Actor bool / DebugActorComp AnimationId, AnimationName, ActiveSkills (เวกเตอร์ของ DebugActiveSkill), DeployedCounts[256]
HasBuffs / Buffs bool / vector<DebugBuff> Buff ที่ใช้งาน: 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> ไอเท็มพร้อม path, ระดับความหายากและ mod

DebugInventoryItem (SDK v4)

ฟิลด์ ประเภท คำอธิบาย
Address uintptr_t ที่อยู่ entity ไอเท็ม
Path string path เมตาดาต้าไอเท็ม
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 จำนวน mod ที่คราฟท์
ImplicitMods vector<DebugModInfo> mod โดยปริยาย
ExplicitMods vector<DebugModInfo> mod โดยชัดเจน
EnchantMods vector<DebugModInfo> mod เสน่ห์
HellscapeMods vector<DebugModInfo> mod Hellscape

DebugActiveSkill (SDK v4)

ฟิลด์ ประเภท คำอธิบาย
Name string ชื่อทักษะ
UseStage int ขั้นตอนการใช้ปัจจุบัน
CastType int ประเภทการร่าย
TotalUses int จำนวนการใช้ทั้งหมด
TotalCooldownTimeInMs int คูลดาวน์เป็นมิลลิวินาที
CanBeUsed bool ทักษะสามารถใช้ได้ในขณะนี้หรือไม่

DebugBuff (SDK v4)

ฟิลด์ ประเภท คำอธิบาย
Name string ชื่อภายในของ buff
TotalTime float ระยะเวลาทั้งหมด
TimeLeft float วินาทีที่เหลือ
Charges short จำนวนชาร์จ
FlaskSlot short ดัชนีช่อง flask
Effectiveness short ประสิทธิผลของ buff
SourceEntityId uint32_t Entity ที่ใช้ buff นี้

DebugModInfo (SDK v4)

ฟิลด์ ประเภท คำอธิบาย
Name string ชื่อแสดงผลของ mod
StatKey string ตัวระบุคีย์ stat
AffixName string ชื่อ affix
GenerationType int 1=Prefix, 2=Suffix, 3=Implicit
Value0 float ค่าแรก (NaN ถ้าไม่มี)
Value1 float ค่าที่สอง (NaN ถ้าไม่มี)

Enum

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 ในปลั๊กอิน

บริบทร่วม

Host และปลั๊กอินใช้บริบท ImGui เดียวกัน คุณต้องเรียก:

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

ในเมธอด SetContext() ของคุณ

ID หน้าต่าง

ใช้ ID หน้าต่างที่ไม่ซ้ำกันเสมอเพื่อหลีกเลี่ยงการชนกับ host หรือปลั๊กอินอื่น:

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

ฟีเจอร์ที่มี

  • หน้าต่าง, แท็บ, ต้นไม้, ตาราง, รายการวาด
  • โหลดเท็กซ์เจอร์ผ่านอุปกรณ์ D3D11
  • เรนเดอร์ overlay ผ่าน ImGui::GetBackgroundDrawList()
  • ไอคอน FontAwesome 6 ผ่าน #include "imgui/IconsFontAwesome6.h" (เช่น ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)

การเรนเดอร์ overlay (SDK v2)

ใช้ WorldToScreen() เพื่อวาดป้ายกำกับ/รูปทรงที่ตำแหน่ง entity:

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

โหมด overlay

override WantsOverlay() ให้คืนค่า true เพื่อขอให้ host เข้าโหมด overlay:

bool WantsOverlay() override { return m_OverlayEnabled; }

เมื่ออยู่ในโหมด overlay หน้าต่าง host จะโปร่งใสและวางตำแหน่งทับเกม การเรียก DrawUI() ของคุณเรนเดอร์ตรงบนหน้าจอเกม

รูปแบบ overlay ลากได้ (SDK v4)

Host overlay ใช้ WS_EX_TRANSPARENT เพื่อทำให้หน้าต่างโปร่งใสต่อการคลิกเมื่อเมนูซ่อน ซึ่งหมายความว่าหน้าต่าง ImGui ไม่สามารถรับ input จากเมาส์ได้เว้นแต่เมนูจะมองเห็นได้ เพื่อสร้างหน้าต่าง overlay ลากได้ (เช่น 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 {
        // === โหมดไม่โต้ตอบ ===
        // Overlay คงที่ — ไม่ลาก, ไม่โต้ตอบเมาส์
        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 ในโหมดไม่โต้ตอบป้องกัน overlay จากการขโมยโฟกัสผ่าน WS_EX_TRANSPARENT
  • ตรวจสอบ null ของตัวชี้ IsMenuVisible เสมอเพื่อความเข้ากันได้ย้อนหลัง: 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";

รูปแบบ key-value อย่างง่าย

สำหรับปลั๊กอินที่มีการตั้งค่าจำนวนมาก รูปแบบข้อความ key=value อย่างง่ายใช้ได้ดี:

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

ตรวจสอบว่าผู้เล่นมี buff เฉพาะหรือไม่

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;

วาดข้อความที่ตำแหน่งโลกของ entity (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");
    }
}

อ่าน mod ไอเท็มจากคลัง

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

นับ entity ตามประเภท

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

ตรวจจับการ kill มอนสเตอร์ (แบบหายไป)

Entity ที่ตายแล้วถูกกรองออกจาก snapshot (EntityState::Useless) ดังนั้นคุณไม่สามารถตรวจจับ HP ที่ลดเป็น 0 แทนที่จะทำเช่นนั้น ให้ติดตาม entity ตาม 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;
        }
    }
}

ทำไมจึงทำงาน: Entity ภายใน ~120 หน่วยกริดที่หายไปอย่างกะทันหันเกือบจะแน่นอนว่าถูกฆ่า (ไม่ใช่แค่เดินออกนอกระยะ) Entity ในโซน Far จะปรากฏและหายไปจากรายการ entity ตามธรรมชาติ -- อย่านับพวกนั้น

สำคัญ: ล้าง m_PrevEntities เมื่อเปลี่ยนพื้นที่ (AreaChangeCounter เปลี่ยน) เพื่อหลีกเลี่ยงผลบวกปลอม

อ่านหน่วยความจำเกมดิบ (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"
}

อ่านที่อยู่คอมโพเนนต์ของ entity

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

ตรวจสอบคอมโพเนนต์ entity ผ่านการเฝ้าดูดีบัก (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 element (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);
        }
    }
}

อ่าน mod ไอเท็มพร้อมตัวช่วยแสดงผล (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
  • Include path ไปยังไดเรกทอรีรากของ POEFixer (สำหรับ SDK header และ ImGui header)
  • เสริม: คัดลอก Plugins/ExamplePlugin/sdk/PluginHelpers.h สำหรับ wrapper MemoryReader และฟังก์ชันยูทิลิตี้

Include Path

.vcxproj ของคุณควรมีไดเรกทอรี include เพิ่มเติมเหล่านี้:

<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

หากใช้ไลบรารีของบุคคลที่สามภายในเครื่อง (เช่น SQLite3 ในโฟลเดอร์ย่อย lib/) ให้เพิ่ม $(ProjectDir)lib ก่อน path ของ solution เพื่อให้ header ท้องถิ่นมีลำดับความสำคัญก่อน:

<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

ไลบรารีของบุคคลที่สาม

SQLite3 (static linking)

เพื่อใช้ SQLite3 ในปลั๊กอิน คุณต้องคอมไพล์ซอร์ส amalgamation โดยตรงเข้าใน DLL -- Windows LoadLibrary ไม่ค้นหาไดเรกทอรีของ DLL เองสำหรับ dependency ดังนั้นการลิงก์แบบ dynamic กับ sqlite3.dll จะล้มเหลวด้วยข้อผิดพลาด 126

ขั้นตอน:

  1. คัดลอก sqlite3.c และ sqlite3.h ไปยังไดเรกทอรี lib/ ของปลั๊กอิน
  2. สร้าง lib/sqlite3-vcpkg-config.h เพื่อ override SQLITE_API (ป้องกันข้อผิดพลาด __declspec(dllimport)):
    #ifndef SQLITE_API
    #define SQLITE_API
    #endif
    #define SQLITE_ENABLE_UNLOCK_NOTIFY 1
    #define SQLITE_OS_WIN 1
    #define SQLITE_ENABLE_COLUMN_METADATA 1
  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) ให้รวม stb_image ในไฟล์ .cpp หนึ่งไฟล์:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

จากนั้นใช้อุปกรณ์ D3D11 จาก m_Context->D3DDevice เพื่อสร้างเท็กซ์เจอร์ GPU

เอาต์พุต

ตั้งค่าไดเรกทอรีเอาต์พุตเป็น:

$(SolutionDir)x64\Release\Plugins\YourPlugin\

การติดตั้ง

คัดลอก DLL ที่บิลด์แล้วไปยัง Plugins/YourPlugin/YourPlugin.dll ข้างไฟล์ executable หลัก

การดีบัก

  1. บิลด์ DLL ปลั๊กอินในโหมด Debug
  2. เริ่มแอปพลิเคชัน host
  3. ใน Visual Studio: Debug -> Attach to Process -> เลือก .exe ของ host
  4. ตั้ง breakpoint ในซอร์สของปลั๊กอิน
  5. ดีบักเกอร์จะหยุดเมื่อโค้ดของคุณถูกเรียก

10. การแก้ไขปัญหา

ปัญหา วิธีแก้
ปลั๊กอินไม่โหลด ตรวจสอบว่าชื่อ DLL ตรงกับชื่อโฟลเดอร์อย่างแน่นอน
"SDK version mismatch" คอมไพล์ปลั๊กอินใหม่ด้วย SDK header ล่าสุด (เวอร์ชันปัจจุบัน: 4)
ข้อผิดพลาด LoadLibrary 126 DLL มี dependency ที่ยังไม่ได้แก้ไข สำหรับไลบรารีของบุคคลที่สามเช่น SQLite3 ให้คอมไพล์แบบ static เข้าใน DLL (ดูส่วนที่ 9) ใช้ dumpbin /dependents YourPlugin.dll เพื่อตรวจสอบ
Crash เมื่อโหลด ตรวจสอบ CRT ไม่ตรงกัน -- ทั้งสองต้องใช้ /MD
ImGui ไม่เรนเดอร์ ตรวจสอบว่า ImGui::SetCurrentContext() ถูกเรียกใน SetContext()
ข้อมูลว่าง/เป็นศูนย์ ตรวจสอบ IsAttached() และ IsInGame() ก่อนอ่านข้อมูล
คลังว่าง เรียก RequestInventoryScan(-1) -- ข้อมูลคลังเป็นแบบตามคำขอ
ข้อผิดพลาด "Missing exports" ตรวจสอบว่า CreatePlugin และ DestroyPlugin ถูก export ด้วย extern "C"
ปลั๊กอินทำให้ host crash สิ่งนี้ไม่ควรเกิดขึ้น -- การเรียกปลั๊กอินทั้งหมดได้รับการปกป้องด้วย SEH ตรวจสอบ log
ข้อมูลเก่า GetSnapshot() คืนค่าข้อมูลของเฟรมล่าสุด อย่าแคชตัวชี้
การอ่านหน่วยความจำคืนค่า 0 ตรวจสอบว่า IsAttached() เป็นจริงและที่อยู่ถูกต้อง
WorldToScreen คืนค่าเท็จ ตำแหน่งอาจอยู่ด้านหลังกล้องหรือนอกหน้าจอ
หน้าต่าง overlay คลิกไม่ได้ Host ใช้ WS_EX_TRANSPARENT เมื่อเมนูซ่อน ใช้ IsMenuVisible() เพื่อแสดงตัวควบคุมแบบโต้ตอบเมื่อเมนูใช้งานอยู่เท่านั้น ดูรูปแบบ Overlay ลากได้ในส่วนที่ 6
การตรวจจับ kill/death ไม่ทำงาน Entity ที่ตายแล้วถูกลบออกจาก snapshot ใช้การตรวจจับแบบหายไปแทนการเปลี่ยนแปลง HP ดูส่วนที่ 8
ข้อผิดพลาด C2491 "dllimport function" header ของไลบรารีบุคคลที่สามกำหนด __declspec(dllimport) สร้าง header override ท้องถิ่นที่ตั้งค่า macro API เป็นว่าง (ดูตัวอย่าง SQLite3 ในส่วนที่ 9)

← Home

Clone this wiki locally