Skip to content

Plugin Development Guide JA

Lafko edited this page Jul 7, 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  (16 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*を返し、16個のサービスの集合体です。

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>
    OverlayService    Overlay;     // SetIncludeSleepingEntities / SetWantsOverlayInput
    FlasksService     Flasks;       // life/mana flasks + charms: charges, usable, active
    PricesService     Prices;       // poe2scout item prices (host-loaded once, shared)
    RuneshapeService  Runeshape;    // Expedition2Encounter devices + per-device rewards
    AtlasService      Atlas;        // endgame-atlas nodes / adjacency / Rite selection / weights
    SekhemaService    Sekhema;      // Trial-of-the-Sekhemas floor graph / choices / content FKs
    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}
OverlayService SetIncludeSleepingEntities / SetWantsOverlayInput — マップピッカー対応
FlasksService ライフ/マナフラスコ + チャーム — チャージ数、UsableActive、使用あたり、Mod数
PricesService LookupPrice / GetRates / GetStatus — ホストが読み込むpoe2scoutの価格、全プラグイン共有
RuneshapeService Runeshapes / Rewards — 解決済みのExpedition2Encounterデバイス + デバイスごとの報酬
AtlasService GetPanel / Nodes / Connections / Selection / GetLineSeed / Weights — エンドゲームアトラスパネルのライブデータ
SekhemaService GetPanel / GetFloor / Rooms / Content / 部屋フラグの読み取り — Trial of the Sekhemasのフロアマップデータ

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

GetHivebloodはGenesisツリー (Hiveblood) のリソースカウンターを読み取ります — GameService経由でルーティングされるhost-tail読み取りです (GetGold / GetAreaIdと同じファミリー):

int32_t hiveblood = 0;
if (ctx()->Game.GetHiveblood(hiveblood)) {
    // in a Genesis map: hiveblood holds the current resource count
}
// Returns false (leaving the out param untouched) when not in game, the chain
// is broken, or the host predates the tail function — always gate on the return.

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, …) を持っている場合は、ヘルパーを再度呼び出すのではなく、そのフィールドに直接アクセスしてください。ヘルパーは呼び出しのたびにコンポーネントを再読込します。

地面エフェクトの識別 — 多くの地面エフェクトは単一のエンティティパス Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect を共有しているため、パスだけではShocked GroundとBurning Groundを区別できません。ReadGroundEffect はエンティティの GroundEffect コンポーネントとその groundeffects.datc64 行を解決します。エンティティアドレスを渡してください (GroundEffect コンポーネントは Components に含まれていないため、ホストが代わりに解決します — ReadPathfinding と同じ規約です)。その後、パッチをまたいで安定したキーである TypeId でマッチさせます:

for (const auto& e : snap.Entities) {
    if (e.Path != L"Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect") continue;
    PluginSDK::GroundEffect ge = ctx()->Components.ReadGroundEffect(e.Address);
    if (!ge.Valid) continue;
    // ge.TypeId -> "ShockedGround" / "IgnitedGround" / "CausticCloud" / "ChilledGround" / ...
    // ge.Radius -> ワールド単位; (e.WorldX, e.WorldY, e.WorldZ) を中心にこの半径の円を描く
    if (ge.TypeId == "ShockedGround") {
        // 設定に応じてハイライト (color/alpha は ge.TypeId でキー)
    }
}

GroundEffect 構造体:

フィールド 意味
Valid エンティティに GroundEffect コンポーネントがないか読み取りに失敗した場合は false
TypeId groundeffecttypes Id — マッチに使う安定したキー (例: ShockedGround)
Radius ワールド単位でのエフェクト半径; バリアントが未設定の場合は 0
EndEffect 終了時の挙動: fadeout / close / end
BuffVisual1 buffvisuals Id (例: ground_fire_burn_white); 未設定の場合は空
BuffVisual2 buffdefinitions Name (例: ground_tar_gold); 未設定の場合は空
AoFile 最初の .ao/.aoc ビジュアルパス; なければ空
GroundEffectsRowAddr / GroundEffectTypesRowAddr 高度なクロスリファレンス用のdat行ポインタ (セッション内安定)

