-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide TH
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - บิลด์ x64 Release
-
Runtime Library:
/MD(Multi-threaded DLL) -- ต้องตรงกับ host
- สร้างโปรเจกต์ C++ DLL ใหม่ใน Visual Studio
- ตั้งค่า include path ให้ชี้ไปยังไดเรกทอรีซอร์สของ POEFixer (สำหรับ SDK header และ ImGui header)
- เพิ่มไฟล์ซอร์ส ImGui เข้าในโปรเจกต์ของคุณ:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - รวม Plugin SDK header ในซอร์สของปลั๊กอิน:
หรือใช้ header สะดวกจาก ExamplePlugin:
#include "plugin_sdk/PluginAPI.h" #include "plugin_sdk/PluginContext.h" #include "imgui/imgui.h"
#include "sdk/PluginHelpers.h" // Includes all SDK headers + MemoryReader + utilities
- กำหนด
PLUGIN_EXPORTSและ_CRT_SECURE_NO_WARNINGSใน Preprocessor Definitions ของโปรเจกต์
Plugins/
YourPlugin/
YourPlugin.dll <-- ชื่อ DLL ต้องตรงกับชื่อโฟลเดอร์
config/
settings.txt <-- ไฟล์การตั้งค่าเสริม
data/
... <-- ไดเรกทอรีข้อมูลเสริม (ฐานข้อมูล, แคช, ฯลฯ)
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 แบบเต็มรูปแบบพร้อมการค้นหา, การนำทาง, การเน้น
ตัวอย่างปลั๊กอินที่สมบูรณ์กว่าพร้อม 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
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
ปลั๊กอินทุกตัวต้อง implement interface IPlugin (กำหนดใน plugin_sdk/PluginAPI.h):
- เมื่อถูกเรียก: ครั้งเดียว ทันทีหลังการสร้าง
-
พารามิเตอร์: พาธสัมพัทธ์เช่น
"Plugins/YourPlugin" - วัตถุประสงค์: เก็บพาธนี้สำหรับโหลดการตั้งค่า/ทรัพยากร
-
เมื่อถูกเรียก: ครั้งเดียว หลังจาก
SetPluginDirectory -
พารามิเตอร์: ตัวชี้ไปยัง
PluginContextของ host (ใช้ได้ตลอดอายุปลั๊กอิน) - วัตถุประสงค์: เก็บตัวชี้นี้ -- มันเป็นประตูสู่ข้อมูลเกมทั้งหมดของคุณ
-
สำคัญ: เรียก
ImGui::SetCurrentContext(ctx->ImGuiContext)ที่นี่
- เมื่อถูกเรียก: เมื่อผู้ใช้เปิดใช้งานปลั๊กอิน หรือเมื่อเริ่มต้นถ้าเปิดใช้งานก่อนหน้านี้
-
พารามิเตอร์:
trueถ้าโปรเซสเกมเชื่อมต่ออยู่ในขณะนี้ - วัตถุประสงค์: โหลดการตั้งค่า, จัดสรรทรัพยากร, เริ่มต้นสถานะ
- เมื่อถูกเรียก: เมื่อผู้ใช้ปิดใช้งานปลั๊กอิน
- วัตถุประสงค์: ปล่อยทรัพยากร, หยุดงานพื้นหลัง
- เมื่อถูกเรียก: ทุกเฟรม เมื่อปลั๊กอินเปิดใช้งานเท่านั้น
- วัตถุประสงค์: เรนเดอร์ overlay ของคุณโดยใช้ ImGui
-
หมายเหตุ: ใช้ ID หน้าต่างที่ไม่ซ้ำกันเช่น
"MyWindow##MyPlugin"เพื่อหลีกเลี่ยงการชน
- เมื่อถูกเรียก: ทุกเฟรม ในแท็บการตั้งค่า Plugins (เมื่อเปิดใช้งานเท่านั้น)
- วัตถุประสงค์: เรนเดอร์การกำหนดค่าปลั๊กอินโดยใช้ ImGui
- เมื่อถูกเรียก: เป็นระยะและเมื่อปิดแอปพลิเคชัน
-
วัตถุประสงค์: บันทึกการตั้งค่าลงดิสก์ (เช่น
Plugins/YourPlugin/config/settings.txt)
-
คืนค่า: ชื่อที่แสดงในแท็บ Plugins (เช่น
"My Plugin")
-
คืนค่า:
PLUGIN_SDK_VERSION(ปัจจุบันคือ 4) - วัตถุประสงค์: Host ตรวจสอบค่านี้สำหรับความเข้ากันได้ -- ต้องตรงกัน
-
คืนค่า:
trueถ้าปลั๊กอินต้องการเรนเดอร์ในโหมด overlay (overlay โปร่งใสทับเกม) -
ค่าเริ่มต้น:
false-- ปลั๊กอินเรนเดอร์เฉพาะในหน้าต่างการตั้งค่าปกติ -
วัตถุประสงค์: เมื่อปลั๊กอินใดคืนค่า
truehost จะเข้าโหมด overlay แม้ว่าจะไม่มีฟีเจอร์ในตัวที่ต้องการ
DLL ของคุณต้อง export ฟังก์ชัน C สองตัวนี้:
extern "C" PLUGIN_API IPlugin* CreatePlugin() {
return new MyPlugin();
}
extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
delete plugin;
}struct PluginContext (กำหนดใน plugin_sdk/PluginContext.h) ให้ function pointer สำหรับเข้าถึงข้อมูลเกม ทุกประเภทอยู่ใน namespace PluginSDK
คืนค่า 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: สูตรทั่วไป สำหรับรายละเอียด
ทางลัดสะดวกสำหรับสัญญาณชีพของผู้เล่น
คืนค่า enum สถานะปัจจุบันของเกม
จริงถ้าโปรเซสเกมเชื่อมต่อแล้วและอ่านได้
จริงถ้าอยู่ในเกมปัจจุบัน (ไม่โหลด, ไม่อยู่หน้าจอเข้าสู่ระบบ)
จริงถ้าหน้าต่างเกมเป็นหน้าต่างเบื้องหน้า
คืนค่า ID โปรเซสเกม
อ่าน mod ทั้งหมดจาก entity ไอเท็ม
คืนค่า: 0=ปกติ, 1=เวทมนตร์, 2=หายาก, 3=ไม่ซ้ำ
คืนค่าจำนวนกองสำหรับไอเท็มเงินตรา/ซ้อนได้
คืนค่าชื่อประเภทฐานของไอเท็ม
คืนค่า path เมตาดาต้าของไอเท็ม
คืนค่าชื่อที่อ่านได้ของไอเท็ม (เช่น "Divine Orb", "Chaos Orb") ต่างจาก ReadItemName ที่คืนค่า path เมตาดาต้า ฟังก์ชันนี้อ่านชื่อจริงจาก BaseItemTypeData.DisplayNamePtr
คืนค่าจริงถ้า host อยู่ในโหมด overlay ปัจจุบัน (overlay โปร่งใสทับหน้าต่างเกม) ใช้สิ่งนี้เพื่อปรับการเรนเดอร์ -- เช่น วาดบน game overlay เทียบกับวาดในหน้าต่างการตั้งค่า
คืนค่าจริงเมื่อเมนูการตั้งค่าของ host มองเห็นได้ (overlay เป็นแบบโต้ตอบ) เมื่อเมนูซ่อนอยู่ หน้าต่าง overlay จะโปร่งใสต่อการคลิก (WS_EX_TRANSPARENT) ดังนั้นหน้าต่าง ImGui ไม่สามารถรับ input จากเมาส์ได้
ใช้สิ่งนี้เพื่อ implement รูปแบบ overlay ลากได้:
- เมนูมองเห็นได้: แสดงตัวจับลาก, อนุญาตการโต้ตอบ (แท็บ, ปุ่ม)
-
เมนูซ่อน: ลบตัวจับลาก, เพิ่ม
ImGuiWindowFlags_NoInputsเพื่อทำให้หน้าต่างไม่โต้ตอบ
ดู ส่วนที่ 6: รูปแบบ overlay ลากได้ สำหรับการ implement เต็มรูปแบบ
เข้าถึงหน่วยความจำโปรเซสเกมโดยตรง การอ่านทั้งหมดปลอดภัย (คืนค่า 0/ว่างเมื่อล้มเหลว)
คืนค่าที่อยู่ฐานของโมดูลเกม คืนค่า 0 ถ้าไม่ได้เชื่อมต่อ
คืนค่าขนาดโมดูลเกมเป็นไบต์ คืนค่า 0 ถ้าไม่ได้เชื่อมต่อ
อ่านบล็อกไบต์ดิบจากโปรเซสเกม 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));อ่านสตริง ASCII ที่จบด้วย null จากหน่วยความจำเกม (สูงสุด 128 ตัวอักษร)
อ่านสตริง Unicode (wide) ที่จบด้วย null จากหน่วยความจำเกม (สูงสุด 128 wchar)
รับที่อยู่ที่แก้ไขแล้วจากการสแกนแพตเทิร์นตามชื่อ คืนค่า 0 ถ้าไม่พบ
แพตเทิร์นมาตรฐาน:
| ชื่อ | คำอธิบาย |
|---|---|
"Game States" |
รากเวกเตอร์ GameStates |
"File Root" |
รีจิสทรีไฟล์ |
"AreaChangeCounter" |
ตัวนับการเปลี่ยนพื้นที่ |
"Terrain Rotator Helper" |
ข้อมูลการหมุน |
"Terrain Rotation Selector" |
ตัวเลือกการหมุน |
"GameCullSize" |
ค่าการตัดหน้าจอ |
แปลงตำแหน่งในพื้นที่โลกเป็นพิกัดหน้าจอ คืนค่าจริงถ้าตำแหน่งมองเห็นได้บนหน้าจอ
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");
}ขอให้ host สแกนคลัง ส่ง -1 เพื่อสแกนคลังทั้งหมด หรือ ID คลังเฉพาะ ข้อมูลคลังใน snapshot จะถูกเติมหลังจากสแกนเสร็จ (เฟรมถัดไป)
หมายเหตุ: ข้อมูลคลังไม่ได้รีเฟรชอัตโนมัติ -- คุณต้องเรียกฟังก์ชันนี้เพื่อเริ่มสแกน เรียกเป็นระยะ (เช่น ทุก 2 วินาที) ถ้าคุณต้องการข้อมูลคลังต่อเนื่อง
คืนค่าตัวชี้ไปยังข้อมูลกริดที่เดินได้ กริดเป็นอาร์เรย์ 2 มิติที่ 0 = เดินไม่ได้, ไม่ใช่ศูนย์ = เดินได้ คืนค่า nullptr ถ้าข้อมูลไม่พร้อมใช้งาน
คืนค่าความสูงภูมิประเทศที่ตำแหน่งกริด คืนค่า 0 ถ้าอยู่นอกขอบเขตหรือข้อมูลไม่พร้อม
ฟังก์ชันเหล่านี้อ่านคอนเทนเนอร์ของไลบรารีมาตรฐาน C++ โดยตรงจากหน่วยความจำเกม สะท้อนเมธอด Core::Process ของ host
อ่าน 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);
}อ่าน StdList (struct 16 ไบต์: {Head, Size}) จากหน่วยความจำเกม สำรวจ linked list และคืนค่าบัฟเฟอร์ต่อเนื่อง ผู้เรียกต้อง free()
อ่าน StdBucket จากหน่วยความจำเกม (อ่าน StdVector ที่ฝังอยู่) ผู้เรียกต้อง free()
สำรวจ 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);อ่าน StdWString (struct 32 ไบต์พร้อมบัฟเฟอร์ inline/heap) จากหน่วยความจำเกม
คืนค่าชื่อที่อ่านได้สำหรับ ID คลัง (เช่น 1 -> "MainInventory1", 3 -> "Weapon1", 64 -> "Currency1")
SDK v4 ให้การเข้าถึงโดยตรงไปยังข้อมูลดีบักของ host -- คอมโพเนนต์ entity, รายละเอียดคลัง และต้นไม้ element UI -- ตรงกับแท็บดีบักในตัว
คืนค่ารายการ entity ทั้งหมดพร้อมเมตาดาต้าดีบัก (Id, Address, Path, Type, SubType, State, Rarity, Zone) สิ่งนี้สะท้อนแท็บ Debug->Entity List
เริ่มเฝ้าดูคอมโพเนนต์ของ entity เธรดทำงานของ host จะอ่านข้อมูลคอมโพเนนต์เต็มรูปแบบสำหรับ entity นี้ทุกเฟรม
หยุดเฝ้าดูคอมโพเนนต์ของ entity เรียกสิ่งนี้เมื่อผู้ใช้ยุบโหนดต้นไม้ entity เพื่อปล่อยทรัพยากร
คืนค่าข้อมูลคอมโพเนนต์เต็มรูปแบบสำหรับ 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);
}
}คืนค่าที่อยู่ฐานของคอมโพเนนต์ ServerData
คืนค่า ID คลังของผู้เล่นทั้งหมดและที่อยู่ (จาก ServerData)
เริ่มเฝ้าดูคลังสำหรับการตรวจสอบดีบักโดยละเอียด Host อ่านการครอบครองช่อง, รายละเอียดไอเท็ม และ mod
คืนค่าข้อมูลเต็มรูปแบบสำหรับคลังที่เฝ้าดูปัจจุบัน: ขนาดกริด, การครอบครองช่อง, ไอเท็มพร้อมระดับความหายากและ 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()));
}คืนค่าที่อยู่ root UI element ของเกม (สำหรับการนำทางต้นไม้ UI ในเกม)
คืนค่าที่อยู่ root UI ระดับบนสุด
คืนค่า 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)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 ระดับ: "Debug", "Info", "Warning", "Error"
บริบท ImGui ของ host เรียก ImGui::SetCurrentContext() ด้วยค่านี้ใน SetContext()
ID3D11Device* ของ host แคสต์และใช้สำหรับโหลดเท็กซ์เจอร์
ทุกประเภทอยู่ใน namespace PluginSDK ปลั๊กอินมักเพิ่ม using namespace PluginSDK;
ข้อมูลต่อ 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/debuff ที่ใช้งานอยู่:
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Name |
string |
ชื่อภายในของ buff (เช่น "flask_effect_life") |
TimeLeft |
float |
วินาทีที่เหลือ |
Charges |
short |
จำนวนชาร์จ |
TotalTime |
float |
ระยะเวลาทั้งหมด |
สถานะแผนที่ขนาดเล็ก/แผนที่ขนาดใหญ่:
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
CenterX/Y |
float |
ศูนย์กลางแผนที่ |
SizeX/Y |
float |
ขนาดแผนที่ |
ShiftX/Y |
float |
ออฟเซ็ตการเลื่อนปัจจุบัน |
DefaultShiftX/Y |
float |
ค่าเลื่อนเริ่มต้น |
Zoom |
float |
ระดับซูม |
Scale |
float |
ตัวคูณสเกลแผนที่ |
IsVisible |
bool |
แผนที่กำลังแสดงอยู่ |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Id |
int |
ID คลัง |
TotalBoxesX/Y |
int |
ขนาดกริด |
Ptr |
uintptr_t |
ที่อยู่หน่วยความจำคลัง |
Items |
vector<InventoryItemInfo> |
ไอเท็มในคลัง |
ฟิลด์ไอเท็ม: Address, Name (path เมตาดาต้า), Path (เหมือน Name), DisplayName (อ่านได้, เช่น "Divine Orb"), SlotX/Y, Width/Height, StackCount, IsCurrency
คืนค่าโดย 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=ไม่ซ้ำ |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Key |
string |
คีย์ stat ของ mod |
Values |
vector<float> |
ค่า mod |
ที่อยู่คอมโพเนนต์ที่แคชไว้ต่อ 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() |
เมตาดาต้า 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>> |
คู่ชื่อ->ที่อยู่ของคอมโพเนนต์ทั้งหมด |
ข้อมูลคอมโพเนนต์เต็มรูปแบบจาก 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, NeedsTrue, MeetsQuestState, NeedsFalse |
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 |
รายละเอียดคลังจาก GetWatchedInventoryData():
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
InventoryId |
int |
ID คลัง (-1 ถ้าไม่มี) |
Address |
uintptr_t |
ที่อยู่คลัง |
TotalBoxesX/Y |
int |
ขนาดกริด |
ServerRequestCounter |
int |
ตัวนับซิงค์เซิร์ฟเวอร์ |
SlotOccupied |
vector<bool> |
การครอบครองต่อช่อง |
Items |
vector<DebugInventoryItem> |
ไอเท็มพร้อม path, ระดับความหายากและ mod |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Address |
uintptr_t |
ที่อยู่ entity ไอเท็ม |
Path |
string |
path เมตาดาต้าไอเท็ม |
DisplayName |
string |
ชื่อไอเท็มที่อ่านได้ |
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 |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Name |
string |
ชื่อทักษะ |
UseStage |
int |
ขั้นตอนการใช้ปัจจุบัน |
CastType |
int |
ประเภทการร่าย |
TotalUses |
int |
จำนวนการใช้ทั้งหมด |
TotalCooldownTimeInMs |
int |
คูลดาวน์เป็นมิลลิวินาที |
CanBeUsed |
bool |
ทักษะสามารถใช้ได้ในขณะนี้หรือไม่ |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Name |
string |
ชื่อภายในของ buff |
TotalTime |
float |
ระยะเวลาทั้งหมด |
TimeLeft |
float |
วินาทีที่เหลือ |
Charges |
short |
จำนวนชาร์จ |
FlaskSlot |
short |
ดัชนีช่อง flask |
Effectiveness |
short |
ประสิทธิผลของ buff |
SourceEntityId |
uint32_t |
Entity ที่ใช้ buff นี้ |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
Name |
string |
ชื่อแสดงผลของ mod |
StatKey |
string |
ตัวระบุคีย์ stat |
AffixName |
string |
ชื่อ affix |
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)
Host และปลั๊กอินใช้บริบท ImGui เดียวกัน คุณต้องเรียก:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));ในเมธอด SetContext() ของคุณ
ใช้ 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)
ใช้ 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));
}override WantsOverlay() ให้คืนค่า true เพื่อขอให้ host เข้าโหมด overlay:
bool WantsOverlay() override { return m_OverlayEnabled; }เมื่ออยู่ในโหมด overlay หน้าต่าง host จะโปร่งใสและวางตำแหน่งทับเกม การเรียก DrawUI() ของคุณเรนเดอร์ตรงบนหน้าจอเกม
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 - บันทึกตำแหน่งไปยังไฟล์การตั้งค่าเพื่อให้คงอยู่ข้ามเซสชัน
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 อย่างง่ายใช้ได้ดี:
// Save
std::ofstream file(configDir / "settings.txt");
file << "ShowOverlay=" << (m_Settings.ShowOverlay ? 1 : 0) << "\n";
file << "WindowAlpha=" << m_Settings.WindowAlpha << "\n";
file << "PosX=" << m_Settings.PosX << "\n";
file << "PosY=" << m_Settings.PosY << "\n";
// Load
std::ifstream file(settingsPath);
std::string line;
while (std::getline(file, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "ShowOverlay") m_Settings.ShowOverlay = (val == "1");
else if (key == "WindowAlpha") m_Settings.WindowAlpha = std::stof(val);
else if (key == "PosX") m_Settings.PosX = std::stof(val);
else if (key == "PosY") m_Settings.PosY = std::stof(val);
}auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100auto snapshot = m_Context->GetSnapshot();
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster &&
e.Zone == NearbyZone::InnerCircle) {
// e.CurrentHP, e.Path, e.WorldX/Y/Z...
}
}auto vitals = m_Context->GetPlayerVitals();
for (auto& buff : vitals.Buffs) {
if (buff.Name == "flask_effect_life") {
// buff.TimeLeft, buff.Charges...
}
}auto snapshot = m_Context->GetSnapshot();
std::string area = snapshot->CurrentAreaName;
bool isTown = snapshot->IsTown;
int level = snapshot->CurrentAreaLevel;for (auto& e : snapshot->Entities) {
if (e.entityType != EntityTypes::Monster) continue;
float sx, sy;
if (m_Context->WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, &sx, &sy)) {
auto* dl = ImGui::GetBackgroundDrawList();
dl->AddText(ImVec2(sx, sy - 15), IM_COL32(255, 255, 0, 255), "Monster");
}
}// First, request an inventory scan (call periodically, e.g. every 2s)
m_Context->RequestInventoryScan(-1);
// Then read from snapshot (next frame)
auto snapshot = m_Context->GetSnapshot();
for (auto& inv : snapshot->Inventories) {
for (auto& item : inv.Items) {
auto mods = m_Context->ReadExtendedItemMods(item.Address);
// mods.ExplicitMods, mods.ImplicitMods...
}
}auto snapshot = m_Context->GetSnapshot();
int monsters = 0, items = 0;
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster) monsters++;
if (e.entityType == EntityTypes::Item) items++;
}static uint64_t lastAreaChange = 0;
auto snapshot = m_Context->GetSnapshot();
if (snapshot->AreaChangeCounter != lastAreaChange) {
lastAreaChange = snapshot->AreaChangeCounter;
// Area changed! Reset state...
}if (m_Context->GetCurrentState() == GameStateTypes::AreaLoadingState) {
// Currently loading...
}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 เปลี่ยน) เพื่อหลีกเลี่ยงผลบวกปลอม
// Read a struct from a known address
struct MyGameStruct { int field1; float field2; };
MyGameStruct data{};
if (m_Context->ReadProcessMemory(someAddress, &data, sizeof(data))) {
// data.field1, data.field2 are now populated
}
// Read a string from memory
std::string str = m_Context->ReadString(stringAddress);uintptr_t gameStatesAddr = m_Context->GetPatternAddress("Game States");
if (gameStatesAddr != 0) {
// Read data at the resolved pattern address
uint64_t value = 0;
m_Context->ReadProcessMemory(gameStatesAddr, &value, sizeof(value));
}int gridW = 0, gridH = 0;
const uint8_t* grid = m_Context->GetWalkableGrid(&gridW, &gridH);
if (grid && gridW > 0 && gridH > 0) {
int x = (int)snapshot->Player.GridPositionX;
int y = (int)snapshot->Player.GridPositionY;
if (x >= 0 && x < gridW && y >= 0 && y < gridH) {
bool walkable = grid[y * gridW + x] != 0;
}
}PluginSDK::MemoryReader mem(m_Context);
// Read a struct from a known address
struct GameData { int level; float health; };
auto data = mem.Read<GameData>(address);
// Read a StdVector of pointers
auto ptrs = mem.ReadStdVector<uintptr_t>(vectorAddr);
for (auto ptr : ptrs) { /* process each pointer */ }
// Read a StdMap<int, float>
auto entries = mem.ReadStdMap<int, float>(mapAddr);
for (auto& [key, value] : entries) { /* key, value */ }for (auto& inv : snapshot->Inventories) {
const char* name = m_Context->GetInventoryName(inv.Id);
// name is e.g. "MainInventory1", "Weapon1", "Currency1"
}for (auto& e : snapshot->Entities) {
auto& cc = e.ComponentCache;
if (cc.HasLife()) {
// cc.LifeAddr contains the Life component address
// Use MemoryReader to read component structs
}
if (cc.HasRender()) {
// cc.RenderAddr has the Render component address
}
}// Get all entities with debug info
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
bool open = ImGui::TreeNode(e.Path.c_str());
if (open) {
m_Context->WatchEntity(e.Id);
auto comp = m_Context->GetWatchedEntityData(e.Id);
if (comp.HasLife) {
ImGui::Text("HP: %d/%d ES: %d/%d MP: %d/%d",
comp.Life.Health.Current, comp.Life.Health.Total,
comp.Life.EnergyShield.Current, comp.Life.EnergyShield.Total,
comp.Life.Mana.Current, comp.Life.Mana.Total);
}
if (comp.HasActor) {
ImGui::Text("Animation: %s (%d) Skills: %d",
comp.Actor.AnimationName.c_str(), comp.Actor.AnimationId,
(int)comp.Actor.ActiveSkills.size());
}
ImGui::TreePop();
} else {
m_Context->UnwatchEntity(e.Id);
}
}auto invList = m_Context->GetPlayerInventoryList();
if (!invList.empty()) {
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
if (inv.InventoryId >= 0) {
ImGui::Text("Grid: %dx%d Items: %d",
inv.TotalBoxesX, inv.TotalBoxesY, (int)inv.Items.size());
for (auto& item : inv.Items) {
ImGui::Text("[R%d iLvl%d] %s (%s) Mods: %d/%d/%d/%d",
item.Rarity, item.ItemLevel,
item.DisplayName.c_str(), item.Path.c_str(),
(int)item.ImplicitMods.size(), (int)item.ExplicitMods.size(),
(int)item.EnchantMods.size(), (int)item.HellscapeMods.size());
}
}
}uintptr_t uiRoot = m_Context->GetGameUiRootAddress();
if (uiRoot) {
PluginSDK::MemoryReader mem(m_Context);
// Read children vector at offset 0x010
auto children = mem.ReadStdVector<uintptr_t>(uiRoot + 0x010);
for (auto childAddr : children) {
// Read StringId at offset 0x448
uintptr_t strPtr = mem.Read<uintptr_t>(childAddr + 0x448);
if (strPtr) {
std::string name = m_Context->ReadString(strPtr);
ImGui::Text("Child: %s (0x%llX)", name.c_str(), childAddr);
}
}
}auto mods = m_Context->ReadExtendedItemMods(item.Address);
ImGui::TextColored(
PluginSDK::GetRarityColor(mods.Rarity),
"Rarity: %s", PluginSDK::GetRarityName(mods.Rarity));
for (auto& mod : mods.ExplicitMods) {
ImGui::BulletText("%s", mod.Key.c_str());
}| การตั้งค่า | ค่า |
|---|---|
| Configuration | Release |
| Platform | x64 |
| C++ Standard | /std:c++20 |
| Runtime Library |
/MD (Multi-threaded DLL) |
| Configuration Type | DLL |
- ไฟล์
.cppของปลั๊กอิน - ไฟล์ซอร์ส ImGui:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Include path ไปยังไดเรกทอรีรากของ POEFixer (สำหรับ SDK header และ ImGui header)
- เสริม: คัดลอก
Plugins/ExamplePlugin/sdk/PluginHelpers.hสำหรับ wrapperMemoryReaderและฟังก์ชันยูทิลิตี้
.vcxproj ของคุณควรมีไดเรกทอรี include เพิ่มเติมเหล่านี้:
<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>หากใช้ไลบรารีของบุคคลที่สามภายในเครื่อง (เช่น SQLite3 ในโฟลเดอร์ย่อย lib/) ให้เพิ่ม $(ProjectDir)lib ก่อน path ของ solution เพื่อให้ header ท้องถิ่นมีลำดับความสำคัญก่อน:
<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>เพื่อใช้ SQLite3 ในปลั๊กอิน คุณต้องคอมไพล์ซอร์ส amalgamation โดยตรงเข้าใน DLL -- Windows LoadLibrary ไม่ค้นหาไดเรกทอรีของ DLL เองสำหรับ dependency ดังนั้นการลิงก์แบบ dynamic กับ sqlite3.dll จะล้มเหลวด้วยข้อผิดพลาด 126
ขั้นตอน:
- คัดลอก
sqlite3.cและsqlite3.hไปยังไดเรกทอรีlib/ของปลั๊กอิน - สร้าง
lib/sqlite3-vcpkg-config.hเพื่อ overrideSQLITE_API(ป้องกันข้อผิดพลาด__declspec(dllimport)):#ifndef SQLITE_API #define SQLITE_API #endif #define SQLITE_ENABLE_UNLOCK_NOTIFY 1 #define SQLITE_OS_WIN 1 #define SQLITE_ENABLE_COLUMN_METADATA 1
- เพิ่ม
sqlite3.cใน.vcxprojของคุณเป็นไฟล์ C พร้อมปิดคำเตือน:<ClCompile Include="lib\sqlite3.c"> <CompileAs>CompileAsC</CompileAs> <WarningLevel>TurnOffAllWarnings</WarningLevel> <SDLCheck>false</SDLCheck> <PreprocessorDefinitions>SQLITE_THREADSAFE=1;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile>
สำหรับโหลดเท็กซ์เจอร์จากไฟล์ภาพ (PNG, JPG) ให้รวม stb_image ในไฟล์ .cpp หนึ่งไฟล์:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"จากนั้นใช้อุปกรณ์ D3D11 จาก m_Context->D3DDevice เพื่อสร้างเท็กซ์เจอร์ GPU
ตั้งค่าไดเรกทอรีเอาต์พุตเป็น:
$(SolutionDir)x64\Release\Plugins\YourPlugin\
คัดลอก DLL ที่บิลด์แล้วไปยัง Plugins/YourPlugin/YourPlugin.dll ข้างไฟล์ executable หลัก
- บิลด์ DLL ปลั๊กอินในโหมด Debug
- เริ่มแอปพลิเคชัน host
- ใน Visual Studio: Debug -> Attach to Process -> เลือก .exe ของ host
- ตั้ง breakpoint ในซอร์สของปลั๊กอิน
- ดีบักเกอร์จะหยุดเมื่อโค้ดของคุณถูกเรียก
| ปัญหา | วิธีแก้ |
|---|---|
| ปลั๊กอินไม่โหลด | ตรวจสอบว่าชื่อ 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) |