-
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.BaseTypeName
คืนค่าชื่อไอเท็มไม่ซ้ำจาก Words.dat (เช่น "Headhunter", "Brimstone Call") คืนค่าสตริงว่างสำหรับไอเท็มที่ไม่ใช่ไม่ซ้ำ
คืนค่าจริงถ้า 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), BaseTypeName (ชื่อประเภทพื้นฐาน, เช่น "Divine Orb"), UniqueName (ชื่อไอเท็มไม่ซ้ำจาก Words.dat, เช่น "Headhunter", ว่างสำหรับไอเท็มที่ไม่ใช่ไม่ซ้ำ), 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, 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 |
รายละเอียดคลังจาก 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 |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
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 |
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
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.BaseTypeName.c_str(), item.Path.c_str(),
(int)item.ImplicitMods.size(), (int)item.ExplicitMods.size(),
(int)item.EnchantMods.size(), (int)item.HellscapeMods.size());
}
}
}uintptr_t uiRoot = m_Context->GetGameUiRootAddress();
if (uiRoot) {
PluginSDK::MemoryReader mem(m_Context);
// Read children vector at offset 0x010
auto children = mem.ReadStdVector<uintptr_t>(uiRoot + 0x010);
for (auto childAddr : children) {
// Read StringId at offset 0x448
uintptr_t strPtr = mem.Read<uintptr_t>(childAddr + 0x448);
if (strPtr) {
std::string name = m_Context->ReadString(strPtr);
ImGui::Text("Child: %s (0x%llX)", name.c_str(), childAddr);
}
}
}auto mods = m_Context->ReadExtendedItemMods(item.Address);
ImGui::TextColored(
PluginSDK::GetRarityColor(mods.Rarity),
"Rarity: %s", PluginSDK::GetRarityName(mods.Rarity));
for (auto& mod : mods.ExplicitMods) {
ImGui::BulletText("%s", mod.Key.c_str());
}| การตั้งค่า | ค่า |
|---|---|
| Configuration | Release |
| Platform | x64 |
| C++ Standard | /std:c++20 |
| Runtime Library |
/MD (Multi-threaded DLL) |
| Configuration Type | DLL |
- ไฟล์
.cppของปลั๊กอิน - ไฟล์ซอร์ส ImGui:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - 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) |