エフェクトのワールド座標はエンティティ自身から取得します (Entity.WorldX/Y/Z、または Render/Positioned コンポーネント) ので、この構造体には重複して含まれません。ReadGroundEffect は呼び出しのたびに再読込するため、スキャンインターバルごとに結果をキャッシュしてください。このAPIより前のホストビルドでは無効な GroundEffect を返します (SDK v6 のappend-onlyテールに存在し、nullチェックされます)。

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

0.5.xに関する注意。 Ui.GetStringId()は現行 (0.5.x) クライアントで正しい識別子を返します — 要素のStringIdフィールドのオフセットが移動し (0x4480x4C0)、ホストブリッジもそれに合わせて修正されました。(トライアルHUDのフィールドリーフが値をレンダリングする数値のStringId — GetText()とは別のフィールド — については、SekhemaHelperctx()->Sekhema.GetUiStringId()を読み取ります。)


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. OverlayService — 入力キャプチャとスリーピングエンティティ

ctx()->Overlay は、オーバーレイ全体の挙動を変更するプラグインごとの「リクエストフラグ」を2つ公開します。ホストはプラグインごとの状態をthisポインタをキーとして保持し、ホスト自身のビルトインフラグ (メインメニューの可視性、AutoCraftロック、ホストビルトインのマップからエンティティ追加ピッカー、他のすべてのプラグインのリクエスト) とORで集約します。プラグインの無効化/アンロード時に自動クリアされます — クラッシュしたプラグインやバグのあるプラグインが、オーバーレイを異常な状態に永続的に固定してしまうことはありません。

SetIncludeSleepingEntities

デフォルトでは、EntitiesService.EnumerateSnapshot.EntitiesEntityState::Useless のエンティティを非表示にします (遠くにいる休眠中のモンスター/NPC/宝箱を除外するホストの「スリーピング」フィルター)。これによりフレームごとのスナップショットコストが制限されます — 典型的なエリアには、プラグインが気にしない数百のUselessエンティティが存在します。

エリアの完全なエンティティプールが必要なマップピッカーUIやデバッグビューア (まだアクティブでないエンティティをユーザーがクリックできるよう) では、フィルターをオフにしてください:

ctx()->Overlay.SetIncludeSleepingEntities(true);
// これで ctx()->Entities.Enumerate は Useless エンティティも見えるようになります。
// Entity::IsSleeping はホストの別の SleepingEntities コレクション由来の
// エンティティを特定的にマークします (EntityState::Useless とは直交)。

コスト: 有効中はフレームあたり約+5〜15%のスナップショットCPU。必要でなければOFFのままにしてください。

SetWantsOverlayInput

オーバーレイウィンドウは通常クリックスルー (WS_EX_TRANSPARENT) です: すべてのマウスクリックは下のゲームにそのまま通過します。これは読み取り専用オーバーレイ (レーダー、ヘルスバー、DPS表示) の正しいデフォルトです — プレイヤーはオーバーレイに気づかずプレイを続けられます。

プラグインがユーザーにオーバーレイ内の何かをクリックさせたい瞬間 — ポップアップの確認、マップ上のエンティティの選択、マーカーのドラッグ — そのデフォルトは機能しなくなります。SetWantsOverlayInput(true) は、ImGuiウィンドウがカバーしている場所でマウスクリックを消費するようホストに依頼します:

ctx()->Overlay.SetWantsOverlayInput(true);
// ...
// 完了時 (ユーザーが選択した、ポップアップを閉じた、Escapeを押した):
ctx()->Overlay.SetWantsOverlayInput(false);

ホストのフレームごとのロジックは、カーソルが可視のImGuiウィンドウ上にある場所のみクリックを消費します — それ以外の場所ではクリックスルーが維持されるため、プレイヤーはポップアップの周りで移動/攻撃/ルート収集を続けられます。

スコープ (重要):

  • マウスボタン (LMB/RMB): YES — このフラグでゲートされます。
  • マウス位置/ホバー: フラグに関わらず常に機能します。ホバーツールチップにはこのフラグは不要です。
  • キーボード: ホストのWindowProc経由で常にプラグインに届きます。このフラグとは独立しています。ImGui::IsKeyPressed(ImGuiKey_Escape) はどちらの場合でも機能します。

