-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide JA
POEFixerのプラグインは、Plugins/<PluginName>/<PluginName>.dllから実行時にロードされるネイティブC++ DLLです。ライブなゲーム状態の読み取り、ImGuiオーバーレイの描画、独自の設定の永続化、ホストイベントの購読が可能です。
プラグインSDKは3層アーキテクチャを採用しています。
Plugin DLL ───► PluginSDK.h (header-only C++ wrapper, owns std::string/vector/function)
│
▼ inline function-pointer calls only
HostAbi (pure-C ABI, POD structs only)
│
▼ SEH-wrapped on the host side
Host bridge: plugin_manager/bridge/Bridge_<Service>.cpp (10 files)
│
▼
GameClient + GameLibrary
- プラグイン作者がインクルードするヘッダーは1つだけです:
POEFixer/plugin_sdk/PluginSDK.h。 - このヘッダーは
PluginSDK::名前空間のすべてを宣言し、内部でPluginAbi.hからC ABIを取り込みます。後者の存在には言及できますが、ほとんど目にすることはありません。 - すべての
std::*コンテナはプラグインDLLの中に存在します。ホスト境界を越えるのはPODのみです。これは、あるツールチェーンバージョンでビルドされたプラグインがホストのSTLと絡まることがないことを意味します。共有される型は整数、浮動小数点数、ポインタ、小さな構造体だけです。
SDKヘッダーは以下の場所にあります。
-
POEFixer/plugin_sdk/PluginSDK.h— プラグイン作者が使うC++ラッパー。 -
POEFixer/plugin_sdk/PluginAbi.h— その下にある純粋なC ABI。
リポジトリに同梱されているリファレンスプラグイン(ドキュメントとして読んでください): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/。
ロードされてホストログにメッセージを出力する最小限のプラグインです。
#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を標準テンプレートとして使用してください。必須の設定:
- 構成タイプ: DynamicLibrary
- プラットフォームツールセット: v143 (Visual Studio 2022)
- 文字セット: Unicode
-
言語標準:
stdcpp20 -
ランタイムライブラリ:
MultiThreadedDLL(Release) /MultiThreadedDebugDLL(Debug)。ホストと一致させる必要があります。 -
プリプロセッサ定義:
PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS -
追加のインクルードディレクトリ:
$(SolutionDir)POEFixer -
出力ディレクトリ:
$(SolutionDir)x64\Release\Plugins\<YourPlugin>\ -
ターゲット名: フォルダ名と一致させる必要があります (
Plugins/MyPlugin/→MyPlugin.dll)
ホストはPlugins/内の各サブフォルダをスキャンし、<FolderName>.dllを探します。DLLは次の3つのシンボルをエクスポートする必要があります。
-
CreatePlugin— ファクトリ。PluginSDK::Plugin*を返します。 -
DestroyPlugin— デストラクタ。PluginSDK::Plugin*を受け取ります。 -
PluginSDK_AttachHost—Contextの配線を行います。PluginSDK.h内で定義されており、PLUGIN_EXPORTSが設定されると自動的に出力されます。
推奨されるソースレイアウト:
Plugins/YourPlugin/
YourPlugin.vcxproj
YourPlugin.vcxproj.filters
src/
YourPlugin.cpp // class YourPlugin : public PluginSDK::Plugin
YourPluginSettings.h // Save()/Load() POCO
config/ // runtime-created by SaveSettings()
settings.json
Directory()はホストEXEのディレクトリを起点とする絶対UTF-8パスを返します。自分でEXEパスを前置する必要はありません。ディレクトリ文字列はPluginSDK::Plugin内で値として保持されているため、ホストのコンテナ再配置やリロードサイクルを経てもライフタイムの懸念なく維持されます。
ImGuiを描画したい場合は、<ClCompile>に以下も追加してください(ホストもこれらをリンクしますが、プラグイン側のImGuiはDLLごとに独立しています)。
..\..\POEFixer\imgui\imgui.cpp
..\..\POEFixer\imgui\imgui_draw.cpp
..\..\POEFixer\imgui\imgui_tables.cpp
..\..\POEFixer\imgui\imgui_widgets.cpp
OnEnableでは、ホストのImGuiコンテキストにアタッチします。
if (ctx()->ImGuiContext)
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx()->ImGuiContext));PluginSDK::Pluginは仮想基底クラスです。プラグインで以下をオーバーライドします(おおむね呼び出し順)。
| メソッド | 呼び出されるタイミング | 一般的な用途 |
|---|---|---|
const char* GetName() const |
構築直後に一度 | プラグインの表示名を返す |
void OnEnable(bool isGameAttached) |
ユーザーがプラグインを有効化したとき(または永続化されていた場合は起動時) | 設定の読み込み、イベント購読、ImGuiコンテキストへのアタッチ |
void DrawSettings() |
プラグインの設定パネルが開いている間、毎フレーム | 設定用のImGuiコントロール |
void DrawUI() |
プラグインが有効な間、毎フレーム | ImGuiオーバーレイ描画 (ゲームオーバーレイにはImGui::GetBackgroundDrawList()を使用) |
bool WantsOverlay() const |
毎フレームポーリング | ホストをオーバーレイ (クリックスルー) モードにしたい場合にtrueを返す |
void SaveSettings() |
定期的 (~5秒) および無効化時 | 設定をディスクに永続化 |
void OnDisable() |
ユーザーが無効化したとき、またはホストシャットダウン時 | リソース解放、イベント購読解除 |
必須なのはGetNameのみです。それ以外は安全なデフォルト実装があります。
ホストはCreatePluginの直後にGetSDKVersion() (基底に定義されているのでオーバーライド禁止) を呼び出し、プラグインとホストの一致を検証します。不一致の場合、プラグインは拒否されます。
ctx()はconst PluginSDK::Context*を返し、10個のサービスの集合体です。
struct Context {
GameService Game; // snapshot, state flags, screen size
EntitiesService Entities; // enumerate, find-by-id, watch
ComponentsService Components; // 21 component readers + collection enumerators
InventoryService Inventory; // scan + iterate + per-item helpers
UiService Ui; // tree walk, FindPanelByStringId, screen-rect
RenderService Render; // WorldToScreen + isometric map projection
TerrainService Terrain; // walkable grid (RAII), height, TGT locations
MemoryService Memory; // direct memory primitives (last resort)
LogService Log; // Debug/Info/Warn/Error
EventsService Events; // Subscribe / Unsubscribe / On<X>
void* ImGuiContext; // pass to ImGui::SetCurrentContext
void* D3DDevice; // ID3D11Device* for texture loading
};ひと目で各サービスの用途がわかる一覧:
| サービス | 何のために使うか |
|---|---|
GameService |
スナップショット、状態フラグ、画面・ウィンドウ情報 |
EntitiesService |
列挙、IDによる検索、ライフサイクルの監視 |
ComponentsService |
21個のReader + 4個のEnumerator + 約10個の便利ヘルパー |
InventoryService |
スキャン、列挙、アイテムごとのMod読み取り |
UiService |
ツリーウォーク、FindPanelByStringId、ComputeScreenRect
|
RenderService |
WorldToScreen、GridTo{Large,Mini}Map、変換 |
TerrainService |
歩行可能グリッド・高さグリッド (RAII)、TGT locations |
MemoryService |
RPMプリミティブ — 上位APIが該当しない場合のみ |
LogService |
Debug / Info / Warn / Error
|
EventsService |
Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
|
ctx()はホストがOnEnableを呼んだ瞬間からOnDisableが戻るまで有効です。ホットリロードやDLLアンロード境界をまたいでctx()をキャッシュしないでください。
ctx()->Game.GetSnapshot()は値型のSnapshotを返します。これは現在のフレームの不変ビュー全体です。GetSnapshot()はabi->entities.enumerateをたどり、戻る前にsnap.Entitiesを構築するため、コストは近傍エンティティの数に応じてスケールします。フレームごとに1回だけ呼び出して再利用してください。
PluginSDK::Snapshot snap = ctx()->Game.GetSnapshot();
if (snap.State != PluginSDK::GameState::InGame) return;
ctx()->Log.Info(snap.CurrentAreaName.c_str());
if (snap.IsTown || snap.IsHideout) return; // safe area
// snap.Vitals.HPPercent, snap.Vitals.MaxES, snap.Vitals.IsPaused
// snap.Player.GridPositionX, snap.Player.Path (wstring), snap.Player.Components
// snap.Entities is a std::vector<Entity> — every nearby entity, fully populated
// snap.LargeMap / snap.MiniMap — visibility + projection inputs
// snap.AreaChangeCounter — increments each portal transitionスナップショットが直接持つもの(追加のサービス呼び出し不要):
- 状態とフラグ:
State,IsAttached,IsWindowValid,GameWindowForeground,IsTown,IsHideout,IsPaused,IsSkillTreeVisible。 - エリア:
CurrentAreaName,CurrentAreaHash,CurrentAreaLevel,AreaChangeCounter。 - ワールド:
Player(完全なEntity),Entities(完全なstd::vector<Entity>),Vitals,LargeMap,MiniMap,WorldToScreenMatrix[16]。 - ウィンドウ:
ScreenWidth,ScreenHeight,ProcessId,GameWindow,LastUpdateTime,WorldToGridConvertor。
スナップショットに含まれないもの — サービス経由で取得します: インベントリの内容 (InventoryService)、バフ (ComponentsService::EnumerateBuffs)、アイテムごとのModリスト (InventoryService::ReadItemMods)、UIパネル (UiService)。
フルスナップショットが不要なときの軽量ヘルパー:
if (ctx()->Game.IsInGame()) { ... }
if (ctx()->Game.IsForeground()) { ... } // game window focused
if (ctx()->Game.IsOverlayMode()) { ... } // host is in overlay (click-through)
if (ctx()->Game.IsMenuVisible()) { ... } // ESC menu, settings, etc.
auto sz = ctx()->Game.GetScreenSize(); // ScreenSize { Width, Height } floats
HWND hw = ctx()->Game.GetGameWindow();
DWORD pid = ctx()->Game.GetProcessId();
PluginSDK::GameState st = ctx()->Game.GetState();エンティティはentity.Components(uintptr_tアドレスからなるComponentAddresses構造体)を介してコンポーネントを公開します。それぞれのアドレスを対応するComponentsService::Read*に渡すと値型のスナップショットが得られます。
for (const auto& e : snap.Entities) {
if (!e.Components.HasLife()) continue;
PluginSDK::Life life = ctx()->Components.ReadLife(e.Components.Life);
if (life.Valid && life.Health.Current > 0) {
ctx()->Log.Info("alive monster");
}
}コンポーネントReaderは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個に加えて3つのマーカー (Buffs, WorldItem, AreaTransition) とOMP (ホスト内部用) です。Buffsは存在マーカーで、実際のバフリストはEnumerateBuffsから取得します。WorldItem / AreaTransitionは実際のコンポーネントというよりエンティティタイプのマーカーです。すべてのスロットには対応するHasX()述語がComponentAddressesに存在します。
可変サイズデータを持つコンポーネント用のコレクション形式Reader:
auto buffs = ctx()->Components.EnumerateBuffs(e.Components.Buffs); // std::vector<Buff>
auto skills = ctx()->Components.EnumerateActiveSkills(e.Components.Actor); // std::vector<ActiveSkill>
auto stats = ctx()->Components.EnumerateStats(e.Components.Stats); // std::vector<StatEntry>
auto mods = ctx()->Components.EnumerateItemMods(e.Components.Mods); // std::vector<Mod>便利ヘルパー (一発呼び出し — 内部でRead*を呼び出します):
float hpPct = ctx()->Components.GetHealthPercent(e.Components.Life);
bool alive = ctx()->Components.IsAlive(e.Components.Life);
float esPct = ctx()->Components.GetEsPercent(e.Components.Life);
float mpPct = ctx()->Components.GetManaPercent(e.Components.Life);
int rarity = ctx()->Components.GetItemRarity(e.Components.Mods);
bool ident = ctx()->Components.IsItemIdentified(e.Components.Mods);
int stack = ctx()->Components.GetStackCount(e.Components.Stack);
bool open = ctx()->Components.IsChestOpened(e.Components.Chest);
std::string name = ctx()->Components.GetPlayerName(e.Components.Player);
float wx, wy, wz;
if (ctx()->Components.GetWorldPosition(e.Components.Render, wx, wy, wz)) { ... }返される構造体すべてにValidフラグがあり、「コンポーネントアドレスが0、または読み取り失敗」を例外を使わずに扱えます。すでに親構造体 (Life, Mods, …) を持っている場合は、ヘルパーを再度呼び出すのではなく、そのフィールドに直接アクセスしてください。ヘルパーは呼び出しのたびにコンポーネントを再読込します。
すべてのEntity (snap.Playerやsnap.Entitiesのメンバーを含む) は同じフィールドセットを持ちます。
| グループ | フィールド |
|---|---|
| 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 (合計値だけが必要ならReadLifeを回避できます) |
| Strings |
Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, asset path) |
| State |
IsSleeping, IsChestOpened
|
| Components |
Components (ComponentAddressesサブ構造体) |
複数フレームにわたって1つのエンティティを追跡したい (例: プレイヤーが開けている宝箱) ものの、毎フレーム全エンティティリストをスキャンしたくない場合は、watchを登録します。
ctx()->Entities.Watch(entityId);
// ...later:
if (auto opt = ctx()->Entities.GetWatchedComponents(entityId)) {
PluginSDK::ComponentAddresses comps = *opt;
PluginSDK::Life l = ctx()->Components.ReadLife(comps.Life);
}
bool active = ctx()->Entities.IsWatched(entityId);
ctx()->Entities.Unwatch(entityId);FindById(id)は単発の検索用にstd::optional<Entity>を返します。GetPlayer()は常にローカルプレイヤーを返します。
地面に落ちているアイテムはsnap.Entities内で、パスMetadata/MiscellaneousObjects/WorldItemのEntityType::Itemエンティティとして現れます。これらはコンテナエンティティです — Mods / Base / Stack / Socketsを直接保持しません。本当のアイテムエンティティは1段階の間接参照の先にあります。
内側のアイテムエンティティを通常のEntityスナップショットとして取得するには、Entities.GetWorldItemInnerを使ってください:
for (const auto& e : snap.Entities) {
if (e.EntityType != PluginSDK::EntityType::Item) continue;
auto inner = ctx()->Entities.GetWorldItemInner(e.Address);
if (!inner) continue; // mid-spawn, retry next frame
// inner->Path — "Metadata/Items/Armours/Gloves/..."
// inner->Components — Mods / Base / Stack / Sockets / etc.
PluginSDK::Mods mods = ctx()->Components.ReadMods(inner->Components.Mods);
int iLvl = mods.ItemLevel;
int rarity = mods.Rarity;
}GetWorldItemInnerは本物のWorldItemコンテナでのみ成功します — インベントリアイテムのアドレスで呼び出すとstd::nulloptを返します。インベントリアイテムと同じデータ形状が欲しい場合(コンポーネントを手動でたどらずに)、次のセクションのInventory.ReadItem*ファミリーはWorldItemコンテナを透過的に自動解決します。
ctx()->Inventory.Scan(inventoryId)はホスト側の再スキャンをトリガします。-1を渡すとすべてのインベントリをスキャンします。
ctx()->Inventory.Scan(-1);
std::vector<PluginSDK::Inventory> all = ctx()->Inventory.GetAll();
for (const auto& inv : all) {
const char* name = ctx()->Inventory.GetName(inv.InventoryId);
ctx()->Log.Info(name);
for (const auto& item : inv.Items) {
ctx()->Log.Info(item.BaseTypeName.c_str());
// item.SlotX, item.SlotY, item.Width, item.Height (grid metrics)
// item.Rarity, item.ItemLevel, item.RequiredLevel, item.CraftedModCount
// item.IsIdentified, item.IsCorrupted, item.IsCurrency
// item.Path (Metadata/Items/...), item.BaseTypeName, item.UniqueName
// item.Address — entity address for direct lookups below
}
}各Inventoryは、画面上のどこにインベントリが描画されているかを示すGrid構造体も公開します。
if (inv.Grid.Valid) {
float originX = inv.Grid.GridScreenX;
float originY = inv.Grid.GridScreenY;
float cell = inv.Grid.CellSize;
// Slot (x, y) screen-space top-left = (originX + x*cell, originY + y*cell)
}IDで単一のインベントリを取得するには (Itemsがすでに構築された同じ構造体を返します):
PluginSDK::Inventory backpack = ctx()->Inventory.Get(/*inventoryId=*/0);ラップ構造体なしでアイテムベクタだけが欲しい場合:
std::vector<PluginSDK::InventoryItem> items = ctx()->Inventory.GetItems(0);ComponentsService::ReadMods(addr)が返すのは要約フラグのみです (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) { ... }すでにアイテムアドレスを保持している場合に再スキャンよりも安価なエンティティごとの直接読み取り:
int rarity = ctx()->Inventory.ReadItemRarity(item.Address);
int stack = ctx()->Inventory.ReadItemStackCount(item.Address);
std::string base = ctx()->Inventory.ReadItemBaseTypeName(item.Address);
std::string uniq = ctx()->Inventory.ReadItemUniqueName(item.Address);
std::string path = ctx()->Inventory.ReadItemPath(item.Address);インベントリAPI経由の地面アイテム。 上記の7つのInventory.ReadItem*読み取り (およびReadItemMods) は、インベントリアイテムのアドレスと、WorldItemコンテナのアドレスの両方を受け付けます。コンテナのアドレスは読み取り前に自動的に内側のアイテムへ解決されるため、同じプラグインコードパスがバッグの中のアイテムにも地面のアイテムにも動作します:
// `addr` may be either an inventory item address or a WorldItem container.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int rarity = ctx()->Inventory.ReadItemRarity(addr);
std::string baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);内側のアイテムのコンポーネントアドレスが直接必要な場合 (例えばctx()->Components.ReadStack(...)を呼び出したり、ソケットをたどったりする場合)、代わりにセクション7のEntities.GetWorldItemInnerを使ってください。
ゲームのUIツリーはuintptr_tの要素アドレスとして公開されています。ルートから開始し、子をたどり、要素フィールドを読み取ります。
StringIdで既知のパネルを探すきれいな方法:
uintptr_t gameUiRoot = ctx()->Ui.GetGameUiRoot();
uintptr_t invPanel = ctx()->Ui.FindPanelByStringId(gameUiRoot, "Inventory");
if (invPanel && ctx()->Ui.IsVisible(invPanel)) {
// panel is on-screen
}StringIdがわからない場合の手動ツリーウォーク:
uintptr_t root = ctx()->Ui.GetUiRoot();
PluginSDK::UiElement e = ctx()->Ui.Read(root);
ctx()->Log.Info(("children=" + std::to_string(e.ChildCount)).c_str());
for (uintptr_t child : ctx()->Ui.GetChildren(root)) {
std::string sid = ctx()->Ui.GetStringId(child);
if (sid == "InventoriesPanel") { /* found it */ }
}
// Or use a known index path:
int path[] = { 5, 1, 2, 0 };
uintptr_t logInButton = ctx()->Ui.FollowPath(root, path, 4);
// Compute screen-space rect (post-scale, post-transform):
float x, y, w, h;
if (ctx()->Ui.ComputeScreenRect(invPanel, x, y, w, h)) {
// draw an overlay box at (x,y,w,h)
}
// Get displayed text:
std::string label = ctx()->Ui.GetText(child);
int cull = ctx()->Ui.GetCullValue(); // host's UI cull thresholdStringId値はゲーム側で安定した識別子です。存在する場合はハードコードされたパスよりも優先してください。
3つの投影ヘルパー、2つの座標系。
透視投影 (3Dワールド → 画面) — ゲームがワールド内のオブジェクトを描画するのと同じ投影。ネームプレート、デバッグマーカー、ターゲットインジケータに向いています。
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));
}アイソメトリック (グリッド → ミニマップ) — 大マップやミニマップに描画するレーダースタイルのオーバーレイ用。可視マップのズーム、パン、回転を尊重します。
if (!snap.LargeMap.IsVisible) return;
for (const auto& e : snap.Entities) {
float sx, sy;
if (ctx()->Render.GridToLargeMap(e.GridPositionX, e.GridPositionY, e.TerrainHeight, sx, sy)) {
ImGui::GetBackgroundDrawList()->AddCircleFilled({sx, sy}, 4.f, color);
}
}
// Mirror: ctx()->Render.GridToMiniMap(gx, gy, worldZ, sx, sy)バッチ処理 (エンティティごとの関数呼び出しをスキップ) には、変換を一度取得してインライン投影します。
PluginSDK::MapTransform t = ctx()->Render.GetLargeMapTransform();
if (t.IsVisible) {
// dx = gx - t.PlayerGridX; dy = gy - t.PlayerGridY;
// sx = t.CenterX + (dx - dy) * t.ScaleX;
// sy = t.CenterY + (worldZ * worldToGrid - (dx + dy)) * t.ScaleY;
}
// And ctx()->Render.GetMiniMapTransform() for the minimap.これらの呼び出しのみで構築されたレーダーの動作例はPlugins/Radar/src/Radar.cppを参照してください。
歩行可能グリッドはタイルあたり4ビットのビットマップで、プレイヤーが踏める地形セルを示します。ホストはエリア変更ごとにこれを更新し、プラグインはプラグインが解放するまで存続する安定したハンドル (RAII経由) を受け取ります。
PluginSDK::WalkableGridHandle h = ctx()->Terrain.GetWalkableGrid();
if (h.Valid()) {
const uint8_t* data = h.Data();
const int w = h.Width();
const int height = h.Height();
const size_t sizeBytes = h.SizeBytes(); // (w * height) / 2
// POE2 packs two cells per byte:
// gx & 1 == 0 → low nibble (data[gy * (w/2) + gx/2] & 0x0F)
// gx & 1 == 1 → high nibble ((data[gy * (w/2) + gx/2] >> 4) & 0x0F)
// Non-zero nibble = walkable.
//
// Always bound your byte index against sizeBytes — the host enforces an
// atomic snapshot, but defense-in-depth has caught at least one real bug.
}
// h is RAII — destructor releases the host reference automatically.HeightGridHandleは同じ形状ですが、タイルあたり1つのfloatを保持します (Data()はconst float*、加えてElementCount()とSizeBytes())。
ハンドル更新のためにOnAreaChangeを購読しないでください。 イベントはホストワーカーがエリア変更を検出したときに発火しますが、新しい歩行可能グリッドがまだパースされていない場合があります。1〜2フレームの間、古いポインタを保持してしまいます。代わりに、DrawUI内でフレームごとにポーリングしてください。
auto current = ctx()->Terrain.GetWalkableGrid();
if (current.Data() != m_walkable.Data()) {
m_walkable = std::move(current); // swap when the host re-parses
}これは安価です (ABI呼び出し1回 + ポインタ比較1回)。プロダクションバージョンはPlugins/Radar/src/Radar.cppを参照してください。
他の地形アクセサ:
bool ok = ctx()->Terrain.IsWalkable(gx, gy);
float worldZ = ctx()->Terrain.GetTerrainHeight(gx, gy);
float worldToG = ctx()->Terrain.GetWorldToGridConvertor();
ctx()->Terrain.EnumerateTgtLocations([](const PluginSDK::TgtLocation& loc) {
// loc.Path, loc.TileX, loc.TileY, loc.X, loc.Y
return true; // continue
});ホストが発火するイベントを購読します。各SubscribeはTokenを返し、後でUnsubscribeに渡せます。EventsServiceのデストラクタ (プラグインが無効化またはアンロードされたときに発火) は残っているものを自動解放するので、厳密には手動で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);
}
};4種類のイベントはAreaChange, Frame, GameAttached, GameDetachedです。ディスパッチテーブルを構築したい場合は汎用のSubscribe(EventKind, callback)もあります。
const_castが必要なのは、Eventsが内部トークンマップを変更するためです。基底クラスはconst Context*を返すことで、他のサービスを誤って変更してしまうことを防ぎます。
プラグインが複数の購読を所有する場合、ExamplePluginのパターンが有効化/無効化を対称に保つきれいな方法です — トークンとカウンタを単一の状態構造体にまとめ、すべてを1つの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()はプラグインフォルダへの絶対UTF-8パスを返します。
簡単な設定なら手書きのJSONライターで十分で、DLLを自己完結に保てます。動く例はPlugins/Radar/src/RadarSettings.hを参照してください。スケルトン:
struct MySettings {
bool DrawEnabled = true;
float Opacity = 0.9f;
void Save(const std::string& directory) const {
std::filesystem::path p =
std::filesystem::path(directory) / "config" / "settings.json";
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
std::ofstream out(p);
if (!out.is_open()) return;
out << "{\n";
out << " \"DrawEnabled\":" << (DrawEnabled ? "true" : "false") << ",\n";
out << " \"Opacity\":" << Opacity << "\n";
out << "}\n";
}
void Load(const std::string& directory) {
std::filesystem::path p =
std::filesystem::path(directory) / "config" / "settings.json";
if (!std::filesystem::exists(p)) return;
// ... parse ...
}
};
// In your plugin:
void OnEnable(bool) override { m_settings.Load(Directory()); }
void SaveSettings() override { m_settings.Save(Directory()); }構造化データ (ネストされたオブジェクト、配列) には、プラグインフォルダ内に実際のJSONライブラリをベンダー化してください。ホストは選択を強制しません。
SaveSettingsは定期的 (~5秒) および無効化時に呼び出されるので、自分で呼ぶ必要はありません。
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");4つのレベルすべてがホストの中央ロガーにルーティングされます。メッセージはホストのLogsタブとディスク上のログファイルに表示されます。呼び出し前に自分でフォーマットしてください。ホストはprintf形式の可変長引数を受け付けません。
内部的には、便利メソッドは文字列"Debug", "Info", "Warning", "Error"を出力します (Warnは"Warning"にマップ)。ホストブリッジは大文字小文字を無視した照合を行うので、プラグインがLog("warn", "msg")と呼んでも正しくルーティングされます。とはいえ、便利メソッドのほうが明瞭です。
直接メモリプリミティブです。可能な限り上位サービスを優先してください — オフセットを理解し、ABI変更に対応し、SEH安全です。直接メモリ読み取りが適切なのは、必要なものに対応する上位呼び出しが存在しない場合だけです。
// Read a fixed-size value:
uint64_t value = 0;
ctx()->Memory.Read(addr, &value, sizeof(value));
// Read game strings (null-terminated, narrow or wide):
std::string s = ctx()->Memory.ReadString (strAddr);
std::wstring ws = ctx()->Memory.ReadWString(wstrAddr);
// Read a std::wstring container in the game's memory (handles SSO):
std::wstring inner = ctx()->Memory.ReadStdWString(containerAddr);
// Read a std::vector<T>; returns raw bytes you reinterpret_cast:
std::vector<uint8_t> raw = ctx()->Memory.ReadStdVector(vecAddr, sizeof(MyT), /*maxElems=*/1024);
const MyT* items = reinterpret_cast<const MyT*>(raw.data());
size_t count = raw.size() / sizeof(MyT);
// Module info:
uintptr_t base = ctx()->Memory.GetBaseAddress();
uintptr_t sz = ctx()->Memory.GetModuleSize();
uintptr_t pat = ctx()->Memory.GetPatternAddress("GameStates"); // resolves a named patternこれらに頻繁に手を伸ばしているなら、必要なデータが上位サービスに属すべきかどうかを問い直してください。
ホストとプラグインの間のDLL間呼び出しはすべて、ホスト側で__try / __exceptブロック内で実行されます。古いポインタを参照したり、ゼロ除算したり、その他SDK呼び出し内でフォルトを起こすような不行儀なプラグインがあっても、ログにエラーが残るだけで、ホストプロセスはクラッシュしません。ゲームは動き続け、ユーザーは他のプラグインを使い続けられます。
だからといって、プラグインがいいかげんでよいわけではありません。SEHは症状を捕えるだけで、原因は捕えません。プラグインがフレームごとにフォルトを起こせば、ユーザーはエラーログの洪水を見ることになり、データは事実上利用不可能になります。SDK呼び出しのnull戻り値を扱い、コンポーネントデータのValidフラグをチェックし、uintptr_tアドレスを直接参照しないでください — すでにRPMを適切にラップしているComponentsService / Ui / Memory呼び出しを介して渡してください。
ホストはバグのあるプラグインには対処できます。しかし、ハングしたプラグインDLLには対処できません — 100msかかるDrawSettingsはUIスレッド全体をブロックします。フレームごとの作業は安価に保ってください。
プラグイン作者が最初に統合するときに遭遇する事柄の短いリスト。これらのほとんどは上記でインラインに文書化されていますが、チェックリストとしてここにまとめています。
-
OnAreaChangeは歩行可能グリッドが再パースされる前に発火します。 イベントからWalkableGridHandleを更新せず、DrawUI内でフレームごとにポーリングし、Data()が変化したらスワップしてください。(§11) -
Entity::Zoneはローカルプレイヤーでは常にNoneです。 プレイヤーからの距離分類なので、プレイヤー自身は定義により距離0です。プレイヤー情報表示には出さないでください。 -
Components.ReadMods()は要約フラグのみを返します — Modリストはありません。 種類別のModリストには、Inventory.ReadItemMods(entityAddr)を呼んでください。(§8) -
地面に落ちたアイテムは
EntitySubtypeを持たない場合があります。 ワールド内のアイテムをフィルタリングするなら、狭いsubtypeチェックよりもEntityType == Item || EntityType == Chestを優先してください。 -
Directory()は絶対パスを返します。 EXEディレクトリを自分で前置しないでください — そうしないとEXEDIR\EXEDIR\Plugins\Xになり、configの書き込みがプラグインフォルダの外側に着地します。 -
ctx()はconst Context*を返します。EventsService::Subscribeのような変更を伴うメソッドはconst_castが必要です。これは意図的なものです — 変更すべきでないサービスは、誤って変更できないようになっています。 -
ImGui::SetCurrentContextはDLLごとです。 描画するすべてのエントリポイント (OnEnable,DrawUI,DrawSettings) で呼び出してください。プラグインDLLはデフォルトで独自のImGui状態を持つためです。 -
便利ヘルパーは呼び出しのたびにコンポーネントを再読込します。
GetHealthPercent(addr)は内部で新たにReadLife(addr)を行います。すでに以前の呼び出しからLife構造体を持っている場合は、代わりにそのフィールドに直接アクセスしてください。
すべてのサービスのすべての公開メソッドの1行要約。完全な型シグネチャと使い方は上記の本文セクションを参照してください。
| メソッド | 戻り値 | 用途 |
|---|---|---|
GetSnapshot() |
Snapshot |
フレームごとの完全ビュー、Entitiesを含む |
GetState() |
GameState |
Enum: InGame, Login, Loading, … |
IsAttached() |
bool |
ゲームプロセスにアタッチ済み |
IsInGame() |
bool |
State == InGame |
IsForeground() |
bool |
ゲームウィンドウにフォーカス |
IsMenuVisible() |
bool |
ESCメニュー / 設定オープン |
IsOverlayMode() |
bool |
ホストがオーバーレイ (クリックスルー) |
GetProcessId() |
DWORD |
ゲームPID |
GetGameWindow() |
HWND |
ゲームウィンドウハンドル |
GetScreenSize() |
ScreenSize |
{Width, Height} floats |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Enumerate(cb) |
— | 近傍エンティティをすべて訪問 (停止するにはfalseを返す) |
GetPlayer() |
Entity |
ローカルプレイヤーエンティティ |
FindById(id) |
std::optional<Entity> |
エンティティIDで検索 |
GetWorldItemInner(addr) |
std::optional<Entity> |
WorldItemコンテナの内側のアイテムエンティティ (地面アイテム) |
Watch(id) |
— | エンティティをピン留めしてコンポーネントを読み取り可能に保つ |
Unwatch(id) |
— | watchを解除 |
IsWatched(id) |
bool |
watch状態 |
GetWatchedComponents(id) |
std::optional<ComponentAddresses> |
ピン留めされたコンポーネントを読み取り |
| メソッド | 戻り値 | 用途 |
|---|---|---|
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime |
Component struct | 21個のReader、コンポーネントタイプごとに1つ |
EnumerateBuffs(addr) |
std::vector<Buff> |
エンティティ上のアクティブなバフ |
EnumerateActiveSkills(addr) |
std::vector<ActiveSkill> |
Actorコンポーネントからのスキル |
EnumerateStats(addr) |
std::vector<StatEntry> |
アイテムとバフから取得したstats |
EnumerateItemMods(addr) |
std::vector<Mod> |
Modsコンポーネントから到達できるmod |
GetHealthPercent / GetEsPercent / GetManaPercent |
float |
便利な%ヘルパー |
IsAlive(addr) |
bool |
Health > 0 |
GetItemRarity(addr) |
int |
Modsコンポーネントからのレアリティ |
IsItemIdentified(addr) |
bool |
識別済みフラグ |
GetStackCount(addr) |
int |
現在のスタック数 |
IsChestOpened(addr) |
bool |
宝箱の開封フラグ |
GetPlayerName(addr) |
std::string |
Playerコンポーネントからのプレイヤー名 |
GetWorldPosition(renderAddr, x, y, z) |
bool |
ワールド座標の便利アクセサ |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Scan(inventoryId) |
— | ホスト側の再スキャンをトリガ (-1 = 全部) |
Get(inventoryId) |
Inventory |
1つのインベントリ、アイテム構築済み |
GetItems(inventoryId) |
std::vector<InventoryItem> |
アイテムのみ |
GetAll() |
std::vector<Inventory> |
スキャン済み全インベントリ |
GetName(inventoryId) |
const char* |
表示名 ("Backpack", "Stash", …) |
ReadItemRarity(addr) |
int |
エンティティごとのレアリティ (WorldItemコンテナを自動解決) |
ReadItemStackCount(addr) |
int |
エンティティごとのスタック (WorldItemコンテナを自動解決) |
ReadItemBaseTypeName(addr) |
std::string |
Base type、WorldItemコンテナを自動解決 |
ReadItemUniqueName(addr) |
std::string |
Unique名、WorldItemコンテナを自動解決 |
ReadItemPath(addr) |
std::string |
Metadata/Items/...パス、WorldItemコンテナを自動解決 |
ReadItemMods(addr) |
ItemMods |
要約フラグ + 種類別modベクタ5つ、WorldItemコンテナを自動解決 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Read(addr) |
UiElement |
要素のフィールド (rect, flags, 子の数) |
GetChildren(addr) |
std::vector<uintptr_t> |
子要素のアドレス |
GetChildAt(addr, index) |
uintptr_t |
インデックスによる単一の子 |
FollowPath(root, indices, count) |
uintptr_t |
既知のインデックスパスをたどる |
IsVisible(addr) |
bool |
要素が画面上にある |
GetStringId(addr) |
std::string |
ゲーム側の安定した識別子 |
GetText(addr) |
std::string |
レンダリングされたテキスト |
ComputeScreenRect(addr, x, y, w, h) |
bool |
最終的なスクリーン空間rect |
GetGameUiRoot() |
uintptr_t |
ゲーム内UIのルート |
GetUiRoot() |
uintptr_t |
トップレベルUIルート |
GetCullValue() |
int |
ホストのUI cullしきい値 |
FindPanelByStringId(parent, stringId) |
uintptr_t |
対象指定された子孫検索 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
WorldToScreen(wx, wy, wz, sx, sy) |
bool |
透視投影 |
GridToLargeMap(gx, gy, worldZ, sx, sy) |
bool |
大マップオーバーレイへの投影 |
GridToMiniMap(gx, gy, worldZ, sx, sy) |
bool |
ミニマップへの投影 |
GetLargeMapTransform() |
MapTransform |
バッチ算術用の事前計算済み変換 |
GetMiniMapTransform() |
MapTransform |
同上、ミニマップ用 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
GetWalkableGrid() |
WalkableGridHandle |
タイルあたり4ビット歩行可能ビットマップへのRAIIハンドル |
GetHeightGrid() |
HeightGridHandle |
タイルごとの地形高さへのRAIIハンドル |
IsWalkable(gx, gy) |
bool |
単一タイル述語 |
GetTerrainHeight(gx, gy) |
float |
ワールド空間Z |
GetWorldToGridConvertor() |
float |
ワールド → グリッド変換係数 |
EnumerateTgtLocations(cb) |
— | 現在のエリア内のすべてのTGTインスタンスを訪問 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Read(addr, buf, size) |
bool |
生のRPM |
ReadString(addr) |
std::string |
null終端ナロー文字列 |
ReadWString(addr) |
std::wstring |
null終端ワイド文字列 |
ReadStdWString(addr) |
std::wstring |
ゲーム側std::wstringコンテナを読む (SSO対応) |
ReadStdVector(addr, elemSize, maxElems) |
std::vector<uint8_t> |
生バイト。自分の型として再解釈 |
GetBaseAddress() |
uintptr_t |
ゲームモジュールのベース |
GetModuleSize() |
uintptr_t |
ゲームモジュールのサイズ |
GetPatternAddress(name) |
uintptr_t |
名前付きパターン検索 |
| メソッド | 用途 |
|---|---|
Debug / Info / Warn / Error(msg) |
対応するレベルで出力 |
Log(level, msg) |
カスタムレベル文字列 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Subscribe(kind, cb) |
Token |
汎用ディスパッチ |
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) |
Token |
1行で書ける購読ヘルパー |
Unsubscribe(token) |
— | 手動解放 (デストラクタが自動解放) |
PluginAbi.hは以下を定義します。
constexpr int PLUGIN_SDK_VERSION = 6;ロード時にホストはplugin->GetSDKVersion()を呼び出し、自身のPLUGIN_SDK_VERSIONと比較します。不一致 → ホストは警告をログに出力し、プラグインのロードを拒否します。
ホストはまたPluginSDK_AttachHost内 (PLUGIN_EXPORTSが設定されているときPluginSDK.hによりインラインで定義) でHostAbi::versionとHostAbi::size_bytesもチェックします。どちらかのフィールドがプラグインがビルドされたものと一致しない場合、ctx()は機能しません。基底クラスアクセサのHostCompatible()はその場合falseを返し、行儀よくしたいプラグインは動作を拒否すべきです。
void OnEnable(bool) override {
if (!HostCompatible()) {
ctx()->Log.Error("Host ABI mismatch — disable plugin");
return;
}
// ...
}リポジトリの4つのプラグインはドキュメントとして読むことを想定しています。
-
Plugins/ExamplePlugin/— 広範囲なショーケース。ほぼすべてのサービスに触れる1つのプラグインで、11個のexamples/Example*.hサブファイル (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) とカバレッジサマリーバナーで構成されます。サービスがコンテキストの中でどのように使われるかを見たいときに読んでください。 -
Plugins/Radar/— 焦点を絞った実世界の例。約200行。公開SDKのみで構築されたレーダーオーバーレイ — オフセットなし、生のメモリ読み取りなし。Render.GridToLargeMapを介して歩行可能マップとエンティティごとのドットを描画します。特定の成果のための最小コードを見たいときに読んでください。 -
Plugins/KillCount/— kill/chest/death トラッカー。SQLite + スプライトアトラス + エリアごとの状態。永続化、ベンダー化されたデータファイル、オーバーレイをすべて1つのDLLに同梱する方法を示します。 -
Plugins/NinjaPricer/— poe.ninja価格オーバーレイ。HTTP fetch (Exchange API) + インベントリスキャン + アイテムごとの価格設定。実際のワークフローでのネットワークコード、サードパーティのデータ取り込み、インベントリ反復を示します。