-
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 (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/。
ロードされてホストログにメッセージを出力する最小限のプラグインです。
#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*を返し、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 |
ツリーウォーク、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}
|
OverlayService |
SetIncludeSleepingEntities / SetWantsOverlayInput — マップピッカー対応 |
FlasksService |
ライフ/マナフラスコ + チャーム — チャージ数、Usable、Active、使用あたり、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()をキャッシュしないでください。
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.エンティティは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 (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を使ってください。
ゲーム内スタイルの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コンテナのアドレスの両方を受け付けます。
ゲームの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値はゲーム側で安定した識別子です。存在する場合はハードコードされたパスよりも優先してください。
0.5.xに関する注意。
Ui.GetStringId()は現行 (0.5.x) クライアントで正しい識別子を返します — 要素のStringIdフィールドのオフセットが移動し (0x448→0x4C0)、ホストブリッジもそれに合わせて修正されました。(トライアルHUDのフィールドリーフが値をレンダリングする数値のStringId —GetText()とは別のフィールド — については、SekhemaHelperはctx()->Sekhema.GetUiStringId()を読み取ります。)
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を参照してください。
ctx()->Overlay は、オーバーレイ全体の挙動を変更するプラグインごとの「リクエストフラグ」を2つ公開します。ホストはプラグインごとの状態をthisポインタをキーとして保持し、ホスト自身のビルトインフラグ (メインメニューの可視性、AutoCraftロック、ホストビルトインのマップからエンティティ追加ピッカー、他のすべてのプラグインのリクエスト) とORで集約します。プラグインの無効化/アンロード時に自動クリアされます — クラッシュしたプラグインやバグのあるプラグインが、オーバーレイを異常な状態に永続的に固定してしまうことはありません。
デフォルトでは、EntitiesService.Enumerate と Snapshot.Entities は EntityState::Useless のエンティティを非表示にします (遠くにいる休眠中のモンスター/NPC/宝箱を除外するホストの「スリーピング」フィルター)。これによりフレームごとのスナップショットコストが制限されます — 典型的なエリアには、プラグインが気にしない数百のUselessエンティティが存在します。
エリアの完全なエンティティプールが必要なマップピッカーUIやデバッグビューア (まだアクティブでないエンティティをユーザーがクリックできるよう) では、フィルターをオフにしてください:
ctx()->Overlay.SetIncludeSleepingEntities(true);
// これで ctx()->Entities.Enumerate は Useless エンティティも見えるようになります。
// Entity::IsSleeping はホストの別の SleepingEntities コレクション由来の
// エンティティを特定的にマークします (EntityState::Useless とは直交)。コスト: 有効中はフレームあたり約+5〜15%のスナップショットCPU。必要でなければOFFのままにしてください。
オーバーレイウィンドウは通常クリックスルー (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)はどちらの場合でも機能します。
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();ピッカー領域だけをカバーする小さなウィンドウでも同様に機能します — ホストはサイズを気にしません。カーソル下に何らかのウィンドウがあるかどうかだけを確認します。
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ワーカーティック (スリーピングエンティティ) で現れます。サブフレームで、観察できません。
- すべてのメソッドはどのスレッドからでも安全に呼び出せます。
ホストはバックグラウンドスレッドで 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に変わります。それまでは、ゼロではなく「価格ロード中…」状態をレンダリングしてください。
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 の黄色スロットドットとリワードごとのマーカーを動かしているものです。
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/Y、uiAddress、marker、flags、biome、mapState); デフォルトの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> |
生の適格性ウェイト行 (key、value) |
GetNodeName(uiAddress) |
std::string |
ノードのuiAddressに対する表示名 (未解決なら"") |
by-valueのAtlasServiceAbiは凍結されています (その後ろに別のHostAbiメンバーが追加されたため)。今後のアトラス読み取りは必ず新しいHostAbi tail関数として追加する必要があります — 新しいAtlasServiceAbiメンバーとしては決して追加しないでください。
ctx()->Sekhemaは Trial 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 |
フロアヘッダー: layerCount、roomCounts、choices (レイヤーごとの選択インデックス、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ではない) |
慣例: <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 |
GetGold() |
int |
キャラクターのゴールドカウンター (ゲーム外では0) |
GetAreaId() |
std::string |
現在のゾーンの生のWorldArea id (Sekhemasのフロア番号を含む) |
GetHiveblood(out) |
bool |
Genesisツリー (Hiveblood) リソース → out; 旧ホスト / ゲーム外ではfalse
|
| メソッド | 戻り値 | 用途 |
|---|---|---|
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 |
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 |
ワールド座標の便利アクセサ |
| メソッド | 戻り値 | 用途 |
|---|---|---|
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コンテナを自動解決 |
| メソッド | 戻り値 | 用途 |
|---|---|---|
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) |
— | 手動解放 (デストラクタが自動解放) |
| メソッド | 戻り値 | 用途 |
|---|---|---|
SetIncludeSleepingEntities(enable) |
— |
EntitiesService.EnumerateでEntityState::Uselessのエンティティも受け取るようにオプトイン |
SetWantsOverlayInput(enable) |
— | クリックスルーの代わりに、オーバーレイがマウスクリックをキャプチャするよう要求 |
両メソッドは冪等であり、プラグインごとに管理され、ホストおよび他プラグインとOR集約され、Disable/Unload時に自動クリアされます。セクション13でフルパターン(マップピッカープラグイン向けのバックグラウンドDrawListに関する注意事項を含む)を参照してください。
| メソッド | 戻り値 | 用途 |
|---|---|---|
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の制限を参照してください。
| メソッド | 戻り値 | 用途 |
|---|---|---|
LookupPrice(name) |
PriceResult |
表示名によるファジーホスト側価格検索 (found、chaos、divine、exalt、category) |
GetRates() |
PriceRates |
Divine / Exalted → Chaos変換レート |
GetStatus() |
PriceStatus |
loadedゲート + カテゴリごとのカウント (catsOk / catsPending / catsFailed) |
| メソッド | 戻り値 | 用途 |
|---|---|---|
Runeshapes() |
std::vector<Runeshape> |
解決済みの全Expedition2Encounterデバイス (id, color, anchor, bestIndex) |
Rewards(entityId) |
std::vector<RuneshapeReward> |
デバイスごとの報酬スロット。各スロットはPricesサービスで価格付け |
| メソッド | 戻り値 | 用途 |
|---|---|---|
GetPanel() |
uintptr_t |
アトラスパネルのアドレス; 0 = なし |
Nodes(detail) |
std::vector<AtlasNode> |
すべてのアトラスノード (グリッド座標、marker、mapState) |
Connections() |
std::vector<AtlasConnection> |
アンカーごとの隣接関係 (ビューポート依存) |
Selection(which) |
std::vector<AtlasGridPoint> |
0 = 公開済みRiteラインのマップ、1 = 選択済みアンカー |
GetLineSeed() |
uint32_t |
Riteリワード選択シード (0 = なし) |
Weights() |
std::vector<AtlasWeight> |
生の適格性ウェイト行 (key、value) |
GetNodeName(uiAddress) |
std::string |
ノードの表示名 (未解決なら"") |
| メソッド | 戻り値 | 用途 |
|---|---|---|
GetPanel() |
uintptr_t |
ホストが解決したトライアルパネル; 0 = なし |
ProbeFloor(uiAddress) |
int |
FloorDataのレイヤー数 (0 = トライアルフロアではない); 低コストな事前フィルター |
GetFloor(panel) |
SekhemaFloor |
フロアヘッダー (layerCount、roomCounts、choices、counter) |
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フィールド) |
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) + インベントリスキャン + アイテムごとの価格設定。実際のワークフローでのネットワークコード、サードパーティのデータ取り込み、インベントリ反復を示します。