Skip to content

Plugin Development Guide JA

Lafko edited this page Jun 24, 2026 · 14 revisions

← Home


プラグイン開発ガイド

POEFixerのプラグインは、Plugins/<PluginName>/<PluginName>.dllから実行時にロードされるネイティブC++ DLLです。ライブなゲーム状態の読み取り、ImGuiオーバーレイの描画、独自の設定の永続化、ホストイベントの購読が可能です。


1. 概要

プラグイン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/


2. Hello-worldプラグイン

ロードされてホストログにメッセージを出力する最小限のプラグインです。

#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タブから有効化します。


3. プロジェクトの設定

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_AttachHostContextの配線を行います。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));

4. ライフサイクルフック

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() (基底に定義されているのでオーバーライド禁止) を呼び出し、プラグインとホストの一致を検証します。不一致の場合、プラグインは拒否されます。


5. Context

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 ツリーウォーク、FindPanelByStringIdComputeScreenRect
RenderService WorldToScreenGridTo{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()をキャッシュしないでください。


6. ゲーム状態の読み取り

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

7. コンポーネントの読み取り

エンティティは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のフィールド

すべてのEntity (snap.Playersnap.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()は常にローカルプレイヤーを返します。

地面アイテム (WorldItemコンテナ)

地面に落ちているアイテムはsnap.Entities内で、パスMetadata/MiscellaneousObjects/WorldItemEntityType::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コンテナを透過的に自動解決します。


8. インベントリ

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

アイテムMod: 2つのAPI、2つのスコープ

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を使ってください。

ゲーム内スタイルのModテキスト + ベース / 集計スタット (v6, 2026-06-24)。 任意のstatキーをゲーム内ツールチップと同じテキストにフォーマットし、アイテムのベース防御値と集計マップ/ウェイストーンプロパティを読み取ります。

// Modをゲームが表示する形式でレンダリングする ("19% increased Monster Damage")。
for (const auto& m : im.ExplicitMods) {
    std::string text = ctx()->Inventory.FormatStat(m.StatKey, m.Value0, m.Value1);
    if (!text.empty()) { /* draw `text` */ }
}

// アイテムのベース防御値。EnergyShieldはゲーム内 (計算済み) の値。
// Ward/Armour/Evasionはアイテムのベース値。アイテムにArmourコンポーネントが
// ない場合 (通貨、ジェム、ジュエリー、ウェイストーンなど) はValid == false。
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// statIdをキーとした集計スタット — 例: ウェイストーンのItem Rarity (8205)、
// Pack Size (8206)、Monster Rarity (8207)、Monster Effectiveness (8208)、
// Waystone Drop Chance (8209)。
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // statIdを自分でラベルにマッピングする; 値は符号付きパーセント
}

FormatStatはホストの.csd stat-descriptionセット (初回使用時にダウンロード) を使うため、そのデータが準備できるまでは空文字列を返します — 生のModフィールドにフォールバックしてください。ReadItemBaseStats / ReadItemAggregatedStatsはどちらもインベントリアイテムのアドレスとWorldItemコンテナのアドレスの両方を受け付けます。


9. UIツリー

ゲームの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 threshold

StringId値はゲーム側で安定した識別子です。存在する場合はハードコードされたパスよりも優先してください。


10. レンダーと投影

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を参照してください。


11. 地形と歩行可能グリッド

歩行可能グリッドはタイルあたり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
});

12. イベント

ホストが発火するイベントを購読します。各SubscribeTokenを返し、後で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を参照してください。


13. 設定の永続化

慣例: <plugin directory>/config/settings.jsonDirectory()はプラグインフォルダへの絶対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秒) および無効化時に呼び出されるので、自分で呼ぶ必要はありません。


14. ロギング

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")と呼んでも正しくルーティングされます。とはいえ、便利メソッドのほうが明瞭です。


15. メモリ (上級者向け)

直接メモリプリミティブです。可能な限り上位サービスを優先してください — オフセットを理解し、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

これらに頻繁に手を伸ばしているなら、必要なデータが上位サービスに属すべきかどうかを問い直してください。


16. ブリッジ / SEH安全性

ホストとプラグインの間のDLL間呼び出しはすべて、ホスト側で__try / __exceptブロック内で実行されます。古いポインタを参照したり、ゼロ除算したり、その他SDK呼び出し内でフォルトを起こすような不行儀なプラグインがあっても、ログにエラーが残るだけで、ホストプロセスはクラッシュしません。ゲームは動き続け、ユーザーは他のプラグインを使い続けられます。

だからといって、プラグインがいいかげんでよいわけではありません。SEHは症状を捕えるだけで、原因は捕えません。プラグインがフレームごとにフォルトを起こせば、ユーザーはエラーログの洪水を見ることになり、データは事実上利用不可能になります。SDK呼び出しのnull戻り値を扱い、コンポーネントデータのValidフラグをチェックし、uintptr_tアドレスを直接参照しないでください — すでにRPMを適切にラップしているComponentsService / Ui / Memory呼び出しを介して渡してください。

ホストはバグのあるプラグインには対処できます。しかし、ハングしたプラグインDLLには対処できません — 100msかかるDrawSettingsはUIスレッド全体をブロックします。フレームごとの作業は安価に保ってください。


17. よくある落とし穴