Background-draw-list の注意点 — マップピッカープラグインは必ずお読みください

ImGui::GetBackgroundDrawList() を使ってクリッカブルなマーカーを描画する場合 (レーダー/大型マップオーバーレイでよく使われます)、バックグラウンドドローリストにはImGuiウィンドウのバッキングがありません。ホストのヒットテストは ctx->Windows を走査し、カーソル下に何もないと判断してクリックスルーを再有効化します — マーカーは見えていますがクリックできません。

修正方法: ピッカーエリアをカバーする本物のImGuiウィンドウを開き、その中に ImGui::InvisibleButton を配置してください。ウィンドウがホストのヒットテスト対象となり、InvisibleButtonで ImGui::IsItemClicked() によるクリック検出が可能になります。スケッチ:

auto snap = ctx()->Game.GetSnapshot();
ImVec2 screenSize{(float)snap.ScreenWidth, (float)snap.ScreenHeight};

ImGui::SetNextWindowPos({0, 0});
ImGui::SetNextWindowSize(screenSize);
ImGui::Begin("##picker", nullptr,
    ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoTitleBar |
    ImGuiWindowFlags_NoMove       | ImGuiWindowFlags_NoResize    |
    ImGuiWindowFlags_NoScrollbar);

ImGui::InvisibleButton("##picker_hit", screenSize);
bool clickedThisFrame = ImGui::IsItemClicked();

// ImGui::GetForegroundDrawList() (または GetWindowDrawList()) でマーカーを描画:
ImDrawList* dl = ImGui::GetForegroundDrawList();
ctx()->Terrain.EnumerateTgtLocations([&](const auto& tgt) {
    float sx, sy;
    if (ctx()->Render.GridToLargeMap(tgt.X, tgt.Y, 0.f, sx, sy)) {
        ImVec2 mp = ImGui::GetMousePos();
        bool hovered = std::hypot(mp.x - sx, mp.y - sy) < 12.f;
        dl->AddCircleFilled({sx, sy}, 8.f, hovered ? 0xFF00FFFF : 0xFFFFFF00);
        if (clickedThisFrame && hovered) {
            // ユーザーがこのPOIを選択した
        }
    }
    return true;
});
ImGui::End();

ピッカー領域だけをカバーする小さなウィンドウでも同様に機能します — ホストはサイズを気にしません。カーソル下に何らかのウィンドウがあるかどうかだけを確認します。

実用例 — マップからPOIを追加する

class RadarPlugin : public PluginSDK::Plugin {
    bool m_pickerMode = false;

    void DrawUI() override {
        if (!ctx()->Game.IsInGame()) return;
        ImGui::SetCurrentContext((ImGuiContext*)ctx()->ImGuiContext);

        // ホットキートグル。キーボードはキャプチャ状態に関わらずプラグインに届くので、
        // オーバーレイがクリックスルーのときでも機能します。
        if (ImGui::IsKeyPressed(ImGuiKey_F8, /*repeat=*/false)) {
            m_pickerMode = !m_pickerMode;
            ctx()->Overlay.SetIncludeSleepingEntities(m_pickerMode);
            ctx()->Overlay.SetWantsOverlayInput     (m_pickerMode);
        }
        if (m_pickerMode && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
            m_pickerMode = false;
            ctx()->Overlay.SetIncludeSleepingEntities(false);
            ctx()->Overlay.SetWantsOverlayInput     (false);
        }

        if (!m_pickerMode) return;
        // ... ピッカーUI (ImGui::Begin + InvisibleButton パターンについては
        //     上記の Background-draw-list の注意点を参照)。
    }

    void OnDisable() override {
        // 念のため。ホストも無効化時にフラグをクリアしますが、
        // OnDisable と PluginManager の後処理の間に同期フレームが
        // 発火した場合に備え、明示的なクリーンアップで状態を一貫させます。
        ctx()->Overlay.SetIncludeSleepingEntities(false);
        ctx()->Overlay.SetWantsOverlayInput     (false);
    }
};

