-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide TH
ปลั๊กอินของ POEFixer คือ DLL C++ แบบเนทีฟที่โหลดในขณะรันไทม์จาก Plugins/<PluginName>/<PluginName>.dll ปลั๊กอินสามารถอ่านสถานะเกมแบบเรียลไทม์ วาด ImGui overlay บันทึกการตั้งค่าของตัวเอง และสมัครรับเหตุการณ์จากโฮสต์
SDK ของปลั๊กอินมีสถาปัตยกรรมสามชั้น:
Plugin DLL ───► PluginSDK.h (header-only C++ wrapper, owns std::string/vector/function)
│
▼ inline function-pointer calls only
HostAbi (pure-C ABI, POD structs only)
│
▼ SEH-wrapped on the host side
Host bridge: plugin_manager/bridge/Bridge_<Service>.cpp (10 files)
│
▼
GameClient + GameLibrary
- ผู้พัฒนาปลั๊กอินจะ include เฮดเดอร์เพียงไฟล์เดียว:
POEFixer/plugin_sdk/PluginSDK.h - เฮดเดอร์นั้นประกาศทุกอย่างใน namespace
PluginSDK::และดึง C ABI จากPluginAbi.hมาให้ภายใน คุณสามารถพูดถึงการมีอยู่ของไฟล์หลังได้ แต่แทบไม่จำเป็นต้องเปิดดู - คอนเทนเนอร์
std::*ทั้งหมดอยู่ภายในปลั๊กอิน DLL เท่านั้น มีเพียง POD เท่านั้นที่ข้ามขอบเขตโฮสต์ ซึ่งหมายความว่าปลั๊กอินที่สร้างด้วย toolchain คนละเวอร์ชันจะไม่ปะปนกับ STL ของโฮสต์ — มีเพียง integer, float, pointer และ struct ขนาดเล็กเท่านั้นที่ใช้ร่วมกัน
เฮดเดอร์ของ SDK อยู่ที่:
-
POEFixer/plugin_sdk/PluginSDK.h— wrapper C++ ที่ผู้เขียนปลั๊กอินใช้ -
POEFixer/plugin_sdk/PluginAbi.h— C ABI ล้วน ๆ ที่อยู่ด้านล่าง
ปลั๊กอินอ้างอิงที่มาพร้อมกับ repo (อ่านได้ในฐานะเอกสารประกอบ): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/
ปลั๊กอินขั้นต่ำที่สามารถโหลดและพิมพ์ข้อความลงในล็อกของโฮสต์:
#define PLUGIN_EXPORTS
#include "POEFixer/plugin_sdk/PluginSDK.h"
class HelloPlugin : public PluginSDK::Plugin {
public:
const char* GetName() const override { return "Hello"; }
void OnEnable(bool) override { ctx()->Log.Info("Hello, world"); }
};
extern "C" PLUGIN_API PluginSDK::Plugin* CreatePlugin() { return new HelloPlugin(); }
extern "C" PLUGIN_API void DestroyPlugin(PluginSDK::Plugin* p) { delete p; }สร้างเป็น Plugins/Hello/Hello.dll รีสตาร์ทโฮสต์ และเปิดใช้จากแท็บ Plugins
ใช้ Plugins/ExamplePlugin/ExamplePlugin.vcxproj เป็นเทมเพลตหลัก การตั้งค่าที่จำเป็น:
- Configuration type: DynamicLibrary
- Platform toolset: v143 (Visual Studio 2022)
- Character set: Unicode
-
Language standard:
stdcpp20 -
Runtime library:
MultiThreadedDLL(Release) /MultiThreadedDebugDLL(Debug) ต้องตรงกับโฮสต์ -
Preprocessor definitions:
PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS -
Additional include directories:
$(SolutionDir)POEFixer -
Output directory:
$(SolutionDir)x64\Release\Plugins\<YourPlugin>\ -
Target name: ต้องตรงกับชื่อโฟลเดอร์ (
Plugins/MyPlugin/→MyPlugin.dll)
โฮสต์จะสแกนแต่ละโฟลเดอร์ย่อยใน Plugins/ และมองหา <FolderName>.dll โดย DLL ต้อง export สัญลักษณ์สามรายการ:
-
CreatePlugin— โรงงานสร้าง คืนค่าเป็นPluginSDK::Plugin* -
DestroyPlugin— destructor รับPluginSDK::Plugin* -
PluginSDK_AttachHost— เชื่อมContextเข้ากับปลั๊กอิน ถูกประกาศไว้ในPluginSDK.hและ emit ให้อัตโนมัติเมื่อกำหนดPLUGIN_EXPORTS
โครงสร้างซอร์สที่แนะนำ:
Plugins/YourPlugin/
YourPlugin.vcxproj
YourPlugin.vcxproj.filters
src/
YourPlugin.cpp // class YourPlugin : public PluginSDK::Plugin
YourPluginSettings.h // Save()/Load() POCO
config/ // runtime-created by SaveSettings()
settings.json
Directory() คืนค่า path แบบ absolute เข้ารหัส UTF-8 ที่ชี้ไปยังไดเรกทอรีของ EXE โฮสต์ — ไม่ต้อง prepend path ของ EXE เอง สตริงไดเรกทอรีถูกเก็บแบบ by-value ภายใน PluginSDK::Plugin จึงคงอยู่ผ่านการ realloc คอนเทนเนอร์ของโฮสต์และวงจรการ reload ได้โดยไม่ต้องกังวลเรื่อง lifetime
หากต้องการวาด ImGui ต้องเพิ่มไฟล์เหล่านี้ใน <ClCompile> ด้วย (โฮสต์เชื่อมโยงไฟล์เหล่านี้ไว้แล้ว แต่ ImGui ฝั่งปลั๊กอินเป็นต่อ DLL):
..\..\POEFixer\imgui\imgui.cpp
..\..\POEFixer\imgui\imgui_draw.cpp
..\..\POEFixer\imgui\imgui_tables.cpp
..\..\POEFixer\imgui\imgui_widgets.cpp
ใน OnEnable ให้เชื่อมต่อกับ ImGui context ของโฮสต์:
if (ctx()->ImGuiContext)
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx()->ImGuiContext));PluginSDK::Plugin เป็น virtual base class ให้ override เมธอดเหล่านี้ในปลั๊กอินของคุณ (ตามลำดับการเรียกโดยประมาณ):
| Method | Called when | Typical use |
|---|---|---|
const char* GetName() const |
ครั้งเดียวหลังการสร้างวัตถุ | คืนชื่อแสดงของปลั๊กอิน |
void OnEnable(bool isGameAttached) |
เมื่อผู้ใช้เปิดใช้งานปลั๊กอิน (หรือตอนเริ่มต้นหากบันทึกไว้) | โหลดการตั้งค่า สมัครรับ event เชื่อมต่อ ImGui context |
void DrawSettings() |
ทุกเฟรมที่แผงการตั้งค่าของปลั๊กอินเปิดอยู่ | ตัวควบคุม ImGui สำหรับการกำหนดค่า |
void DrawUI() |
ทุกเฟรมที่ปลั๊กอินเปิดใช้งาน | วาด overlay ImGui (ใช้ ImGui::GetBackgroundDrawList() สำหรับ overlay ในเกม) |
bool WantsOverlay() const |
สำรวจทุกเฟรม | คืน true ถ้าต้องการให้โฮสต์อยู่ในโหมด overlay (click-through) |
void SaveSettings() |
เป็นระยะ (~5 วินาที) และตอนปิดใช้งาน | บันทึกการกำหนดค่าลงดิสก์ |
void OnDisable() |
เมื่อผู้ใช้ปิดใช้งาน หรือตอนโฮสต์ shutdown | ปล่อยทรัพยากร ยกเลิกการรับ event |
มีเพียง GetName เท่านั้นที่จำเป็น ส่วนที่เหลือมีค่า default ที่ปลอดภัย
โฮสต์ยังเรียก GetSDKVersion() (ประกาศไว้ใน base class อย่า override) ทันทีหลัง CreatePlugin เพื่อตรวจสอบว่าปลั๊กอินและโฮสต์เข้ากันได้ หากไม่ตรงกัน → ปลั๊กอินจะถูกปฏิเสธ
ctx() คืนค่า const PluginSDK::Context* ซึ่งเป็นการรวมของบริการทั้ง 10:
struct Context {
GameService Game; // snapshot, state flags, screen size
EntitiesService Entities; // enumerate, find-by-id, watch
ComponentsService Components; // 21 component readers + collection enumerators
InventoryService Inventory; // scan + iterate + per-item helpers
UiService Ui; // tree walk, FindPanelByStringId, screen-rect
RenderService Render; // WorldToScreen + isometric map projection
TerrainService Terrain; // walkable grid (RAII), height, TGT locations
MemoryService Memory; // direct memory primitives (last resort)
LogService Log; // Debug/Info/Warn/Error
EventsService Events; // Subscribe / Unsubscribe / On<X>
void* ImGuiContext; // pass to ImGui::SetCurrentContext
void* D3DDevice; // ID3D11Device* for texture loading
};ภาพรวมแบบเร็ว ๆ — แต่ละบริการมีไว้ทำอะไร:
| Service | What you reach for it |
|---|---|
GameService |
Snapshot, state flags, screen + window info |
EntitiesService |
Enumerate, find-by-id, watch lifecycle |
ComponentsService |
21 readers + 4 enumerators + ~10 convenience helpers |
InventoryService |
Scan, enumerate, per-item mod reads |
UiService |
Tree walk, FindPanelByStringId, ComputeScreenRect
|
RenderService |
WorldToScreen, GridTo{Large,Mini}Map, transforms |
TerrainService |
Walkable + height grids (RAII), TGT locations |
MemoryService |
RPM primitives — only when no higher-level call fits |
LogService |
Debug / Info / Warn / Error
|
EventsService |
Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
|
ctx() ใช้งานได้ตั้งแต่วินาทีที่โฮสต์เรียก OnEnable จนกระทั่ง OnDisable คืนค่ากลับ ห้าม cache ctx() ข้ามการ hot-reload หรือขอบเขตการ unload DLL
ctx()->Game.GetSnapshot() คืนค่า Snapshot แบบ value type — มุมมองที่สมบูรณ์และไม่เปลี่ยนแปลงของเฟรมปัจจุบัน GetSnapshot() จะเรียก abi->entities.enumerate และเติม snap.Entities ก่อนคืนค่า ดังนั้นต้นทุนจึงปรับตามจำนวนเอนทิตีที่อยู่ใกล้ ๆ ควรเรียกเพียงครั้งเดียวต่อเฟรมและนำกลับมาใช้ใหม่
PluginSDK::Snapshot snap = ctx()->Game.GetSnapshot();
if (snap.State != PluginSDK::GameState::InGame) return;
ctx()->Log.Info(snap.CurrentAreaName.c_str());
if (snap.IsTown || snap.IsHideout) return; // safe area
// snap.Vitals.HPPercent, snap.Vitals.MaxES, snap.Vitals.IsPaused
// snap.Player.GridPositionX, snap.Player.Path (wstring), snap.Player.Components
// snap.Entities is a std::vector<Entity> — every nearby entity, fully populated
// snap.LargeMap / snap.MiniMap — visibility + projection inputs
// snap.AreaChangeCounter — increments each portal transitionข้อมูลที่ snapshot นำมาด้วยโดยตรง (ไม่ต้องเรียกบริการเพิ่ม):
- State + flags:
State,IsAttached,IsWindowValid,GameWindowForeground,IsTown,IsHideout,IsPaused,IsSkillTreeVisible - Area:
CurrentAreaName,CurrentAreaHash,CurrentAreaLevel,AreaChangeCounter - World:
Player(Entityแบบเต็ม),Entities(std::vector<Entity>แบบเต็ม),Vitals,LargeMap,MiniMap,WorldToScreenMatrix[16] - Window:
ScreenWidth,ScreenHeight,ProcessId,GameWindow,LastUpdateTime,WorldToGridConvertor
สิ่งที่ ไม่ได้ อยู่ใน snapshot — ต้องดึงผ่านบริการ: เนื้อหา inventory (InventoryService), buff (ComponentsService::EnumerateBuffs), รายการ mod ต่อไอเทม (InventoryService::ReadItemMods), แผง UI (UiService)
ตัวช่วยราคาถูกเมื่อคุณไม่ต้องการ snapshot แบบเต็ม:
if (ctx()->Game.IsInGame()) { ... }
if (ctx()->Game.IsForeground()) { ... } // game window focused
if (ctx()->Game.IsOverlayMode()) { ... } // host is in overlay (click-through)
if (ctx()->Game.IsMenuVisible()) { ... } // ESC menu, settings, etc.
auto sz = ctx()->Game.GetScreenSize(); // ScreenSize { Width, Height } floats
HWND hw = ctx()->Game.GetGameWindow();
DWORD pid = ctx()->Game.GetProcessId();
PluginSDK::GameState st = ctx()->Game.GetState();เอนทิตีเปิดเผย component ของตนผ่าน entity.Components ซึ่งเป็น struct ComponentAddresses ที่บรรจุ address แบบ uintptr_t ส่งแต่ละ address ให้ ComponentsService::Read* ที่สอดคล้องเพื่อรับ snapshot แบบ value type
for (const auto& e : snap.Entities) {
if (!e.Components.HasLife()) continue;
PluginSDK::Life life = ctx()->Components.ReadLife(e.Components.Life);
if (life.Valid && life.Health.Current > 0) {
ctx()->Log.Info("alive monster");
}
}มีตัวอ่าน component ทั้งหมด 21 ตัว: ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime
ตัว ComponentAddresses มี 24 ช่อง: 21 ตัวข้างต้นบวกกับตัวบ่งชี้สามตัว (Buffs, WorldItem, AreaTransition) และ OMP (สำหรับโฮสต์ภายใน) Buffs เป็นตัวบ่งชี้การมีอยู่ — รายการ buff จริง ๆ มาจาก EnumerateBuffs ส่วน WorldItem / AreaTransition เป็นตัวบ่งชี้ประเภทเอนทิตีมากกว่าเป็น component จริง ทุกช่องมี predicate HasX() ที่ตรงกันใน ComponentAddresses
ตัวอ่านแบบ collection สำหรับ component ที่มีข้อมูลขนาดผันแปร:
auto buffs = ctx()->Components.EnumerateBuffs(e.Components.Buffs); // std::vector<Buff>
auto skills = ctx()->Components.EnumerateActiveSkills(e.Components.Actor); // std::vector<ActiveSkill>
auto stats = ctx()->Components.EnumerateStats(e.Components.Stats); // std::vector<StatEntry>
auto mods = ctx()->Components.EnumerateItemMods(e.Components.Mods); // std::vector<Mod>ตัวช่วยอำนวยความสะดวก (one-shot — เรียก Read* ภายในให้คุณ):
float hpPct = ctx()->Components.GetHealthPercent(e.Components.Life);
bool alive = ctx()->Components.IsAlive(e.Components.Life);
float esPct = ctx()->Components.GetEsPercent(e.Components.Life);
float mpPct = ctx()->Components.GetManaPercent(e.Components.Life);
int rarity = ctx()->Components.GetItemRarity(e.Components.Mods);
bool ident = ctx()->Components.IsItemIdentified(e.Components.Mods);
int stack = ctx()->Components.GetStackCount(e.Components.Stack);
bool open = ctx()->Components.IsChestOpened(e.Components.Chest);
std::string name = ctx()->Components.GetPlayerName(e.Components.Player);
float wx, wy, wz;
if (ctx()->Components.GetWorldPosition(e.Components.Render, wx, wy, wz)) { ... }ทุก struct ที่คืนค่ามี flag Valid ช่วยให้คุณจัดการกรณี "address ของ component เป็น 0 / อ่านล้มเหลว" ได้โดยไม่ต้องใช้ exception หากคุณมี struct หลักอยู่แล้ว (Life, Mods, ...) ให้เข้าถึง field โดยตรงแทนการเรียกตัวช่วยซ้ำ — ตัวช่วยจะอ่าน component ใหม่ทุกครั้ง
ทุก Entity (รวมถึง snap.Player และสมาชิกของ snap.Entities) มี field ชุดเดียวกัน:
| Group | Fields |
|---|---|
| Identity |
Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
|
| Classification |
EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far) |
| Position |
GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
|
| Quick vitals |
CurrentHP, MaxHP, CurrentES, MaxES (avoids a ReadLife if you only need the totals) |
| Strings |
Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, asset path) |
| State |
IsSleeping, IsChestOpened
|
| Components |
Components (ComponentAddresses sub-struct) |
หากต้องการติดตามเอนทิตีเดียวข้ามเฟรม (เช่น หีบที่ผู้เล่นกำลังเปิด) และไม่ต้องการสแกนรายการเอนทิตีทั้งหมดทุกเฟรม ให้ลงทะเบียน watch:
ctx()->Entities.Watch(entityId);
// ...later:
if (auto opt = ctx()->Entities.GetWatchedComponents(entityId)) {
PluginSDK::ComponentAddresses comps = *opt;
PluginSDK::Life l = ctx()->Components.ReadLife(comps.Life);
}
bool active = ctx()->Entities.IsWatched(entityId);
ctx()->Entities.Unwatch(entityId);FindById(id) คืน std::optional<Entity> สำหรับการค้นหาแบบ one-shot และ GetPlayer() คืนผู้เล่นโลคัลเสมอ
ไอเทมที่ถูกทิ้งลงบนพื้นจะปรากฏใน snap.Entities เป็นเอนทิตี EntityType::Item ที่พาธ Metadata/MiscellaneousObjects/WorldItem ซึ่งเป็น เอนทิตี container — ไม่ได้บรรจุ Mods / Base / Stack / Sockets โดยตรง เอนทิตีไอเทมจริงอยู่ห่างออกไปอีกหนึ่ง indirection
เพื่อรับเอนทิตีไอเทมด้านในเป็น snapshot Entity ปกติ ให้ใช้ Entities.GetWorldItemInner:
for (const auto& e : snap.Entities) {
if (e.EntityType != PluginSDK::EntityType::Item) continue;
auto inner = ctx()->Entities.GetWorldItemInner(e.Address);
if (!inner) continue; // mid-spawn, retry next frame
// inner->Path — "Metadata/Items/Armours/Gloves/..."
// inner->Components — Mods / Base / Stack / Sockets / etc.
PluginSDK::Mods mods = ctx()->Components.ReadMods(inner->Components.Mods);
int iLvl = mods.ItemLevel;
int rarity = mods.Rarity;
}GetWorldItemInner สำเร็จเฉพาะกับ WorldItem container จริงเท่านั้น — การเรียกด้วย address ของไอเทมในอินเวนทอรี่จะคืน std::nullopt หากคุณต้องการรูปแบบข้อมูลแบบเดียวกับไอเทมในอินเวนทอรี่ (โดยไม่ต้องเดิน component เอง) กลุ่ม Inventory.ReadItem* ในหัวข้อถัดไปจะ resolve WorldItem container ให้แบบโปร่งใส
ctx()->Inventory.Scan(inventoryId) ทริกเกอร์การสแกนใหม่ฝั่งโฮสต์ ใช้ -1 เพื่อสแกน inventory ทั้งหมด
ctx()->Inventory.Scan(-1);
std::vector<PluginSDK::Inventory> all = ctx()->Inventory.GetAll();
for (const auto& inv : all) {
const char* name = ctx()->Inventory.GetName(inv.InventoryId);
ctx()->Log.Info(name);
for (const auto& item : inv.Items) {
ctx()->Log.Info(item.BaseTypeName.c_str());
// item.SlotX, item.SlotY, item.Width, item.Height (grid metrics)
// item.Rarity, item.ItemLevel, item.RequiredLevel, item.CraftedModCount
// item.IsIdentified, item.IsCorrupted, item.IsCurrency
// item.Path (Metadata/Items/...), item.BaseTypeName, item.UniqueName
// item.Address — entity address for direct lookups below
}
}แต่ละ Inventory ยังเปิดเผย struct Grid ซึ่งอธิบายตำแหน่งที่ inventory ถูกวาดบนหน้าจอ:
if (inv.Grid.Valid) {
float originX = inv.Grid.GridScreenX;
float originY = inv.Grid.GridScreenY;
float cell = inv.Grid.CellSize;
// Slot (x, y) screen-space top-left = (originX + x*cell, originY + y*cell)
}หากต้องการดึง inventory เดี่ยวด้วย id (คืน struct เดิมที่ Items ถูกเติมไว้แล้ว):
PluginSDK::Inventory backpack = ctx()->Inventory.Get(/*inventoryId=*/0);หรือถ้าต้องการเพียง vector ของไอเทมโดยไม่ต้องการ struct ครอบ:
std::vector<PluginSDK::InventoryItem> items = ctx()->Inventory.GetItems(0);ComponentsService::ReadMods(addr) คืนเฉพาะ flag สรุป (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount) ไม่ได้ บรรจุรายการ mod ต่อประเภท
สำหรับภาพรวมทั้งหมด (สรุป + รายการ mod) ให้ใช้ InventoryService::ReadItemMods(entityAddr):
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(item.Address);
if (!im.Valid) return;
// Same summary fields as the Mods component, plus:
for (const auto& m : im.ImplicitMods) { ... } // std::vector<Mod>
for (const auto& m : im.ExplicitMods) { ... }
for (const auto& m : im.EnchantMods) { ... }
for (const auto& m : im.HellscapeMods) { ... }
for (const auto& m : im.CrucibleMods) { ... }การอ่านโดยตรงต่อเอนทิตีอื่น ๆ (ถูกกว่าการสแกนใหม่เมื่อคุณมี address ของไอเทมอยู่แล้ว):
int rarity = ctx()->Inventory.ReadItemRarity(item.Address);
int stack = ctx()->Inventory.ReadItemStackCount(item.Address);
std::string base = ctx()->Inventory.ReadItemBaseTypeName(item.Address);
std::string uniq = ctx()->Inventory.ReadItemUniqueName(item.Address);
std::string path = ctx()->Inventory.ReadItemPath(item.Address);ไอเทมบนพื้นผ่าน inventory API การอ่าน Inventory.ReadItem* ทั้งเจ็ดด้านบน (และ ReadItemMods) รับ ทั้ง address ของไอเทมในอินเวนทอรี่ และ address ของ WorldItem container address ของ container จะถูก resolve เป็นไอเทมด้านในก่อนอ่านโดยอัตโนมัติ ดังนั้นโค้ดปลั๊กอินชุดเดียวกันใช้ได้ทั้งกับไอเทมในกระเป๋าและไอเทมบนพื้น:
// `addr` may be either an inventory item address or a WorldItem container.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int rarity = ctx()->Inventory.ReadItemRarity(addr);
std::string baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);หากคุณต้องการ address ของ component ภายในไอเทมโดยตรง (เช่น เพื่อเรียก ctx()->Components.ReadStack(...) หรือเดิน socket) ให้ใช้ Entities.GetWorldItemInner จากหัวข้อ 7 แทน
ต้นไม้ UI ของเกมถูกเปิดเผยเป็น address ของ element แบบ uintptr_t เริ่มจาก root เดิน child ไป และอ่าน field ของ element
วิธีที่สะอาดในการค้นหาแผงที่รู้จักจาก StringId:
uintptr_t gameUiRoot = ctx()->Ui.GetGameUiRoot();
uintptr_t invPanel = ctx()->Ui.FindPanelByStringId(gameUiRoot, "Inventory");
if (invPanel && ctx()->Ui.IsVisible(invPanel)) {
// panel is on-screen
}การเดินต้นไม้ด้วยมือเมื่อคุณไม่รู้ StringId:
uintptr_t root = ctx()->Ui.GetUiRoot();
PluginSDK::UiElement e = ctx()->Ui.Read(root);
ctx()->Log.Info(("children=" + std::to_string(e.ChildCount)).c_str());
for (uintptr_t child : ctx()->Ui.GetChildren(root)) {
std::string sid = ctx()->Ui.GetStringId(child);
if (sid == "InventoriesPanel") { /* found it */ }
}
// Or use a known index path:
int path[] = { 5, 1, 2, 0 };
uintptr_t logInButton = ctx()->Ui.FollowPath(root, path, 4);
// Compute screen-space rect (post-scale, post-transform):
float x, y, w, h;
if (ctx()->Ui.ComputeScreenRect(invPanel, x, y, w, h)) {
// draw an overlay box at (x,y,w,h)
}
// Get displayed text:
std::string label = ctx()->Ui.GetText(child);
int cull = ctx()->Ui.GetCullValue(); // host's UI cull thresholdค่า StringId เป็นตัวระบุที่เสถียรฝั่งเกม ควรใช้แทน path แบบ hardcode เมื่อมีให้ใช้
มีตัวช่วยการฉายสามตัว ระบบพิกัดสองระบบ
Perspective (โลก 3D → หน้าจอ) — เป็น projection แบบเดียวกับที่เกมใช้วาดสิ่งของในโลก เหมาะสำหรับ nameplate, debug marker, ตัวบ่งชี้เป้าหมาย:
float sx, sy;
if (ctx()->Render.WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, sx, sy)) {
ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, IM_COL32(255,0,0,255));
}Isometric (grid → minimap) — สำหรับ overlay แบบเรดาร์ที่วาดบนแผนที่ใหญ่หรือ minimap ตัวเหล่านี้เคารพการซูม การ pan และการหมุนของแผนที่ที่มองเห็น:
if (!snap.LargeMap.IsVisible) return;
for (const auto& e : snap.Entities) {
float sx, sy;
if (ctx()->Render.GridToLargeMap(e.GridPositionX, e.GridPositionY, e.TerrainHeight, sx, sy)) {
ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, color);
}
}
// Mirror: ctx()->Render.GridToMiniMap(gx, gy, worldZ, sx, sy)สำหรับการคำนวณแบบ batched (ข้ามการเรียกฟังก์ชันต่อเอนทิตี) ให้คว้า transform ครั้งเดียวและทำ projection แบบ inline:
PluginSDK::MapTransform t = ctx()->Render.GetLargeMapTransform();
if (t.IsVisible) {
// dx = gx - t.PlayerGridX; dy = gy - t.PlayerGridY;
// sx = t.CenterX + (dx - dy) * t.ScaleX;
// sy = t.CenterY + (worldZ * worldToGrid - (dx + dy)) * t.ScaleY;
}
// And ctx()->Render.GetMiniMapTransform() for the minimap.ดู Plugins/Radar/src/Radar.cpp สำหรับเรดาร์ที่ใช้งานได้ซึ่งสร้างขึ้นบนการเรียกเหล่านี้ทั้งหมด
ตาราง walkable คือ bitmap แบบ 4-bit ต่อ tile ที่ระบุว่าผู้เล่นสามารถเหยียบเซลล์ภูมิประเทศใดได้บ้าง โฮสต์จะอัปเดตทุกครั้งที่มีการเปลี่ยนพื้นที่ ปลั๊กอินจะได้รับ handle ที่เสถียรซึ่งคงอยู่จนกว่าปลั๊กอินจะปล่อยมัน (ผ่าน RAII)
PluginSDK::WalkableGridHandle h = ctx()->Terrain.GetWalkableGrid();
if (h.Valid()) {
const uint8_t* data = h.Data();
const int w = h.Width();
const int height = h.Height();
const size_t sizeBytes = h.SizeBytes(); // (w * height) / 2
// POE2 packs two cells per byte:
// gx & 1 == 0 → low nibble (data[gy * (w/2) + gx/2] & 0x0F)
// gx & 1 == 1 → high nibble ((data[gy * (w/2) + gx/2] >> 4) & 0x0F)
// Non-zero nibble = walkable.
//
// Always bound your byte index against sizeBytes — the host enforces an
// atomic snapshot, but defense-in-depth has caught at least one real bug.
}
// h is RAII — destructor releases the host reference automatically.HeightGridHandle มีรูปแบบเดียวกันแต่เก็บ float หนึ่งค่าต่อ tile (Data() เป็น const float* พร้อม ElementCount() และ SizeBytes())
อย่าสมัครรับ OnAreaChange เพื่อรีเฟรช handle event จะถูก fire เมื่อ worker ของโฮสต์ตรวจพบการเปลี่ยนพื้นที่ แต่ตาราง walkable ใหม่อาจยังไม่ถูก parse — คุณจะถือ pointer เก่าค้างไว้หนึ่งหรือสองเฟรม แทนที่จะเป็นเช่นนั้น ให้สำรวจทุกเฟรมใน DrawUI:
auto current = ctx()->Terrain.GetWalkableGrid();
if (current.Data() != m_walkable.Data()) {
m_walkable = std::move(current); // swap when the host re-parses
}วิธีนี้ราคาถูก (เรียก ABI หนึ่งครั้ง + เปรียบเทียบ pointer หนึ่งครั้ง) ดูเวอร์ชันโปรดักชันใน Plugins/Radar/src/Radar.cpp
ตัวเข้าถึงภูมิประเทศอื่น ๆ:
bool ok = ctx()->Terrain.IsWalkable(gx, gy);
float worldZ = ctx()->Terrain.GetTerrainHeight(gx, gy);
float worldToG = ctx()->Terrain.GetWorldToGridConvertor();
ctx()->Terrain.EnumerateTgtLocations([](const PluginSDK::TgtLocation& loc) {
// loc.Path, loc.TileX, loc.TileY, loc.X, loc.Y
return true; // continue
});สมัครรับ event ที่ปล่อยมาจากโฮสต์ แต่ละ Subscribe จะคืน Token ที่คุณสามารถนำไปส่งให้ Unsubscribe ในภายหลัง destructor ของ EventsService (ทำงานเมื่อปลั๊กอินถูกปิดใช้งานหรือ unload) จะปล่อย token ที่ค้างอยู่ทั้งหมดอัตโนมัติ ดังนั้นจึงไม่ จำเป็น ต้อง unsubscribe ด้วยมือ แต่เป็นการปฏิบัติที่ดี
class MyPlugin : public PluginSDK::Plugin {
PluginSDK::EventsService::Token m_areaTok{};
PluginSDK::EventsService::Token m_frameTok{};
public:
void OnEnable(bool) override {
auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);
m_areaTok = ev.OnAreaChange([this]{
ctx()->Log.Info("area changed");
});
m_frameTok = ev.OnFrame([this]{
// called every frame; keep work cheap
});
ev.OnGameAttached([this]{ ctx()->Log.Info("game attached"); });
ev.OnGameDetached([this]{ ctx()->Log.Info("game detached"); });
}
void OnDisable() override {
auto& ev = const_cast<PluginSDK::EventsService&>(ctx()->Events);
ev.Unsubscribe(m_areaTok);
ev.Unsubscribe(m_frameTok);
}
};ประเภทของ event มีสี่ชนิดคือ AreaChange, Frame, GameAttached, GameDetached นอกจากนี้ยังมี Subscribe(EventKind, callback) ทั่วไปหากคุณต้องการสร้างตาราง dispatch เอง
const_cast จำเป็นเพราะ Events มีการเปลี่ยนแปลงตาราง token ภายใน base class คืน const Context* เพื่อป้องกันการแก้ไขบริการอื่น ๆ โดยไม่ได้ตั้งใจ
หากปลั๊กอินของคุณเป็นเจ้าของ subscription หลายตัว รูปแบบของ ExamplePlugin เป็นวิธีที่สะอาดในการให้การเปิด/ปิดสมมาตรกัน — รวม token และตัวนับเข้าไว้ใน state struct เดียวและส่งผ่านทุกอย่างผ่านคู่ SubscribeAll/UnsubscribeAll:
struct EventsDemoState {
std::atomic<int> frameCount{0}, areaChangeCount{0};
PluginSDK::EventsService::Token frameTok{}, areaTok{};
bool subscribed = false;
};
void SubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
s.frameTok = ev.OnFrame ([&s]{ s.frameCount.fetch_add(1); });
s.areaTok = ev.OnAreaChange ([&s]{ s.areaChangeCount.fetch_add(1); });
s.subscribed = true;
}
void UnsubscribeAll(const PluginSDK::Context* ctx, EventsDemoState& s) {
auto& ev = const_cast<PluginSDK::EventsService&>(ctx->Events);
ev.Unsubscribe(s.frameTok);
ev.Unsubscribe(s.areaTok);
s.frameTok = {}; s.areaTok = {};
s.subscribed = false;
}ดู Plugins/ExamplePlugin/examples/ExampleEvents.h สำหรับรูปแบบเต็ม
หลักการ: <plugin directory>/config/settings.json Directory() คืน path แบบ absolute UTF-8 ไปยังโฟลเดอร์ปลั๊กอินของคุณ
สำหรับการตั้งค่าง่าย ๆ JSON writer ที่เขียนเองทำงานได้ดีและช่วยให้ DLL พึ่งพาตัวเองได้ ดู Plugins/Radar/src/RadarSettings.h เป็นตัวอย่างที่ใช้งานได้ โครงสร้าง:
struct MySettings {
bool DrawEnabled = true;
float Opacity = 0.9f;
void Save(const std::string& directory) const {
std::filesystem::path p =
std::filesystem::path(directory) / "config" / "settings.json";
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
std::ofstream out(p);
if (!out.is_open()) return;
out << "{\n";
out << " \"DrawEnabled\":" << (DrawEnabled ? "true" : "false") << ",\n";
out << " \"Opacity\":" << Opacity << "\n";
out << "}\n";
}
void Load(const std::string& directory) {
std::filesystem::path p =
std::filesystem::path(directory) / "config" / "settings.json";
if (!std::filesystem::exists(p)) return;
// ... parse ...
}
};
// In your plugin:
void OnEnable(bool) override { m_settings.Load(Directory()); }
void SaveSettings() override { m_settings.Save(Directory()); }สำหรับข้อมูลที่มีโครงสร้าง (object ที่ซ้อนกัน, array) ให้นำ JSON library จริงมาใส่ในโฟลเดอร์ปลั๊กอินของคุณ โฮสต์ไม่ได้บังคับให้เลือก
SaveSettings ถูกเรียกเป็นระยะ (~5 วินาที) และตอนปิดใช้งาน คุณไม่จำเป็นต้องเรียกเอง
ctx()->Log.Debug("verbose detail");
ctx()->Log.Info ("normal status");
ctx()->Log.Warn ("something unexpected");
ctx()->Log.Error("operation failed");
ctx()->Log.Log ("custom-level", "message");ทั้งสี่ระดับจะถูกส่งต่อไปยัง logger หลักของโฮสต์ ข้อความจะปรากฏในแท็บ Logs ของโฮสต์และในไฟล์ log บนดิสก์ จัดรูปแบบข้อความเองก่อนเรียก โฮสต์ไม่รับ printf-style varargs
ภายในเมธอดอำนวยความสะดวกจะปล่อยสตริง "Debug", "Info", "Warning" และ "Error" (Warn map ไปยัง "Warning") bridge ของโฮสต์ทำการเทียบแบบ case-insensitive ดังนั้นปลั๊กอินที่เรียก Log("warn", "msg") ก็ยัง route ถูกต้อง — แต่เมธอดอำนวยความสะดวกชัดเจนกว่า
Memory primitive โดยตรง ควรใช้บริการระดับสูงก่อน หากเป็นไปได้ — บริการเหล่านั้นเข้าใจ offset จัดการการเปลี่ยนแปลง ABI และปลอดภัยจาก SEH การอ่าน memory โดยตรงเหมาะสมเฉพาะเมื่อไม่มีการเรียกระดับสูงสำหรับสิ่งที่คุณต้องการ
// Read a fixed-size value:
uint64_t value = 0;
ctx()->Memory.Read(addr, &value, sizeof(value));
// Read game strings (null-terminated, narrow or wide):
std::string s = ctx()->Memory.ReadString (strAddr);
std::wstring ws = ctx()->Memory.ReadWString(wstrAddr);
// Read a std::wstring container in the game's memory (handles SSO):
std::wstring inner = ctx()->Memory.ReadStdWString(containerAddr);
// Read a std::vector<T>; returns raw bytes you reinterpret_cast:
std::vector<uint8_t> raw = ctx()->Memory.ReadStdVector(vecAddr, sizeof(MyT), /*maxElems=*/1024);
const MyT* items = reinterpret_cast<const MyT*>(raw.data());
size_t count = raw.size() / sizeof(MyT);
// Module info:
uintptr_t base = ctx()->Memory.GetBaseAddress();
uintptr_t sz = ctx()->Memory.GetModuleSize();
uintptr_t pat = ctx()->Memory.GetPatternAddress("GameStates"); // resolves a named patternหากคุณพบว่าตัวเองเรียกใช้สิ่งเหล่านี้บ่อย ๆ ให้ถามว่าข้อมูลที่คุณต้องการควรอยู่ในบริการระดับสูงกว่าหรือไม่
การเรียกข้าม DLL ทุกครั้งระหว่างโฮสต์และปลั๊กอินจะทำงานภายในบล็อก __try / __except ฝั่งโฮสต์ ปลั๊กอินที่มีปัญหาซึ่ง dereference pointer เก่า, หารด้วยศูนย์ หรือเกิด fault ภายใน SDK call จะได้รับ error ที่ถูก log — กระบวนการโฮสต์ ไม่ crash, เกมยังคงทำงาน และผู้ใช้ยังคงใช้ปลั๊กอินอื่นได้
นั่นไม่ได้แปลว่าปลั๊กอินสามารถเขียนแบบสะเพร่า SEH จับอาการ ไม่ได้จับสาเหตุ หากปลั๊กอินของคุณเกิด fault ทุกเฟรม ผู้ใช้จะเห็น error log จำนวนมหาศาลและข้อมูลของคุณจะใช้งานไม่ได้จริง ๆ ให้จัดการ null return จาก SDK call ตรวจสอบ flag Valid ในข้อมูล component และอย่า dereference address แบบ uintptr_t โดยตรง — ส่งผ่าน ComponentsService / Ui / Memory call ที่ wrap RPM ไว้ให้แล้ว
โฮสต์รับมือกับปลั๊กอินที่มีบั๊กได้ แต่รับมือกับปลั๊กอิน DLL ที่ค้างไม่ได้ — DrawSettings ที่ใช้เวลา 100ms จะ block UI thread ทั้งหมด ทำงานต่อเฟรมให้ราคาถูก
รายการสั้น ๆ ของสิ่งที่ผู้เขียนปลั๊กอินมักพบเมื่อเริ่ม integrate ส่วนใหญ่ได้รับการบันทึก inline ด้านบนแล้ว รวบรวมที่นี่เป็น checklist
-
OnAreaChangeถูก fire ก่อนที่ตาราง walkable จะถูก re-parse อย่ารีเฟรชWalkableGridHandleจาก event — ให้สำรวจทุกเฟรมในDrawUIและสลับเมื่อData()เปลี่ยน (§11) -
Entity::Zoneเป็นNoneเสมอสำหรับผู้เล่นโลคัล เป็นการจำแนกตามระยะห่างจากผู้เล่น ดังนั้นโดยนิยามผู้เล่นอยู่ที่ระยะศูนย์ อย่าแสดงในจอแสดงข้อมูลผู้เล่น -
Components.ReadMods()คืนเฉพาะ flag สรุป — ไม่มีรายการ mod สำหรับรายการ mod ต่อประเภท ให้เรียกInventory.ReadItemMods(entityAddr)(§8) -
ไอเทมที่ตกบนพื้นอาจไม่มี
EntitySubtypeหากคุณกรองไอเทมในโลก ให้ใช้EntityType == Item || EntityType == Chestแทนการเช็ค subtype ที่แคบกว่า -
Directory()คืน path แบบ absolute อย่า prepend ไดเรกทอรี EXE เอง — คุณจะได้EXEDIR\EXEDIR\Plugins\Xและการเขียน config จะตกลงนอกโฟลเดอร์ปลั๊กอิน -
ctx()คืนconst Context*เมธอดที่เปลี่ยนแปลงเช่นEventsService::Subscribeต้องใช้const_castนี่เป็นความตั้งใจ — บริการที่ไม่เปลี่ยนแปลงควรเป็นไปไม่ได้ที่จะเปลี่ยนโดยอุบัติเหตุ -
ImGui::SetCurrentContextเป็นต่อ DLL เรียกที่จุดเข้าทุกจุดที่วาด (OnEnable,DrawUI,DrawSettings) เพราะปลั๊กอิน DLL มี state ของ ImGui ของตัวเองโดย default -
ตัวช่วยอำนวยความสะดวกจะอ่าน component ใหม่ทุกครั้ง
GetHealthPercent(addr)จะทำReadLife(addr)ใหม่ภายใน หากคุณมี structLifeจากการเรียกก่อนหน้านี้แล้ว ให้เข้าถึง field โดยตรงแทน
สรุปบรรทัดเดียวของทุก public method ในทุกบริการ ใช้ส่วน prose ด้านบนสำหรับ type signature เต็มและหมายเหตุการใช้งาน
| Method | Returns | Purpose |
|---|---|---|
GetSnapshot() |
Snapshot |
Full per-frame view, including Entities
|
GetState() |
GameState |
Enum: InGame, Login, Loading, … |
IsAttached() |
bool |
Game process attached |
IsInGame() |
bool |
State == InGame |
IsForeground() |
bool |
Game window has focus |
IsMenuVisible() |
bool |
ESC menu / settings open |
IsOverlayMode() |
bool |
Host is in overlay (click-through) |
GetProcessId() |
DWORD |
Game PID |
GetGameWindow() |
HWND |
Game window handle |
GetScreenSize() |
ScreenSize |
{Width, Height} floats |
| Method | Returns | Purpose |
|---|---|---|
Enumerate(cb) |
— | Visit every nearby entity (return false to stop) |
GetPlayer() |
Entity |
The local player entity |
FindById(id) |
std::optional<Entity> |
Lookup by entity id |
GetWorldItemInner(addr) |
std::optional<Entity> |
Inner item entity for a WorldItem container (ground items) |
Watch(id) |
— | Pin an entity so its components stay readable |
Unwatch(id) |
— | Release a watch |
IsWatched(id) |
bool |
Watch state |
GetWatchedComponents(id) |
std::optional<ComponentAddresses> |
Read pinned components |
| Method | Returns | Purpose |
|---|---|---|
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime |
Component struct | 21 readers, one per component type |
EnumerateBuffs(addr) |
std::vector<Buff> |
Active buffs on the entity |
EnumerateActiveSkills(addr) |
std::vector<ActiveSkill> |
Skills from an Actor component |
EnumerateStats(addr) |
std::vector<StatEntry> |
Items + buffs sourced stats |
EnumerateItemMods(addr) |
std::vector<Mod> |
Mods reachable from a Mods component |
GetHealthPercent / GetEsPercent / GetManaPercent |
float |
Convenience % helpers |
IsAlive(addr) |
bool |
Health > 0 |
GetItemRarity(addr) |
int |
Rarity from a Mods component |
IsItemIdentified(addr) |
bool |
Identified flag |
GetStackCount(addr) |
int |
Current stack count |
IsChestOpened(addr) |
bool |
Chest open flag |
GetPlayerName(addr) |
std::string |
Player name from a Player component |
GetWorldPosition(renderAddr, x, y, z) |
bool |
Convenience accessor for world coords |
| Method | Returns | Purpose |
|---|---|---|
Scan(inventoryId) |
— | Trigger a host-side rescan (-1 = all) |
Get(inventoryId) |
Inventory |
One inventory, items already populated |
GetItems(inventoryId) |
std::vector<InventoryItem> |
Items only |
GetAll() |
std::vector<Inventory> |
All scanned inventories |
GetName(inventoryId) |
const char* |
Display name ("Backpack", "Stash", …) |
ReadItemRarity(addr) |
int |
Per-entity rarity (auto-resolves WorldItem containers) |
ReadItemStackCount(addr) |
int |
Per-entity stack (auto-resolves WorldItem containers) |
ReadItemBaseTypeName(addr) |
std::string |
Base type, auto-resolves WorldItem containers |
ReadItemUniqueName(addr) |
std::string |
Unique name, auto-resolves WorldItem containers |
ReadItemPath(addr) |
std::string |
Metadata/Items/... path, auto-resolves WorldItem containers |
ReadItemMods(addr) |
ItemMods |
Summary flags + 5 per-kind mod vectors, auto-resolves WorldItem containers |
| Method | Returns | Purpose |
|---|---|---|
Read(addr) |
UiElement |
Element fields (rect, flags, children count) |
GetChildren(addr) |
std::vector<uintptr_t> |
Child element addresses |
GetChildAt(addr, index) |
uintptr_t |
Single child by index |
FollowPath(root, indices, count) |
uintptr_t |
Walk a known index path |
IsVisible(addr) |
bool |
Element is on-screen |
GetStringId(addr) |
std::string |
Game-side stable identifier |
GetText(addr) |
std::string |
Rendered text |
ComputeScreenRect(addr, x, y, w, h) |
bool |
Final screen-space rect |
GetGameUiRoot() |
uintptr_t |
Root of the in-game UI |
GetUiRoot() |
uintptr_t |
Top-level UI root |
GetCullValue() |
int |
Host's UI cull threshold |
FindPanelByStringId(parent, stringId) |
uintptr_t |
Targeted descendant lookup |
| Method | Returns | Purpose |
|---|---|---|
WorldToScreen(wx, wy, wz, sx, sy) |
bool |
Perspective projection |
GridToLargeMap(gx, gy, worldZ, sx, sy) |
bool |
Project to the large map overlay |
GridToMiniMap(gx, gy, worldZ, sx, sy) |
bool |
Project to the minimap |
GetLargeMapTransform() |
MapTransform |
Pre-multiplied transform for batched math |
GetMiniMapTransform() |
MapTransform |
Same, for the minimap |
| Method | Returns | Purpose |
|---|---|---|
GetWalkableGrid() |
WalkableGridHandle |
RAII handle to the 4-bit-per-tile walkable bitmap |
GetHeightGrid() |
HeightGridHandle |
RAII handle to the per-tile terrain heights |
IsWalkable(gx, gy) |
bool |
Single-tile predicate |
GetTerrainHeight(gx, gy) |
float |
World-space Z |
GetWorldToGridConvertor() |
float |
World → grid conversion factor |
EnumerateTgtLocations(cb) |
— | Visit every TGT instance in the current area |
| Method | Returns | Purpose |
|---|---|---|
Read(addr, buf, size) |
bool |
Raw RPM |
ReadString(addr) |
std::string |
Null-terminated narrow string |
ReadWString(addr) |
std::wstring |
Null-terminated wide string |
ReadStdWString(addr) |
std::wstring |
Reads a game-side std::wstring container (handles SSO) |
ReadStdVector(addr, elemSize, maxElems) |
std::vector<uint8_t> |
Raw bytes; reinterpret as your type |
GetBaseAddress() |
uintptr_t |
Game module base |
GetModuleSize() |
uintptr_t |
Game module size |
GetPatternAddress(name) |
uintptr_t |
Named pattern lookup |
| Method | Purpose |
|---|---|
Debug / Info / Warn / Error(msg) |
Emit at the corresponding level |
Log(level, msg) |
Custom level string |
| Method | Returns | Purpose |
|---|---|---|
Subscribe(kind, cb) |
Token |
Generic dispatch |
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) |
Token |
One-line subscribe helpers |
Unsubscribe(token) |
— | Manual release (destructor auto-releases anyway) |
PluginAbi.h กำหนด:
constexpr int PLUGIN_SDK_VERSION = 6;ในเวลาโหลด โฮสต์จะเรียก plugin->GetSDKVersion() และเปรียบเทียบกับ PLUGIN_SDK_VERSION ของตัวเอง หากไม่ตรงกัน → โฮสต์จะ log คำเตือนและปฏิเสธการโหลดปลั๊กอิน
โฮสต์ยังตรวจสอบ HostAbi::version และ HostAbi::size_bytes ภายใน PluginSDK_AttachHost (ประกาศแบบ inline ใน PluginSDK.h เมื่อกำหนด PLUGIN_EXPORTS) หาก field ใดไม่ตรงกับสิ่งที่ปลั๊กอินถูกสร้างด้วย ctx() จะใช้งานไม่ได้ accessor ของ base class HostCompatible() จะรายงานเป็นเท็จในกรณีนั้น และปลั๊กอินที่สุภาพควรปฏิเสธที่จะทำงาน:
void OnEnable(bool) override {
if (!HostCompatible()) {
ctx()->Log.Error("Host ABI mismatch — disable plugin");
return;
}
// ...
}ปลั๊กอินสี่ตัวใน repo ถูกออกแบบให้อ่านเป็นเอกสารประกอบ:
-
Plugins/ExamplePlugin/— การแสดงตัวอย่างที่ครอบคลุมกว้าง ปลั๊กอินเดียวที่สัมผัสเกือบทุกบริการ จัดระเบียบเป็นไฟล์ย่อยexamples/Example*.h11 ไฟล์ (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) บวกกับแบนเนอร์สรุปความครอบคลุม อ่านเมื่อต้องการเห็นการ ใช้ บริการในบริบท -
Plugins/Radar/— ตัวอย่างที่เน้นการใช้งานจริง ~200 บรรทัด overlay แบบเรดาร์ที่สร้างขึ้นบน public SDK ทั้งหมด — ไม่มี offset ไม่มีการอ่าน memory ดิบ render แผนที่ walkable พร้อมจุดต่อเอนทิตีผ่านRender.GridToLargeMapอ่านเมื่อต้องการดูโค้ด ขั้นต่ำ สำหรับผลลัพธ์เฉพาะ -
Plugins/KillCount/— ตัวติดตามการสังหาร/หีบ/ความตาย SQLite + sprite atlas + state ต่อพื้นที่ แสดงวิธีการจัดส่งการ persist, ไฟล์ข้อมูลแบบ vendored และ overlay ทั้งหมดใน DLL เดียว -
Plugins/NinjaPricer/— overlay ราคาของ poe.ninja HTTP fetch (Exchange API) + inventory scan + การตั้งราคาต่อไอเทม แสดงโค้ดเครือข่าย การรับข้อมูลจากบุคคลที่สาม และการ iterate inventory ใน workflow จริง