プラグイン作者が最初に統合するときに遭遇する事柄の短いリスト。これらのほとんどは上記でインラインに文書化されていますが、チェックリストとしてここにまとめています。

  1. OnAreaChangeは歩行可能グリッドが再パースされる前に発火します。 イベントからWalkableGridHandleを更新せず、DrawUI内でフレームごとにポーリングし、Data()が変化したらスワップしてください。(§11)
  2. Entity::Zoneはローカルプレイヤーでは常にNoneです。 プレイヤーからの距離分類なので、プレイヤー自身は定義により距離0です。プレイヤー情報表示には出さないでください。
  3. Components.ReadMods()は要約フラグのみを返します — Modリストはありません。 種類別のModリストには、Inventory.ReadItemMods(entityAddr)を呼んでください。(§8)
  4. 地面に落ちたアイテムはEntitySubtypeを持たない場合があります。 ワールド内のアイテムをフィルタリングするなら、狭いsubtypeチェックよりもEntityType == Item || EntityType == Chestを優先してください。
  5. Directory()は絶対パスを返します。 EXEディレクトリを自分で前置しないでください — そうしないとEXEDIR\EXEDIR\Plugins\Xになり、configの書き込みがプラグインフォルダの外側に着地します。
  6. ctx()const Context*を返します。 EventsService::Subscribeのような変更を伴うメソッドはconst_castが必要です。これは意図的なものです — 変更すべきでないサービスは、誤って変更できないようになっています。
  7. ImGui::SetCurrentContextはDLLごとです。 描画するすべてのエントリポイント (OnEnable, DrawUI, DrawSettings) で呼び出してください。プラグインDLLはデフォルトで独自のImGui状態を持つためです。
  8. 便利ヘルパーは呼び出しのたびにコンポーネントを再読込します。 GetHealthPercent(addr)は内部で新たにReadLife(addr)を行います。すでに以前の呼び出しからLife構造体を持っている場合は、代わりにそのフィールドに直接アクセスしてください。

18. サービスクイックリファレンス

すべてのサービスのすべての公開メソッドの1行要約。完全な型シグネチャと使い方は上記の本文セクションを参照してください。

GameService

メソッド 戻り値 用途
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

EntitiesService

メソッド 戻り値 用途
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> ピン留めされたコンポーネントを読み取り

ComponentsService

メソッド 戻り値 用途
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 ワールド座標の便利アクセサ

InventoryService

メソッド 戻り値 用途
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コンテナを自動解決
FormatStat(statKey, v0, v1) std::string ホストの.csdフォーマッタによるstatキー+値のゲーム内スタイルテキスト; descriptionがロードされるまでは空文字列
ReadItemBaseStats(addr) ItemBaseStats ベース防御値 (計算済みEnergy Shield; ベースWard/Armour/Evasion); Armourコンポーネントがない場合はValid false; WorldItemコンテナを自動解決
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> 集計された{statId, value} (ウェイストーンのItem Rarity 8205 / Pack Size 8206 / Monster Rarity 8207 / Monster Effectiveness 8208 / Waystone Drop Chance 8209); WorldItemコンテナを自動解決

UiService

メソッド 戻り値 用途
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 対象指定された子孫検索

RenderService

メソッド 戻り値 用途
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 同上、ミニマップ用

TerrainService

メソッド 戻り値 用途
GetWalkableGrid() WalkableGridHandle タイルあたり4ビット歩行可能ビットマップへのRAIIハンドル
GetHeightGrid() HeightGridHandle タイルごとの地形高さへのRAIIハンドル
IsWalkable(gx, gy) bool 単一タイル述語
GetTerrainHeight(gx, gy) float ワールド空間Z
GetWorldToGridConvertor() float ワールド → グリッド変換係数
EnumerateTgtLocations(cb) 現在のエリア内のすべてのTGTインスタンスを訪問

MemoryService

メソッド 戻り値 用途
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 名前付きパターン検索

LogService

メソッド 用途
Debug / Info / Warn / Error(msg) 対応するレベルで出力
Log(level, msg) カスタムレベル文字列

EventsService

メソッド 戻り値 用途
Subscribe(kind, cb) Token 汎用ディスパッチ
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token 1行で書ける購読ヘルパー
Unsubscribe(token) 手動解放 (デストラクタが自動解放)

19. バージョニング

PluginAbi.hは以下を定義します。

constexpr int PLUGIN_SDK_VERSION = 6;

ロード時にホストはplugin->GetSDKVersion()を呼び出し、自身のPLUGIN_SDK_VERSIONと比較します。不一致 → ホストは警告をログに出力し、プラグインのロードを拒否します。

ホストはまたPluginSDK_AttachHost内 (PLUGIN_EXPORTSが設定されているときPluginSDK.hによりインラインで定義) でHostAbi::versionHostAbi::size_bytesもチェックします。どちらかのフィールドがプラグインがビルドされたものと一致しない場合、ctx()は機能しません。基底クラスアクセサのHostCompatible()はその場合falseを返し、行儀よくしたいプラグインは動作を拒否すべきです。

void OnEnable(bool) override {
    if (!HostCompatible()) {
        ctx()->Log.Error("Host ABI mismatch — disable plugin");
        return;
    }
    // ...
}

20. サンプルプラグイン

リポジトリの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) + インベントリスキャン + アイテムごとの価格設定。実際のワークフローでのネットワークコード、サードパーティのデータ取り込み、インベントリ反復を示します。


← Home

Clone this wiki locally