ライフサイクルと集約

  • 両フラグは冪等です — Set(true) を連続して2回呼んでも2回目はno-opで、カウンタは二重インクリメントされません。
  • 両フラグはホスト自身の状態と他のすべてのプラグインのフラグとORで集約されます。複数のプラグインが同時にピッカーモードになっても問題ありません。
  • ホストはプラグインが無効化されたとき (Pluginsタブ、クラッシュ無効化、またはシャットダウン) にそのプラグインのフラグをすべて自動クリアします。フレーム途中のクラッシュでオーバーレイがキャプチャモードに永続的に固定されることはありません — ただし、行儀の良いプラグインはon/off呼び出しを対称にして、他のプラグインとゲームが途中でも反応できるようにします。
  • レイテンシー: Set呼び出しはフラグを同期的に更新しますが、実際の挙動変化は次のホストフレーム (オーバーレイ入力) または次のGameClientワーカーティック (スリーピングエンティティ) で現れます。サブフレームで、観察できません。
  • すべてのメソッドはどのスレッドからでも安全に呼び出せます

14. Prices — ホストがロードするアイテム価格

ホストはバックグラウンドスレッドで poe2scout からセッションごとに一度マーケット価格をロードし、ctx()->Prices 経由ですべてのプラグインに公開します。プラグイン自身は価格を取得しません — ビルトインレーダー、ホストオーバーレイ、すべてのプラグインの背後には単一の共有価格データベースがあるため、APIはコンシューマーごとではなく一度だけ叩かれます。

  • 価格のリーグはユーザーが Configuration → Settings で選択し (デフォルト Runes of Aldur)、ホスト側で永続化されます。プラグインは常にユーザーが選択したリーグを読み取り、自分では選択しません。
  • ロードはカテゴリーごとのバックオフ付きの一度きりです (失敗後は1 → 5 → 10 → 20 → 30 → 60分後にリトライし、その後はアプリが再起動するまで諦めます)。定期リフレッシュはありません — 価格はセッション全体を通じて安定しています。
  • すべての価格はChaos建てです。それらの単位で表示したい場合は GetRates() でDivine/Exaltedの換算レートを取得できます。

価格の検索

PluginSDK::PriceResult p = ctx()->Prices.LookupPrice("Divine Orb");
if (p.found) {
    ctx()->Log.Info(("Divine Orb = " + std::to_string(p.chaos) + "c").c_str());
    // p.category — どの poe2scout バケットがマッチしたか (currency, fragments, runes,
    // …, またはユニークアイテムカテゴリー)。通貨かユニークかで分岐するのに便利。
}

LookupPrice はアイテムの表示名 (通貨名、ユニーク名、またはベースタイプ) を受け取り、ロード済みのすべてのカテゴリーにわたってホスト側でファジーマッチングを行います。ミスの場合は found == false ですべての価格がゼロで返ります。

PriceResult フィールド 意味
found bool 価格がマッチした
chaos float Chaos Orb建ての価格 (基本単位)
divine float Divine Orb建ての同価格
exalt float Exalted Orb建ての同価格
category std::string マッチした poe2scout カテゴリー (currency / fragments / runes / … / ユニークカテゴリー)

レートとロード状態

PluginSDK::PriceRates  r = ctx()->Prices.GetRates();   // divineInChaos, exaltedInChaos
PluginSDK::PriceStatus s = ctx()->Prices.GetStatus();
if (!s.loaded) {
    // まだ準備できていません (ロード中、またはすべてのカテゴリーが失敗)。
    // s.catsOk / s.catsPending / s.catsFailed でローダーがどこまで進んだかを確認できます。
}

GetStatus().loaded は価格を表示する前に確認すべきゲートです — 換算レートと少なくとも最初のカテゴリーが到着して初めてtrueに変わります。それまでは、ゼロではなく「価格ロード中…」状態をレンダリングしてください。


15. Runeshapeデバイス

ctx()->Runeshape は、ホストが現在のエリアで解決した Expedition2Encounter ("Runeshape") デバイスと、各レシピが付与するリワードを公開します。ホストがデバイスチェーンのウォークとオフラインのレシピマッチングを行い、プラグインはその結果を読み取るだけです。これはビルトインレーダーのリワードタグと NinjaPricer の Runeshape ウィンドウを動かしているものです — サードパーティプラグインも同じデータをレンダリングできます。

for (const PluginSDK::Runeshape& rs : ctx()->Runeshape.Runeshapes()) {
    // rs.color は各デバイスを識別する色 (グループ化/着色に使用)。
    // rs.bestIndex は最高価格のリワードのインデックス (-1の場合あり)。
    for (const PluginSDK::RuneshapeReward& rw : ctx()->Runeshape.Rewards(rs.entityId)) {
        if (rw.priced)
            ctx()->Log.Info((rw.name + " x" + std::to_string(rw.count) +
                             " = " + std::to_string(rw.totalChaos) + "c").c_str());
        if (rw.propagatingCount > 0)   // 0.5.4: 次の残骸に引き継がれるルーン
            ctx()->Log.Info(("  propagates: " + rw.propagatingRunes).c_str());
    }
}
Runeshape フィールド 意味
entityId uint64_t デバイスエンティティID — Rewards() に渡す
color uint32_t パックされたRGBA、デバイスごとに固定 (グループ化/着色に使用)
isUnique bool デバイスがユニークアイテムレシピを提供している
holeCount int アンカーのルーン穴の数
anchorName std::string アンカールーン名
rewardCount int リワードスロット数
bestIndex int 最高 totalChaos リワードのインデックス。なければ -1
propagatingSlots std::vector<int> ルーンが次の残骸に引き継がれるルーン穴スロットインデックス (0.5.4 キャリーオーバー)。通常1、まれに2
RuneshapeReward フィールド 意味
name std::string リワードアイテム名
count int 付与数量
unitChaos float 単体Chaos価格 (Pricesサービスから)
totalChaos float unitChaos × count
priced bool このリワードの価格が見つかった
propagatingRunes std::string このレシピの伝播スロットのルーン — レシピを完了したときに引き継がれるもの。例: "Power" または "Cold, Time"。レシピがそのスロットをカバーしない場合は空
propagatingCount int このリワードの伝播ルーン数
propagatingHasRare bool 伝播ルーンのいずれかがレア ("紫"/高価値)

リワード価格は同じ ctx()->Prices データベースから取得されるため、未定価のリワード (priced == false) は通常、価格がまだロードされていないか、アイテムが poe2scout に掲載されていないことを意味します。

ルーン伝播 (0.5.4)。 各残骸は、ルーンが次の残骸に引き継がれるルーンスロットをランダムに選択します (ゲーム内: Runeshape Recipesリストの金色の王冠ハイライト)。Runeshape::propagatingSlots は生のスロットリストです。スロットの位置であるため、伝播するルーンはレシピごとに異なり、RuneshapeReward::propagatingRunes がリワードごとに解決します。これは NinjaPricer の黄色スロットドットとリワードごとのマーカーを動かしているものです。


16. アトラスパネルデータ

ctx()->Atlasはライブのエンドゲームアトラスパネルを公開します — マップノード、アンカーごとの隣接関係、現在のRite選択、生の適格性ウェイト — これらはGameLibraryのアトラスオフセットを通じてホスト側で読み取られます。これはビルトインのアトラスオーバーレイとリファレンスプラグイン ForetoldRewards を動かしているものです。すべてはGetPanel()が起点です: 0はアトラスUIが構築されていないことを意味します (ゲーム外 / パネルが閉じている)。

if (!ctx()->Atlas.GetPanel()) return;                     // atlas UI absent
for (const PluginSDK::AtlasNode& n : ctx()->Atlas.Nodes()) {
    // n.gridX / n.gridY are stable per map; n.marker / n.mapState carry UI state.
    std::string name = ctx()->Atlas.GetNodeName(n.uiAddress);  // resolve lazily
}
uint32_t seed = ctx()->Atlas.GetLineSeed();               // 0 = no Rite line
メソッド 戻り値 用途
GetPanel() uintptr_t アトラスパネルのアドレス; 0 = パネルなし
Nodes(detail) std::vector<AtlasNode> すべてのアトラスノード (gridX/YuiAddressmarkerflagsbiomemapState); デフォルトのdetailは名前をスキップ — GetNodeName()で解決
Connections() std::vector<AtlasConnection> アンカーごとの隣接関係 (x/y + 最大5つのneighbors); ビューポート依存
Selection(which) std::vector<AtlasGridPoint> which=0は公開済みRiteラインのマップ、which=1は選択済みアンカー (選択順)
GetLineSeed() uint32_t Riteリワード選択シード; 0 = Riteラインなし / パネルなし
Weights() std::vector<AtlasWeight> 生の適格性ウェイト行 (keyvalue)
GetNodeName(uiAddress) std::string ノードのuiAddressに対する表示名 (未解決なら"")

by-valueのAtlasServiceAbiは凍結されています (その後ろに別のHostAbiメンバーが追加されたため)。今後のアトラス読み取りは必ず新しいHostAbi tail関数として追加する必要があります — 新しいAtlasServiceAbiメンバーとしては決して追加しないでください。


17. Sekhemaトライアルデータ

ctx()->SekhemaTrial of the Sekhemas のフロアマップデータを公開します — 静的な部屋グラフ、ラン中の選択、各部屋のコンテンツFK行 — さらにトライアルの部屋に必要なStateMachineフラグ読み取りも提供します。グラフ系の呼び出しはトライアルパネルのUIアドレスを明示的に受け取ります: GetPanel() (ホストによる直接の子インデックス解決) から始めるか、低コストのProbeFloor()で事前フィルタリングした独自のUIツリーBFSを実行してください。これはリファレンスプラグイン SekhemaHelper を動かしているものです。

uintptr_t panel = ctx()->Sekhema.GetPanel();
if (!panel) return;                                       // not on a trial floor
PluginSDK::SekhemaFloor floor = ctx()->Sekhema.GetFloor(panel);
for (const PluginSDK::SekhemaRoom& r : ctx()->Sekhema.Rooms(panel)) {
    bool chosen = r.layer < (int)floor.choices.size() && floor.choices[r.layer] == r.index;
    // r.connections lists the room indices in the NEXT layer
}
メソッド 戻り値 用途
GetPanel() uintptr_t ホストが解決したトライアルパネル; 0 = なし / コントローラーモード / ゲーム外
ProbeFloor(uiAddress) int uiAddressにあるFloorDataのレイヤー数。0 = トライアルフロアではない (低コストなBFS事前フィルター)
GetFloor(panel) SekhemaFloor フロアヘッダー: layerCountroomCountschoices (レイヤーごとの選択インデックス、0xFF=なし)、counter
Rooms(panel) std::vector<SekhemaRoom> (layer, index)順の静的な部屋グラフ; 各部屋は次レイヤーへのconnectionsを持つ
Content(panel) std::vector<SekhemaContentEntry> 部屋ごとのコンテンツFK行をrowId / rowNameに解決したもの (tablePathでディスパッチ)
GetRoomUsedFlag(sm) int StateMachineの使用済み/クローズフラグ: 1=使用済み, 0=アクティブ, -1=読み取り不能
GetStateMachineValue(sm, i, out) bool shared-state値を1つ (定義順でdefine_shared_stateエントリごとに8バイト)
GetUiStringId(uiAddress) std::string UI要素のStringIdをUTF-8で — トライアルHUDのリーフが値をレンダリングする数値フィールド (Ui.GetTextではない)

18. 設定の永続化

慣例: <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秒) および無効化時に呼び出されるので、自分で呼ぶ必要はありません。


19. ロギング

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


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

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

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


21. ブリッジ / SEH安全性

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

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

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


22. よくある落とし穴

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

  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構造体を持っている場合は、代わりにそのフィールドに直接アクセスしてください。

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

すべてのサービスのすべての公開メソッドの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
GetGold() int キャラクターのゴールドカウンター (ゲーム外では0)
GetAreaId() std::string 現在のゾーンの生のWorldArea id (Sekhemasのフロア番号を含む)
GetHiveblood(out) bool Genesisツリー (Hiveblood) リソース → out; 旧ホスト / ゲーム外ではfalse

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
EnumerateMonsterMods(ompAddr) std::vector<MonsterMod> ObjectMagicPropertiesコンポーネント (Components.OMP) からモンスターModを取得 — Id / Name / Metadata + Hash16 / Hash32; バフが適用される前のスポーン時に検出可能
ReadGroundEffect(entityAddr) GroundEffect VisibleServerGroundEffect エンティティから地面エフェクトの種類と半径を取得 — ENTITYアドレスを渡す; TypeId でマッチ (ShockedGround/IgnitedGround/…)。単一のエンティティパスを共有するエフェクトを識別する
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) 手動解放 (デストラクタが自動解放)

OverlayService

メソッド 戻り値 用途
SetIncludeSleepingEntities(enable) EntitiesService.EnumerateEntityState::Uselessのエンティティも受け取るようにオプトイン
SetWantsOverlayInput(enable) クリックスルーの代わりに、オーバーレイがマウスクリックをキャプチャするよう要求

両メソッドは冪等であり、プラグインごとに管理され、ホストおよび他プラグインとOR集約され、Disable/Unload時に自動クリアされます。セクション13でフルパターン(マップピッカープラグイン向けのバックグラウンドDrawListに関する注意事項を含む)を参照してください。

FlasksService

メソッド 戻り値 用途
GetFlask(slot) std::optional<Flask> ベルトスロットのライフ/マナフラスコ (0=ライフ, 1=マナ)。範囲外またはゲーム外ではnullopt
GetCharm(slot) std::optional<Charm> ベルトスロットのチャーム (0..2)。範囲外またはゲーム外ではnullopt
AllFlasks() std::vector<Flask> 空スロットを含む全フラスコスロット (FlaskSlotCount()件)
AllCharms() std::vector<Charm> 空スロットを含む全チャームスロット (CharmSlotCount()件)
FlaskSlotCount() int32_t フラスコスロット数 (POE2では2)
CharmSlotCount() int32_t チャームスロット数 (POE2では3)

セクション8(「フラスコとチャーム」)でFlask / Charmのフィールド表とPerUseEffectiveの制限を参照してください。

PricesService

メソッド 戻り値 用途
LookupPrice(name) PriceResult 表示名によるファジーホスト側価格検索 (foundchaosdivineexaltcategory)
GetRates() PriceRates Divine / Exalted → Chaos変換レート
GetStatus() PriceStatus loadedゲート + カテゴリごとのカウント (catsOk / catsPending / catsFailed)

RuneshapeService

メソッド 戻り値 用途
Runeshapes() std::vector<Runeshape> 解決済みの全Expedition2Encounterデバイス (id, color, anchor, bestIndex)
Rewards(entityId) std::vector<RuneshapeReward> デバイスごとの報酬スロット。各スロットはPricesサービスで価格付け

AtlasService

メソッド 戻り値 用途
GetPanel() uintptr_t アトラスパネルのアドレス; 0 = なし
Nodes(detail) std::vector<AtlasNode> すべてのアトラスノード (グリッド座標、markermapState)
Connections() std::vector<AtlasConnection> アンカーごとの隣接関係 (ビューポート依存)
Selection(which) std::vector<AtlasGridPoint> 0 = 公開済みRiteラインのマップ、1 = 選択済みアンカー
GetLineSeed() uint32_t Riteリワード選択シード (0 = なし)
Weights() std::vector<AtlasWeight> 生の適格性ウェイト行 (keyvalue)
GetNodeName(uiAddress) std::string ノードの表示名 (未解決なら"")

SekhemaService

メソッド 戻り値 用途
GetPanel() uintptr_t ホストが解決したトライアルパネル; 0 = なし
ProbeFloor(uiAddress) int FloorDataのレイヤー数 (0 = トライアルフロアではない); 低コストな事前フィルター
GetFloor(panel) SekhemaFloor フロアヘッダー (layerCountroomCountschoicescounter)
Rooms(panel) std::vector<SekhemaRoom> (layer, index)順の静的な部屋グラフ
Content(panel) std::vector<SekhemaContentEntry> 部屋ごとのコンテンツFK行 (rowId / rowName)
GetRoomUsedFlag(sm) int 1=使用済み, 0=アクティブ, -1=読み取り不能
GetStateMachineValue(sm, i, out) bool shared-state値を1つ (定義順)
GetUiStringId(uiAddress) std::string UI要素のStringIdをUTF-8で (数値のHUDフィールド)

24. バージョニング

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;
    }
    // ...
}

25. サンプルプラグイン

リポジトリの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