-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide FR
Les plugins POEFixer sont des DLL C++ natives chargées à l'exécution depuis Plugins/<PluginName>/<PluginName>.dll. Ils lisent l'état du jeu en direct, dessinent des superpositions ImGui, persistent leurs propres paramètres et s'abonnent aux événements de l'hôte.
Le SDK de plugin possède une architecture à trois couches :
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
- Les auteurs de plugins incluent exactement un en-tête :
POEFixer/plugin_sdk/PluginSDK.h. - Cet en-tête déclare tout dans l'espace de noms
PluginSDK::et tire l'ABI C depuisPluginAbi.hen dessous. Vous pouvez mentionner que ce dernier existe ; vous n'y jetez presque jamais un œil. - Tous les conteneurs
std::*vivent à l'intérieur de la DLL du plugin. Seuls les POD traversent la frontière de l'hôte. Cela signifie qu'un plugin construit avec une version de toolchain ne peut pas s'emmêler avec la STL de l'hôte — les seuls types partagés sont les entiers, flottants, pointeurs et petites structures.
Localisez les en-têtes du SDK ici :
-
POEFixer/plugin_sdk/PluginSDK.h— le wrapper C++ que les auteurs de plugins utilisent. -
POEFixer/plugin_sdk/PluginAbi.h— l'ABI C pure en dessous.
Plugins de référence livrés avec le dépôt (à lire comme documentation) : Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.
Plugin minimal qui se charge et affiche un message dans le journal de l'hôte :
#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; }Compilez sous forme de Plugins/Hello/Hello.dll, redémarrez l'hôte, activez depuis l'onglet Plugins.
Utilisez Plugins/ExamplePlugin/ExamplePlugin.vcxproj comme modèle canonique. Les paramètres essentiels :
- Type de configuration : DynamicLibrary
- Jeu d'outils de plateforme : v143 (Visual Studio 2022)
- Jeu de caractères : Unicode
-
Standard du langage :
stdcpp20 -
Bibliothèque d'exécution :
MultiThreadedDLL(Release) /MultiThreadedDebugDLL(Debug). DOIT correspondre à l'hôte. -
Définitions du préprocesseur :
PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS -
Répertoires d'inclusion additionnels :
$(SolutionDir)POEFixer -
Répertoire de sortie :
$(SolutionDir)x64\Release\Plugins\<YourPlugin>\ -
Nom de la cible : doit correspondre au nom du dossier (
Plugins/MyPlugin/→MyPlugin.dll)
L'hôte analyse chaque sous-dossier dans Plugins/ et recherche <FolderName>.dll. La DLL doit exporter trois symboles :
-
CreatePlugin— factory ; retournePluginSDK::Plugin*. -
DestroyPlugin— destructeur ; prendPluginSDK::Plugin*. -
PluginSDK_AttachHost— connecte leContext. Défini pour vous à l'intérieur dePluginSDK.het émis automatiquement lorsquePLUGIN_EXPORTSest défini.
Disposition source recommandée :
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() renvoie un chemin UTF-8 absolu enraciné dans le répertoire de l'EXE de l'hôte — n'ajoutez pas vous-même le chemin de l'EXE. La chaîne du répertoire est possédée par valeur à l'intérieur de PluginSDK::Plugin, donc elle survit aux réallocations de conteneurs de l'hôte et aux cycles de rechargement sans préoccupation de durée de vie.
Si vous souhaitez dessiner avec ImGui, ajoutez également ceci à <ClCompile> (l'hôte les lie également, mais l'ImGui côté plugin est par DLL) :
..\..\POEFixer\imgui\imgui.cpp
..\..\POEFixer\imgui\imgui_draw.cpp
..\..\POEFixer\imgui\imgui_tables.cpp
..\..\POEFixer\imgui\imgui_widgets.cpp
Dans OnEnable, attachez-vous au contexte ImGui de l'hôte :
if (ctx()->ImGuiContext)
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx()->ImGuiContext));PluginSDK::Plugin est une classe de base virtuelle. Redéfinissez-les dans votre plugin (dans l'ordre d'appel approximatif) :
| Méthode | Appelée quand | Utilisation typique |
|---|---|---|
const char* GetName() const |
Une fois, juste après la construction | Renvoyer le nom d'affichage de votre plugin |
void OnEnable(bool isGameAttached) |
Lorsque l'utilisateur active le plugin (ou au démarrage si persisté) | Charger les paramètres, s'abonner aux événements, attacher le contexte ImGui |
void DrawSettings() |
Chaque frame tant que le panneau de paramètres du plugin est ouvert | Contrôles ImGui pour votre configuration |
void DrawUI() |
Chaque frame tant que le plugin est activé | Dessin de la superposition ImGui (utilisez ImGui::GetBackgroundDrawList() pour la superposition du jeu) |
bool WantsOverlay() const |
Interrogée chaque frame | Renvoyer true si vous souhaitez que l'hôte soit en mode superposition (transparent au clic) |
void SaveSettings() |
Périodiquement (~5s) et à la désactivation | Persister la config sur disque |
void OnDisable() |
Lorsque l'utilisateur désactive, ou à l'arrêt de l'hôte | Libérer les ressources, se désabonner des événements |
Seul GetName est obligatoire ; le reste a des valeurs par défaut sûres.
L'hôte appelle également GetSDKVersion() (défini sur la base, NE PAS redéfinir) immédiatement après CreatePlugin pour vérifier que le plugin et l'hôte s'accordent. Inadéquation → plugin refusé.
ctx() renvoie const PluginSDK::Context*, un agrégat de 16 services :
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
};En un coup d'œil — à quoi sert chaque service :
| Service | Pourquoi vous y avez recours |
|---|---|
GameService |
Snapshot, indicateurs d'état, infos écran + fenêtre |
EntitiesService |
Énumérer, rechercher par id, surveiller le cycle de vie |
ComponentsService |
21 lecteurs + 4 énumérateurs + ~10 helpers de commodité |
InventoryService |
Scan, énumération, lectures de mods par item |
UiService |
Parcours d'arbre, FindPanelByStringId, ComputeScreenRect
|
RenderService |
WorldToScreen, GridTo{Large,Mini}Map, transformations |
TerrainService |
Grilles walkable + hauteur (RAII), emplacements TGT |
MemoryService |
Primitives RPM — uniquement quand aucun appel de plus haut niveau ne convient |
LogService |
Debug / Info / Warn / Error
|
EventsService |
Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
|
OverlayService |
SetIncludeSleepingEntities / SetWantsOverlayInput — compatible avec les plugins carte |
FlasksService |
Fioles de vie/mana + charmes — charges, Usable, Active, par utilisation, nombre de mods |
PricesService |
LookupPrice / GetRates / GetStatus — prix poe2scout chargés par l'hôte, partagés par tous les plugins |
RuneshapeService |
Runeshapes / Rewards — dispositifs Expedition2Encounter résolus + récompenses par dispositif |
AtlasService |
GetPanel / Nodes / Connections / Selection / GetLineSeed / Weights — données live du panneau d'atlas endgame |
SekhemaService |
GetPanel / GetFloor / Rooms / Content / lectures de flags de salle — données de carte d'étage du Trial of the Sekhemas |
ctx() est valide depuis le moment où l'hôte appelle OnEnable jusqu'au retour de OnDisable. Ne mettez pas en cache ctx() à travers les rechargements à chaud ou les frontières de déchargement de DLL.
ctx()->Game.GetSnapshot() renvoie un Snapshot typé par valeur — une vue immuable complète de la frame actuelle. GetSnapshot() parcourt abi->entities.enumerate et peuple snap.Entities avant de retourner, donc le coût se met à l'échelle avec le nombre d'entités proches. Appelez-le une fois par frame et réutilisez.
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 transitionCe que le snapshot transporte directement (aucun autre appel de service nécessaire) :
- État + indicateurs :
State,IsAttached,IsWindowValid,GameWindowForeground,IsTown,IsHideout,IsPaused,IsSkillTreeVisible. - Zone :
CurrentAreaName,CurrentAreaHash,CurrentAreaLevel,AreaChangeCounter. - Monde :
Player(Entitycomplet),Entities(std::vector<Entity>complet),Vitals,LargeMap,MiniMap,WorldToScreenMatrix[16]. - Fenêtre :
ScreenWidth,ScreenHeight,ProcessId,GameWindow,LastUpdateTime,WorldToGridConvertor.
Ce qui n'est pas sur le snapshot — récupérez via les services : contenu de l'inventaire (InventoryService), buffs (ComponentsService::EnumerateBuffs), listes de mods par item (InventoryService::ReadItemMods), panneaux UI (UiService).
Helpers économiques lorsque vous n'avez pas besoin d'un snapshot complet :
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 lit le compteur de ressource de l'arbre Genesis (Hiveblood) — une lecture host-tail routée via GameService (même famille que 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.Les entités exposent leurs composants via entity.Components — une structure ComponentAddresses d'adresses uintptr_t. Passez chaque adresse au ComponentsService::Read* correspondant pour obtenir un snapshot typé par valeur.
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");
}
}Il existe 21 lecteurs de composants : ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime.
ComponentAddresses lui-même contient 24 slots : les 21 ci-dessus plus trois marqueurs (Buffs, WorldItem, AreaTransition) et OMP (interne à l'hôte). Buffs est un marqueur de présence — la liste réelle des buffs vient de EnumerateBuffs. WorldItem / AreaTransition sont des marqueurs de type d'entité plutôt que de vrais composants. Tous les slots ont des prédicats HasX() correspondants sur ComponentAddresses.
Lecteurs de style collection pour les composants avec données de taille variable :
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>Helpers de commodité (à un coup — ils appellent en interne Read* pour vous) :
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)) { ... }Un indicateur Valid sur chaque structure renvoyée vous permet de gérer « adresse de composant à 0 / lecture échouée » sans exceptions. Si vous avez déjà la structure parent (Life, Mods, …), accédez directement à ses champs plutôt que d'appeler à nouveau le helper — le helper relit le composant à chaque fois.
Distinguer les effets au sol — de nombreux effets au sol partagent le chemin d'entité unique Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect, de sorte que le chemin seul ne permet pas de distinguer un Sol Choqué d'un Sol en Feu. ReadGroundEffect résout le composant GroundEffect de l'entité et sa ligne groundeffects.datc64. Passez l'adresse de l'entité (le composant GroundEffect n'est pas dans Components, l'hôte le résout donc pour vous — même convention que ReadPathfinding), puis faites correspondre sur TypeId, la clé stable et indépendante des mises à jour :
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 -> unités monde ; dessinez un cercle à (e.WorldX, e.WorldY, e.WorldZ) de ce rayon
if (ge.TypeId == "ShockedGround") {
// surligner selon votre config (couleur/alpha indexés par ge.TypeId)
}
}La structure GroundEffect :
| Champ | Signification |
|---|---|
Valid |
false si l'entité n'a pas de composant GroundEffect ou si la lecture a échoué |
TypeId |
Id groundeffecttypes — la clé stable sur laquelle faire correspondre (ex. ShockedGround) |
Radius |
Rayon de l'effet en unités monde ; 0 quand la variante ne le définit pas |
EndEffect |
Comportement de fin : fadeout / close / end
|
BuffVisual1 |
Id buffvisuals (ex. ground_fire_burn_white) ; vide si non défini |
BuffVisual2 |
Nom buffdefinitions (ex. ground_tar_gold) ; vide si non défini |
AoFile |
Premier chemin visuel .ao/.aoc ; vide si aucun |
GroundEffectsRowAddr / GroundEffectTypesRowAddr
|
Pointeurs bruts vers les lignes dat (stables par session) pour des références croisées avancées |
La position mondiale de l'effet provient de l'entité elle-même (Entity.WorldX/Y/Z, ou les composants Render/Positioned), elle n'est donc pas dupliquée dans la structure. ReadGroundEffect relit à chaque appel, mettez donc en cache le résultat selon votre intervalle de scan. Il renvoie un GroundEffect invalide sur les hôtes construits avant cette API (il se trouve dans la queue append-only du SDK v6 et est vérifié null).
EnumerateActiveSkills(actorAddr) renvoie une ActiveSkill par compétence accordée ou sertie sur l'Actor de l'entité. Au-delà de Name, la structure porte l'état de recharge et un descripteur de gemme/socket décodé :
| Champ | Signification |
|---|---|
Name |
Nom interne de la compétence |
CurrentSize / TotalUses / UseStage
|
Compteurs de stage / d'utilisation (bruts) |
CastType |
Id brut de type de lancement |
TotalCooldownMs |
Durée totale du temps de recharge, en millisecondes |
CanBeUsed |
Indicateur hôte « utilisable maintenant » |
MaxUses |
Charges de recharge que possède la compétence (0 = non liée à un temps de recharge) |
TotalActiveCooldowns |
Charges actuellement en recharge. Utilisations restantes = MaxUses - TotalActiveCooldowns quand MaxUses > 0. |
GrantedEffectsPerLevelAddr, ActiveSkillsDatAddr, GrantedEffectStatSetsPerLevelAddr, SkillDetailsAddr
|
Adresses brutes de lignes DAT — passez-les à ctx()->Memory.Read* pour des données de compétence plus poussées. |
EquipmentInfoPacked |
Mot brut empaqueté gemme/socket (décodé dans Equipment, ci-dessous). |
Le mot empaqueté est décodé pour vous dans skill.Equipment :
Champ Equipment
|
Signification |
|---|---|
GemNameHash |
16 bits de poids fort — hash d'identité de la gemme |
InventorySlot |
Emplacement d'équipement (indexé à partir de 1) où se trouve la gemme |
LinkIndex |
Index du groupe de liens dans l'item |
SocketIndex |
Index du socket dans le groupe de liens |
UnknownFlag / CanBeOnPlayerItem
|
Indicateurs résiduels (disposition des bits dans PluginSDK.h) |
auto skills = ctx()->Components.EnumerateActiveSkills(e.Components.Actor);
for (const auto& s : skills) {
if (s.MaxUses > 0) {
int remaining = s.MaxUses - s.TotalActiveCooldowns;
ctx()->Log.Info((s.Name + ": " + std::to_string(remaining) +
"/" + std::to_string(s.MaxUses) + " charges").c_str());
}
// s.Equipment.LinkIndex / s.Equipment.SocketIndex — où se trouve la gemme
}EnumerateSkillStats(skillDetailsAddr) expose les propres conteneurs de stats évaluées du jeu pour une compétence — y compris la famille DPS affichée par le panneau de compétences en jeu. Passez ActiveSkill::SkillDetailsAddr depuis un résultat EnumerateActiveSkills de la même frame (les adresses de compétence deviennent obsolètes d'une frame à l'autre / lors des changements de zone ; une adresse obsolète renvoie sans risque un vecteur vide, tout comme un hôte construit avant cette API).
Chaque SkillStatEntry renvoyé est {SetIndex, StatId, Value} :
-
SetIndex 0est le set de stats du contexte courant de la compétence — présent sur chaque compétence, persistant (survit à la fermeture des panneaux), et la source exacte de la ligne DPS du panneau de compétences. - Les sets suivants sont les sets de stats par partie de la compétence — pour les compétences d'invocation/de commandement, les stats côté sbire s'y trouvent.
-
StatIdest l'index de ligne Stats.dat + 1 (la clé de stat runtime du moteur ;0est la sentinelle « pas de stat » du jeu). Résolvez les noms en dumpantStats.dat. -
Valueest unint32brut ; de nombreuses stats de la famille DPS sont en virgule fixe ×100.
Ids runtime utiles :
| StatId | Stat (ligne Stats.dat + 1) | Échelle |
|---|---|---|
| 691 | hundred_times_attacks_per_second |
×100 |
| 692 | hundred_times_damage_per_second |
×100 |
| 695 | hundred_times_casts_per_second |
×100 |
| 1982 / 1983 |
hundred_times_average_damage_per_hit / ..._per_skill_use
|
×100 |
| 694 | base_spell_cast_time_ms |
ms |
| 2079 | skill_show_average_damage_instead_of_dps |
indicateur |
auto skills = ctx()->Components.EnumerateActiveSkills(snap.Player.Components.Actor);
for (const auto& s : skills) {
for (const auto& st : ctx()->Components.EnumerateSkillStats(s.SkillDetailsAddr)) {
if (st.StatId == 692) { // hundred_times_damage_per_second
ctx()->Log.Info((s.Name + " DPS: " +
std::to_string(st.Value / 100.0)).c_str());
}
}
}Mises en garde à connaître :
-
La famille DPS est virtuelle. Le moteur calcule ces stats via des callbacks (
DPS = rate/100 × avg damage) et ne stocke le résultat que pour les contextes qu'il a affichés. Le set du contexte courant porte la dernière valeur évaluée par le jeu lui-même ; les contextes alternatifs (onglets d'infobulle d'infusion, aperçus d'échange d'arme) sont évalués de manière transitoire au survol et ne sont pas lisibles de façon persistante. Les valeurs peuvent donc accuser un retard de quelques pourcents par rapport à l'infobulle en direct sur les monstres avec des buffs de dégâts dynamiques — la liste de compétences et l'infobulle du jeu lui-même divergent de la même façon. -
Le DPS du sbire vit sur le sbire. Les propres sets d'une compétence d'invocation ne décrivent que l'invocation elle-même ; les chiffres « Attaque de base » de l'infobulle proviennent de l'
Actorde l'entité du sbire — énumérez les entités, trouvez le monstre amical, puisEnumerateActiveSkills(minion.Components.Actor)→EnumerateSkillStats(...)sur sa compétence d'attaque. -
EnumerateActiveSkillsrenvoie deux entrées par nom de compétence (contextes d'évaluation différents, ex. jeux d'armes) — interrogez les deux si vous recherchez une stat spécifique.
Chaque Entity (y compris snap.Player et les membres de snap.Entities) porte le même ensemble de champs :
| Groupe | Champs |
|---|---|
| Identité |
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
|
| Vitaux rapides |
CurrentHP, MaxHP, CurrentES, MaxES (évite un ReadLife si vous n'avez besoin que des totaux) |
| Chaînes |
Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, chemin d'asset) |
| État |
IsSleeping, IsChestOpened
|
| Composants |
Components (sous-structure ComponentAddresses) |
Si vous avez besoin de suivre une entité à travers les frames (par ex. un coffre que le joueur ouvre) et ne souhaitez pas analyser la liste complète des entités à chaque frame, enregistrez une surveillance :
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) renvoie std::optional<Entity> pour les recherches ponctuelles, et GetPlayer() renvoie toujours le joueur local.
Les items jetés au sol apparaissent dans snap.Entities comme entités EntityType::Item au chemin Metadata/MiscellaneousObjects/WorldItem. Ce sont des entités conteneurs — elles ne portent pas directement Mods / Base / Stack / Sockets. La véritable entité d'item se trouve à une indirection.
Pour obtenir l'entité interne de l'item sous forme de snapshot Entity régulier, utilisez 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 ne réussit que sur les véritables conteneurs WorldItem — l'appel avec une adresse d'item d'inventaire renvoie std::nullopt. Si vous souhaitez la même forme de données que pour les items d'inventaire (sans parcourir manuellement les composants), la famille Inventory.ReadItem* de la section suivante résout les conteneurs WorldItem de manière transparente.
ctx()->Inventory.Scan(inventoryId) déclenche un nouveau scan côté hôte. Utilisez -1 pour scanner tous les inventaires.
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
}
}Chaque Inventory expose également une structure Grid décrivant où l'inventaire est dessiné à l'écran :
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)
}Pour récupérer un seul inventaire par id (renvoie la même structure avec Items déjà peuplée) :
PluginSDK::Inventory backpack = ctx()->Inventory.Get(/*inventoryId=*/0);Ou si vous voulez uniquement le vecteur d'items sans la structure enveloppante :
std::vector<PluginSDK::InventoryItem> items = ctx()->Inventory.GetItems(0);ComponentsService::ReadMods(addr) ne renvoie que les indicateurs de résumé (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). Il ne porte pas les listes de mods par type.
Pour l'image complète (résumé + listes de mods), utilisez 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) { ... }Autres lectures directes par entité (moins coûteuses que de rescanner lorsque vous détenez déjà l'adresse d'un item) :
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);Items au sol via l'API d'inventaire. Les sept lectures Inventory.ReadItem* ci-dessus (et ReadItemMods) acceptent À LA FOIS les adresses d'items d'inventaire ET les adresses de conteneurs WorldItem. Les adresses de conteneurs sont automatiquement résolues vers l'item interne avant la lecture, de sorte que le même chemin de code de plugin fonctionne pour les items dans les sacs et les items au sol :
// `addr` peut être soit une adresse d'item d'inventaire, soit un conteneur WorldItem.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int rarity = ctx()->Inventory.ReadItemRarity(addr);
std::string baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);Si vous avez besoin directement des adresses de composants de l'item interne (par exemple, pour appeler ctx()->Components.ReadStack(...) ou parcourir les sockets), utilisez plutôt Entities.GetWorldItemInner de la section 7.
Texte de mod en style in-game + statistiques de base / agrégées (v6, 2026-06-24). Formatez n'importe quelle clé de statistique en le même texte que l'infobulle in-game affiche, et lisez les valeurs défensives de base d'un item ainsi que les propriétés agrégées de map/waystone :
// Afficher un mod comme le jeu le fait ("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()) { /* afficher `text` */ }
}
// Valeurs défensives de base de l'item. EnergyShield est la valeur en jeu (calculée) ;
// Ward/Armour/Evasion sont les valeurs de base de l'item. Valid == false quand l'item
// n'a pas de composant Armour (monnaie, gemmes, bijoux, waystones, ...).
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }
// Statistiques agrégées indexées par id de stat — par ex. Item Rarity d'un waystone (8205),
// Pack Size (8206), Monster Rarity (8207), Monster Effectiveness (8208),
// Waystone Drop Chance (8209).
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
// associez statId -> libellé vous-même ; les valeurs sont des pourcentages signés
}FormatStat utilise l'ensemble de descriptions de stats .csd de l'hôte (téléchargé à la première utilisation), et renvoie donc une chaîne vide tant que ces données ne sont pas prêtes — retombez sur les champs bruts de Mod. ReadItemBaseStats / ReadItemAggregatedStats acceptent tous les deux des adresses d'items d'inventaire OU des adresses de conteneurs WorldItem.
L'arbre UI du jeu est exposé sous forme d'adresses d'éléments uintptr_t. Commencez depuis une racine, parcourez les enfants, lisez les champs d'élément.
La manière propre de trouver un panneau connu par son 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
}Parcours manuel de l'arbre lorsque vous ne connaissez pas le 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 thresholdLes valeurs StringId sont des identifiants stables côté jeu ; préférez-les aux chemins codés en dur lorsqu'ils existent.
Note 0.5.x.
Ui.GetStringId()renvoie l'identifiant correct sur les clients actuels (0.5.x) — l'offset du champStringIdde l'élément a bougé (0x448→0x4C0) et le pont hôte a été corrigé en conséquence. (Pour le StringId numérique dans lequel les feuilles de champ du HUD du trial rendent leurs valeurs — un champ différent deGetText()—SekhemaHelperlitctx()->Sekhema.GetUiStringId().)
Trois helpers de projection, deux systèmes de coordonnées.
Perspective (monde 3D → écran) — même projection que le jeu utilise pour dessiner les choses dans le monde. Bon pour les plaques de noms, marqueurs de debug, indicateurs de cible :
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));
}Isométrique (grille → minimap) — pour les superpositions de style radar dessinées sur la grande ou la mini carte. Celles-ci respectent le zoom, le panoramique et la rotation de la carte visible :
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)Pour les calculs par lots (sauter les appels de fonction par entité), récupérez la transformation une fois et effectuez la projection en ligne :
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.Voir Plugins/Radar/src/Radar.cpp pour un radar fonctionnel construit entièrement sur ces appels.
La grille walkable est un bitmap de 4 bits par tuile indiquant les cellules de terrain sur lesquelles le joueur peut marcher. L'hôte la met à jour à chaque changement de zone ; les plugins reçoivent un handle stable qui survit jusqu'à ce que le plugin le libère (via 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 reflète la même forme mais contient un float par tuile (Data() est const float*, plus ElementCount() et SizeBytes()).
Ne vous abonnez pas à OnAreaChange pour rafraîchir le handle. L'événement se déclenche lorsque le worker de l'hôte détecte un changement de zone, mais la nouvelle grille walkable peut ne pas avoir encore été parsée — vous garderiez un pointeur obsolète pendant une ou deux frames. À la place, sondez par frame dans DrawUI :
auto current = ctx()->Terrain.GetWalkableGrid();
if (current.Data() != m_walkable.Data()) {
m_walkable = std::move(current); // swap when the host re-parses
}C'est peu coûteux (un appel ABI + une comparaison de pointeur). Voir Plugins/Radar/src/Radar.cpp pour la version de production.
Autres accesseurs de terrain :
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
});Abonnez-vous aux événements émis par l'hôte. Chaque Subscribe renvoie un Token que vous pouvez plus tard passer à Unsubscribe. Le destructeur EventsService (déclenché lorsque le plugin se désactive ou se décharge) libère automatiquement tout ce qui reste en cours — donc vous n'avez pas strictement besoin de vous désabonner manuellement, mais c'est poli.
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);
}
};Les quatre types d'événements sont AreaChange, Frame, GameAttached, GameDetached. Il existe également un Subscribe(EventKind, callback) générique si vous préférez construire une table de dispatch.
Le const_cast est nécessaire parce que Events mute sa carte de tokens interne. La classe de base renvoie const Context* pour décourager la mutation accidentelle d'autres services.
Si votre plugin possède plusieurs abonnements, le pattern ExamplePlugin est une manière propre de garder enable/disable symétriques — groupez les tokens et compteurs dans une seule structure d'état et routez tout par une seule paire 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;
}Voir Plugins/ExamplePlugin/examples/ExampleEvents.h pour le pattern complet.
ctx()->Overlay expose deux « indicateurs de requête » par plugin qui modifient le comportement global de la superposition. L'hôte stocke l'état par plugin, indexé par votre pointeur this, et le combine par OR avec les indicateurs intégrés de l'hôte (visibilité du menu principal, verrou AutoCraft, le sélecteur intégré Ajouter-Entité-depuis-la-Carte, la requête de chaque autre plugin). Effacés automatiquement à la désactivation/déchargement du plugin — un plugin planté ou buggé ne peut pas bloquer définitivement la superposition dans un état incohérent.
Par défaut, EntitiesService.Enumerate et Snapshot.Entities masquent les entités ayant EntityState::Useless (le filtre « endormi » de l'hôte qui exclut les monstres / PNJ / coffres dormants éloignés). Cela maintient le coût du snapshot par frame dans des limites raisonnables — une zone typique contient des centaines d'entités Useless dont le plugin n'a pas besoin.
Pour les UI de sélection sur la carte et les vues de débogage qui nécessitent l'ensemble complet des entités de la zone (afin que l'utilisateur puisse cliquer sur une entité pas encore active), désactivez le filtre :
ctx()->Overlay.SetIncludeSleepingEntities(true);
// Désormais ctx()->Entities.Enumerate voit aussi les entités Useless.
// Entity::IsSleeping marque spécifiquement celles qui proviennent de la collection
// SleepingEntities séparée de l'hôte (orthogonale à EntityState::Useless).Coût : environ +5–15 % de CPU de snapshot par frame lorsqu'activé. Laissez DÉSACTIVÉ si vous n'en avez pas besoin.
La fenêtre de superposition est normalement transparente aux clics (WS_EX_TRANSPARENT) : chaque clic de souris passe directement au jeu en dessous. C'est la valeur par défaut appropriée pour les superpositions en lecture seule (radar, barres de vie, afficheurs de DPS) — le joueur continue de jouer sans remarquer la superposition.
Dès que votre plugin veut que l'utilisateur clique sur quelque chose dans la superposition — confirmer une fenêtre contextuelle, sélectionner une entité sur la carte, faire glisser un marqueur — cette valeur par défaut pose problème. SetWantsOverlayInput(true) demande à l'hôte de commencer à consommer les clics de souris là où vos fenêtres ImGui les couvrent :
ctx()->Overlay.SetWantsOverlayInput(true);
// ...
// Une fois terminé (utilisateur a choisi, fermé la popup, appuyé sur Échap) :
ctx()->Overlay.SetWantsOverlayInput(false);La logique par frame de l'hôte capture les clics UNIQUEMENT là où le curseur survole une fenêtre ImGui visible — partout ailleurs, la transparence aux clics est préservée afin que le joueur puisse encore se déplacer/attaquer/ramasser autour de votre fenêtre contextuelle.
Portée (important) :
- Boutons de souris (LMB/RMB) : OUI — conditionné par cet indicateur.
- Position de souris / survol : TOUJOURS disponible quelle que soit la valeur. Les infobulles de survol n'ont pas besoin de cet indicateur.
-
Clavier : ATTEINT TOUJOURS le plugin via le WindowProc de l'hôte, indépendamment de cet indicateur.
ImGui::IsKeyPressed(ImGuiKey_Escape)fonctionne dans tous les cas.
Mise en garde sur la liste de dessin d'arrière-plan — À LIRE pour les plugins de sélection sur la carte
Si vous dessinez des marqueurs cliquables via ImGui::GetBackgroundDrawList() (typique pour les superpositions radar / grande carte), la liste de dessin d'arrière-plan n'a aucune fenêtre ImGui en support. Le test de collision de l'hôte parcourt ctx->Windows, ne trouve rien sous le curseur et réactive la transparence aux clics — vos marqueurs sont visibles mais non cliquables.
La solution : ouvrez une vraie fenêtre ImGui couvrant votre zone de sélection et placez-y un ImGui::InvisibleButton. C'est la fenêtre que l'hôte soumet au test de collision ; l'InvisibleButton vous fournit ImGui::IsItemClicked() pour la détection de clics. Exemple :
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();
// Dessinez les marqueurs via ImGui::GetForegroundDrawList() (ou 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) {
// l'utilisateur a sélectionné ce POI
}
}
return true;
});
ImGui::End();Des fenêtres plus petites couvrant uniquement la région du sélecteur fonctionnent de la même manière — l'hôte ne se soucie pas de la taille, seulement qu'une fenêtre se trouve sous le curseur.
class RadarPlugin : public PluginSDK::Plugin {
bool m_pickerMode = false;
void DrawUI() override {
if (!ctx()->Game.IsInGame()) return;
ImGui::SetCurrentContext((ImGuiContext*)ctx()->ImGuiContext);
// Bascule par raccourci. Le clavier atteint le plugin quel que soit l'état de capture
// de la superposition, cela fonctionne même en mode transparent aux clics.
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 du sélecteur (voir la mise en garde sur la liste de dessin d'arrière-plan
// ci-dessus pour le pattern ImGui::Begin + InvisibleButton).
}
void OnDisable() override {
// Sécurité redondante. L'hôte efface aussi les indicateurs à la désactivation, mais
// un nettoyage explicite maintient l'état cohérent si une frame synchrone
// se déclenche entre OnDisable et la gestion interne du PluginManager.
ctx()->Overlay.SetIncludeSleepingEntities(false);
ctx()->Overlay.SetWantsOverlayInput (false);
}
};- Les deux indicateurs sont idempotents — appeler
Set(true)deux fois de suite est sans effet la deuxième fois, le compteur n'est pas doublé. - Les deux sont agrégés par OR avec l'état propre de l'hôte et les indicateurs de chaque autre plugin. Plusieurs plugins en mode sélection simultanément coexistent bien.
- L'hôte efface automatiquement tous les indicateurs d'un plugin lorsqu'il est désactivé (via l'onglet Plugins, désactivation par crash, ou arrêt). Un crash en milieu de frame ne bloquera pas définitivement la superposition en mode capture — mais les plugins bien conçus couplent toujours leurs appels on/off pour que les autres plugins et le jeu restent réactifs entre-temps.
- Latence : un appel Set met à jour l'indicateur de manière synchrone, mais le changement de comportement effectif se manifeste à la frame d'hôte suivante (entrée de superposition) ou au prochain tick du worker GameClient (entités endormies). Sous la frame, imperceptible.
- Toutes les méthodes sont sûres à appeler depuis n'importe quel thread.
L'hôte charge les prix du marché une seule fois par session depuis poe2scout sur un thread d'arrière-plan et les expose à chaque plugin via ctx()->Prices. Les plugins ne récupèrent pas les prix eux-mêmes — il existe une base de prix partagée unique derrière le radar intégré, les superpositions de l'hôte et tous les plugins, de sorte que l'API est appelée une seule fois plutôt qu'une fois par consommateur.
- La ligue des prix est choisie par l'utilisateur dans Configuration → Paramètres (défaut Runes d'Aldur) et persistée côté hôte. Les plugins lisent toujours la ligue sélectionnée par l'utilisateur ; ils ne la choisissent pas.
- Le chargement est unique avec recul par catégorie (nouvelle tentative après 1 → 5 → 10 → 20 → 30 → 60 min en cas d'échec, puis abandon jusqu'au redémarrage de l'application). Il n'y a pas de rafraîchissement périodique — les prix sont stables pour toute la session.
- Chaque prix est exprimé en Chaos.
GetRates()fournit la conversion Divine / Exalted si vous souhaitez afficher dans ces unités.
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 — quel groupe poe2scout a correspondu (currency, fragments, runes,
// …, ou une catégorie d'item unique). Pratique pour distinguer monnaie vs. unique.
}LookupPrice prend le nom d'affichage d'un item (nom de monnaie, nom unique ou type de base) et effectue une correspondance approximative côté hôte dans toutes les catégories chargées. Une absence de correspondance renvoie found == false avec tous les prix à zéro.
Champ PriceResult
|
Type | Signification |
|---|---|---|
found |
bool |
Un prix a été trouvé |
chaos |
float |
Prix en Chaos Orbs (l'unité canonique) |
divine |
float |
Même prix exprimé en Divine Orbs |
exalt |
float |
Même prix exprimé en Exalted Orbs |
category |
std::string |
Catégorie poe2scout correspondante (currency / fragments / runes / … / une catégorie d'item unique) |
PluginSDK::PriceRates r = ctx()->Prices.GetRates(); // divineInChaos, exaltedInChaos
PluginSDK::PriceStatus s = ctx()->Prices.GetStatus();
if (!s.loaded) {
// Pas encore prêt (chargement en cours, ou toutes les catégories ont échoué).
// s.catsOk / s.catsPending / s.catsFailed indiquent l'avancement du chargeur.
}GetStatus().loaded est la condition à vérifier avant d'afficher les prix — il passe à true uniquement une fois que les taux de conversion et au moins la première catégorie sont arrivés. Jusque-là, affichez un état « chargement des prix… » plutôt que des zéros.
ctx()->Runeshape expose les appareils Expedition2Encounter (« Runeshape ») que l'hôte a résolus dans la zone courante, ainsi que la récompense que chaque recette accorderait. L'hôte effectue le parcours de la chaîne d'appareils et la correspondance de recettes hors ligne ; votre plugin se contente de lire le résultat. C'est ce qui alimente l'étiquette de récompense du radar intégré et la fenêtre Runeshape de NinjaPricer — un plugin tiers peut afficher les mêmes données.
for (const PluginSDK::Runeshape& rs : ctx()->Runeshape.Runeshapes()) {
// rs.color donne à chaque appareil une couleur distincte (utiliser pour grouper/teinter).
// rs.bestIndex est l'index de la récompense au prix le plus élevé (ou -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 : rune(s) se propageant au remnant suivant
ctx()->Log.Info((" propagates: " + rw.propagatingRunes).c_str());
}
}Champ Runeshape
|
Type | Signification |
|---|---|---|
entityId |
uint64_t |
Id d'entité de l'appareil — à passer à Rewards()
|
color |
uint32_t |
RGBA groupé, stable par appareil (pour le groupement / le teintage) |
isUnique |
bool |
L'appareil propose une recette d'objet unique |
holeCount |
int |
Nombre de logements de runes sur l'ancre |
anchorName |
std::string |
Nom de la rune ancre |
rewardCount |
int |
Nombre d'emplacements de récompense |
bestIndex |
int |
Index de la récompense avec le totalChaos le plus élevé, ou -1
|
propagatingSlots |
std::vector<int> |
Index(es) de logement de rune dont la rune se propage au remnant suivant (report 0.5.4) ; généralement 1, parfois 2 |
Champ RuneshapeReward
|
Type | Signification |
|---|---|---|
name |
std::string |
Nom de l'objet récompense |
count |
int |
Quantité accordée |
unitChaos |
float |
Prix par unité en Chaos (issu du service Prices) |
totalChaos |
float |
unitChaos × count |
priced |
bool |
Un prix a été trouvé pour cette récompense |
propagatingRunes |
std::string |
Rune(s) aux emplacements propagants de cette recette — ce qui est reporté si vous complétez cette recette ; ex. "Power" ou "Cold, Time" ; vide si la recette ne couvre pas le logement |
propagatingCount |
int |
Nombre de runes propagantes pour cette récompense |
propagatingHasRare |
bool |
Au moins une rune propagante est rare (« violette » / précieuse) |
Les prix des récompenses proviennent de la même base de données ctx()->Prices, donc une récompense non valorisée (priced == false) signifie généralement que les prix ne sont pas encore chargés, ou que l'objet n'est pas référencé sur poe2scout.
Propagation des runes (0.5.4). Chaque remnant choisit aléatoirement un logement de rune dont la rune est reportée au remnant suivant (en jeu : le surlignage à couronne dorée dans la liste Runeshape Recipes). Runeshape::propagatingSlots contient la liste brute des logements ; comme il s'agit d'une position de logement, la rune qui se propage diffère par recette, donc RuneshapeReward::propagatingRunes la résout par récompense. C'est ce qui alimente le point jaune de logement et le marqueur par récompense de NinjaPricer.
ctx()->Atlas expose le panneau live de l'atlas endgame — les nœuds de carte, leur adjacence par ancre, la sélection Rite courante et les poids d'éligibilité bruts — lus côté hôte via les offsets d'atlas de la GameLibrary. C'est ce qui alimente l'overlay d'atlas intégré et le plugin de référence ForetoldRewards. Tout part de GetPanel() : 0 signifie que l'UI de l'atlas n'est pas construite (pas en jeu / panneau fermé).
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| Méthode | Renvoie | Objectif |
|---|---|---|
GetPanel() |
uintptr_t |
Adresse du panneau d'atlas ; 0 = panneau absent |
Nodes(detail) |
std::vector<AtlasNode> |
Tous les nœuds de l'atlas (gridX/Y, uiAddress, marker, flags, biome, mapState) ; le detail par défaut saute les noms — résolvez via GetNodeName()
|
Connections() |
std::vector<AtlasConnection> |
Adjacence par ancre (x/y + jusqu'à 5 neighbors) ; dépend du viewport |
Selection(which) |
std::vector<AtlasGridPoint> |
which=0 cartes révélées de la ligne Rite, which=1 ancres choisies (dans l'ordre de choix) |
GetLineSeed() |
uint32_t |
Seed de sélection des récompenses Rite ; 0 = pas de ligne Rite / panneau absent |
Weights() |
std::vector<AtlasWeight> |
Lignes de poids d'éligibilité brutes (key, value) |
GetNodeName(uiAddress) |
std::string |
Nom affiché pour l'uiAddress d'un nœud ("" si non résolu) |
L'AtlasServiceAbi passé par valeur est gelé (un autre membre HostAbi a été ajouté après lui), donc toute future lecture d'atlas doit arriver comme une nouvelle fonction tail de HostAbi — jamais comme un nouveau membre d'AtlasServiceAbi.
ctx()->Sekhema expose les données de carte d'étage du Trial of the Sekhemas — le graphe statique des salles, les choix du run en cours et les lignes de FK de contenu de chaque salle — plus les lectures de flags StateMachine dont les salles du trial ont besoin. Les appels de graphe prennent explicitement l'adresse UI du panneau du trial : partez de GetPanel() (la résolution directe par index d'enfant de l'hôte) ou exécutez votre propre BFS de l'arbre UI, préfiltré avec le peu coûteux ProbeFloor(). C'est ce qui alimente le plugin de référence 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
}| Méthode | Renvoie | Objectif |
|---|---|---|
GetPanel() |
uintptr_t |
Panneau du trial résolu par l'hôte ; 0 = absent / mode manette / pas en jeu |
ProbeFloor(uiAddress) |
int |
Nombre de couches du FloorData à uiAddress, 0 = pas un étage de trial (préfiltre BFS peu coûteux) |
GetFloor(panel) |
SekhemaFloor |
En-tête d'étage : layerCount, roomCounts, choices (index choisi par couche, 0xFF=aucun), counter
|
Rooms(panel) |
std::vector<SekhemaRoom> |
Graphe statique des salles dans l'ordre (layer, index) ; chacune a des connections vers la couche suivante |
Content(panel) |
std::vector<SekhemaContentEntry> |
Lignes de FK de contenu par salle, résolues en rowId / rowName (dispatch sur tablePath) |
GetRoomUsedFlag(sm) |
int |
Flag StateMachine utilisée/fermée : 1=utilisée, 0=active, -1=illisible |
GetStateMachineValue(sm, i, out) |
bool |
Une valeur de shared-state (8 octets par entrée define_shared_state, dans l'ordre de définition) |
GetUiStringId(uiAddress) |
std::string |
Le StringId d'un élément UI en UTF-8 — le champ numérique dans lequel les feuilles du HUD du trial rendent leurs valeurs (pas Ui.GetText) |
Convention : <plugin directory>/config/settings.json. Directory() renvoie le chemin UTF-8 absolu vers le dossier de votre plugin.
Pour les paramètres triviaux, un writer JSON fait main fonctionne bien et garde la DLL auto-contenue. Voir Plugins/Radar/src/RadarSettings.h pour un exemple fonctionnel. Le squelette :
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()); }Pour les données structurées (objets imbriqués, tableaux), embarquez une vraie bibliothèque JSON dans votre dossier de plugin. L'hôte n'impose pas de choix.
SaveSettings est appelé périodiquement (~5s) et à la désactivation ; vous n'avez pas besoin de l'appeler vous-même.
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");Les quatre niveaux sont routés vers le logger central de l'hôte. Les messages apparaissent dans l'onglet Logs de l'hôte et dans le fichier journal sur disque. Formatez vous-même avant d'appeler ; l'hôte n'accepte pas de varargs de style printf.
En interne, les méthodes de commodité émettent les chaînes "Debug", "Info", "Warning" et "Error" (Warn se mappe sur "Warning"). Le pont de l'hôte effectue une correspondance insensible à la casse, donc un plugin appelant Log("warn", "msg") est tout de même routé correctement — mais les méthodes de commodité sont plus claires.
Primitives mémoire directes. Préférez les services de haut niveau chaque fois que possible — ils comprennent les offsets, gèrent les changements d'ABI et sont SEH-safe. Les lectures mémoire directes ne sont appropriées que lorsqu'aucun appel de plus haut niveau n'existe pour ce dont vous avez besoin.
// 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 patternSi vous vous retrouvez à les utiliser souvent, demandez-vous si les données dont vous avez besoin appartiennent aux services de plus haut niveau.
Chaque appel inter-DLL entre l'hôte et un plugin s'exécute à l'intérieur d'un bloc __try / __except côté hôte. Un plugin qui se comporte mal et déréférence un pointeur obsolète, divise par zéro, ou défaille autrement à l'intérieur d'un appel SDK obtient une erreur consignée — le processus hôte ne crashe pas, le jeu continue de tourner, et l'utilisateur peut continuer à utiliser d'autres plugins.
Cela ne signifie pas que les plugins peuvent être négligents. SEH attrape le symptôme, pas la cause. Si votre plugin lève des fautes à chaque frame, l'utilisateur voit un flot de journaux d'erreur et vos données sont effectivement indisponibles. Gérez les retours null des appels SDK, vérifiez les indicateurs Valid sur les données de composant, et ne déréférencez pas directement les adresses uintptr_t — passez-les par les appels ComponentsService / Ui / Memory qui enveloppent déjà RPM correctement.
L'hôte peut gérer un plugin buggé. Il ne peut pas gérer une DLL de plugin figée — un DrawSettings qui prend 100ms bloque tout le thread UI. Gardez le travail par frame peu coûteux.
Une courte liste de choses sur lesquelles les auteurs de plugins butent lors de l'intégration initiale. La plupart sont documentées en ligne plus haut ; rassemblées ici sous forme de checklist.
-
OnAreaChangese déclenche avant que la grille walkable ne soit re-parsée. Ne rafraîchissez pasWalkableGridHandledepuis l'événement — sondez par frame dansDrawUIet faites le swap quandData()change. (§11) -
Entity::Zoneest toujoursNonepour le joueur local. C'est une classification de distance-par-rapport-au-joueur, donc le joueur est par définition à distance zéro. Ne l'affichez pas dans les écrans d'info joueur. -
Components.ReadMods()ne renvoie que les indicateurs de résumé — pas de listes de mods. Pour les listes de mods par type, appelezInventory.ReadItemMods(entityAddr). (§8) -
Les items lâchés au sol peuvent manquer d'
EntitySubtype. Si vous filtrez les items dans le monde, préférezEntityType == Item || EntityType == Chestà une vérification de sous-type plus restrictive. -
Directory()renvoie un chemin absolu. N'ajoutez pas vous-même le répertoire de l'EXE — vous obtiendrezEXEDIR\EXEDIR\Plugins\Xet vos écritures de config atterriront en dehors du dossier du plugin. -
ctx()renvoieconst Context*. Les méthodes mutantes commeEventsService::Subscribenécessitent unconst_cast. C'est intentionnel — les services qui ne mutent pas devraient être impossibles à muter par accident. -
ImGui::SetCurrentContextest par DLL. Appelez-le à chaque point d'entrée qui dessine (OnEnable,DrawUI,DrawSettings) car la DLL du plugin a son propre état ImGui par défaut. -
Les helpers de commodité relisent le composant à chaque appel.
GetHealthPercent(addr)effectue unReadLife(addr)frais en interne. Si vous avez déjà la structureLifed'un appel précédent, accédez directement à ses champs au lieu de cela.
Résumé en une ligne de chaque méthode publique sur chaque service. Utilisez les sections de prose ci-dessus pour les signatures de types complètes et les notes d'utilisation.
| Méthode | Renvoie | Objectif |
|---|---|---|
GetSnapshot() |
Snapshot |
Vue complète par frame, incluant Entities
|
GetState() |
GameState |
Enum : InGame, Login, Loading, … |
IsAttached() |
bool |
Processus du jeu attaché |
IsInGame() |
bool |
State == InGame |
IsForeground() |
bool |
La fenêtre du jeu a le focus |
IsMenuVisible() |
bool |
Menu ESC / paramètres ouvert |
IsOverlayMode() |
bool |
L'hôte est en superposition (transparent au clic) |
GetProcessId() |
DWORD |
PID du jeu |
GetGameWindow() |
HWND |
Handle de la fenêtre du jeu |
GetScreenSize() |
ScreenSize |
Flottants {Width, Height}
|
GetGold() |
int |
Compteur d'or du personnage (0 quand pas en jeu) |
GetAreaId() |
std::string |
Id WorldArea brut de la zone courante (porte le numéro d'étage des Sekhemas) |
GetHiveblood(out) |
bool |
Ressource de l'arbre Genesis (Hiveblood) → out ; false sur les hôtes plus anciens / pas en jeu |
| Méthode | Renvoie | Objectif |
|---|---|---|
Enumerate(cb) |
— | Visiter chaque entité proche (renvoyer false pour arrêter) |
GetPlayer() |
Entity |
L'entité du joueur local |
FindById(id) |
std::optional<Entity> |
Recherche par id d'entité |
GetWorldItemInner(addr) |
std::optional<Entity> |
Entité interne d'item pour un conteneur WorldItem (items au sol) |
Watch(id) |
— | Épingler une entité pour que ses composants restent lisibles |
Unwatch(id) |
— | Libérer une surveillance |
IsWatched(id) |
bool |
État de surveillance |
GetWatchedComponents(id) |
std::optional<ComponentAddresses> |
Lire les composants épinglés |
| Méthode | Renvoie | Objectif |
|---|---|---|
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime |
Structure de composant | 21 lecteurs, un par type de composant |
EnumerateBuffs(addr) |
std::vector<Buff> |
Buffs actifs sur l'entité |
EnumerateActiveSkills(addr) |
std::vector<ActiveSkill> |
Compétences depuis un composant Actor (voir §7 pour le tableau des champs ActiveSkill) |
EnumerateSkillStats(skillDetailsAddr) |
std::vector<SkillStatEntry> |
Sets de stats évaluées d'UNE compétence ({SetIndex, StatId, Value}, StatId = ligne Stats.dat + 1) — le set 0 est le contexte courant, incl. le DPS du panneau de compétences (692, ×100) ; voir §7 |
EnumerateStats(addr) |
std::vector<StatEntry> |
Stats provenant d'items + buffs |
EnumerateItemMods(addr) |
std::vector<Mod> |
Mods accessibles depuis un composant Mods
|
ReadGroundEffect(entityAddr) |
GroundEffect |
Type + rayon d'un effet au sol depuis une entité VisibleServerGroundEffect — passez l'adresse de l'ENTITÉ ; faites correspondre sur TypeId (ShockedGround/IgnitedGround/…). Distingue les effets qui partagent un même chemin d'entité |
GetHealthPercent / GetEsPercent / GetManaPercent |
float |
Helpers de commodité en % |
IsAlive(addr) |
bool |
Santé > 0 |
GetItemRarity(addr) |
int |
Rareté depuis un composant Mods
|
IsItemIdentified(addr) |
bool |
Indicateur d'identification |
GetStackCount(addr) |
int |
Nombre actuel dans la pile |
IsChestOpened(addr) |
bool |
Indicateur de coffre ouvert |
GetPlayerName(addr) |
std::string |
Nom du joueur depuis un composant Player
|
GetWorldPosition(renderAddr, x, y, z) |
bool |
Accesseur de commodité pour les coords monde |
| Méthode | Renvoie | Objectif |
|---|---|---|
Scan(inventoryId) |
— | Déclencher un nouveau scan côté hôte (-1 = tous) |
Get(inventoryId) |
Inventory |
Un inventaire, items déjà peuplés |
GetItems(inventoryId) |
std::vector<InventoryItem> |
Items uniquement |
GetAll() |
std::vector<Inventory> |
Tous les inventaires scannés |
GetName(inventoryId) |
const char* |
Nom d'affichage ("Backpack", "Stash", …) |
ReadItemRarity(addr) |
int |
Rareté par entité (résout automatiquement les conteneurs WorldItem) |
ReadItemStackCount(addr) |
int |
Pile par entité (résout automatiquement les conteneurs WorldItem) |
ReadItemBaseTypeName(addr) |
std::string |
Type de base, résout automatiquement les conteneurs WorldItem |
ReadItemUniqueName(addr) |
std::string |
Nom unique, résout automatiquement les conteneurs WorldItem |
ReadItemPath(addr) |
std::string |
Chemin Metadata/Items/..., résout automatiquement les conteneurs WorldItem |
ReadItemMods(addr) |
ItemMods |
Indicateurs de résumé + 5 vecteurs de mods par type, résout automatiquement les conteneurs WorldItem |
FormatStat(statKey, v0, v1) |
std::string |
Texte en style in-game pour une clé de stat + valeur(s) via le formateur .csd de l'hôte ; vide tant que les descriptions ne sont pas chargées |
ReadItemBaseStats(addr) |
ItemBaseStats |
Valeurs défensives de base (Energy Shield calculé ; Ward/Armour/Evasion de base) ; Valid faux sans composant Armour ; résout automatiquement les conteneurs WorldItem |
ReadItemAggregatedStats(addr) |
std::vector<std::pair<int,int>> |
{statId, value} agrégés (waystone Item Rarity 8205 / Pack Size 8206 / Monster Rarity 8207 / Monster Effectiveness 8208 / Waystone Drop Chance 8209) ; résout automatiquement les conteneurs WorldItem |
| Méthode | Renvoie | Objectif |
|---|---|---|
Read(addr) |
UiElement |
Champs d'élément (rect, indicateurs, nombre d'enfants) |
GetChildren(addr) |
std::vector<uintptr_t> |
Adresses d'éléments enfants |
GetChildAt(addr, index) |
uintptr_t |
Un seul enfant par index |
FollowPath(root, indices, count) |
uintptr_t |
Parcourir un chemin d'index connu |
IsVisible(addr) |
bool |
L'élément est à l'écran |
GetStringId(addr) |
std::string |
Identifiant stable côté jeu |
GetText(addr) |
std::string |
Texte rendu |
ComputeScreenRect(addr, x, y, w, h) |
bool |
Rect espace-écran final |
GetGameUiRoot() |
uintptr_t |
Racine de l'UI en jeu |
GetUiRoot() |
uintptr_t |
Racine UI de niveau supérieur |
GetCullValue() |
int |
Seuil de cull UI de l'hôte |
FindPanelByStringId(parent, stringId) |
uintptr_t |
Recherche ciblée de descendant |
| Méthode | Renvoie | Objectif |
|---|---|---|
WorldToScreen(wx, wy, wz, sx, sy) |
bool |
Projection en perspective |
GridToLargeMap(gx, gy, worldZ, sx, sy) |
bool |
Projeter sur la superposition grande carte |
GridToMiniMap(gx, gy, worldZ, sx, sy) |
bool |
Projeter sur la minimap |
GetLargeMapTransform() |
MapTransform |
Transformation pré-multipliée pour calculs par lots |
GetMiniMapTransform() |
MapTransform |
Idem, pour la minimap |
| Méthode | Renvoie | Objectif |
|---|---|---|
GetWalkableGrid() |
WalkableGridHandle |
Handle RAII vers le bitmap walkable 4 bits par tuile |
GetHeightGrid() |
HeightGridHandle |
Handle RAII vers les hauteurs de terrain par tuile |
IsWalkable(gx, gy) |
bool |
Prédicat à une tuile |
GetTerrainHeight(gx, gy) |
float |
Z espace-monde |
GetWorldToGridConvertor() |
float |
Facteur de conversion monde → grille |
EnumerateTgtLocations(cb) |
— | Visiter chaque instance TGT dans la zone actuelle |
| Méthode | Renvoie | Objectif |
|---|---|---|
Read(addr, buf, size) |
bool |
RPM brut |
ReadString(addr) |
std::string |
Chaîne étroite null-terminée |
ReadWString(addr) |
std::wstring |
Chaîne large null-terminée |
ReadStdWString(addr) |
std::wstring |
Lit un conteneur std::wstring côté jeu (gère SSO) |
ReadStdVector(addr, elemSize, maxElems) |
std::vector<uint8_t> |
Octets bruts ; réinterpréter selon votre type |
GetBaseAddress() |
uintptr_t |
Base du module du jeu |
GetModuleSize() |
uintptr_t |
Taille du module du jeu |
GetPatternAddress(name) |
uintptr_t |
Recherche de pattern nommé |
| Méthode | Objectif |
|---|---|
Debug / Info / Warn / Error(msg) |
Émettre au niveau correspondant |
Log(level, msg) |
Chaîne de niveau personnalisé |
| Méthode | Renvoie | Objectif |
|---|---|---|
Subscribe(kind, cb) |
Token |
Dispatch générique |
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) |
Token |
Helpers d'abonnement en une ligne |
Unsubscribe(token) |
— | Libération manuelle (le destructeur libère automatiquement de toute façon) |
| Méthode | Renvoie | Objectif |
|---|---|---|
SetIncludeSleepingEntities(enable) |
— | Recevoir les entités EntityState::Useless dans EntitiesService.Enumerate (opt-in) |
SetWantsOverlayInput(enable) |
— | Demander à la superposition de capturer les clics souris au lieu d'être transparente aux clics |
Les deux sont idempotentes, par plugin, agrégées par OU avec l'hôte + les autres plugins, et automatiquement annulées lors de Disable/Unload. Voir la section 13 pour le modèle complet (y compris la mise en garde sur la liste de dessin en arrière-plan pour les plugins de sélection de carte).
| Méthode | Renvoie | Objectif |
|---|---|---|
GetFlask(slot) |
std::optional<Flask> |
Fiole de vie/mana par emplacement de ceinture (0=vie, 1=mana) ; nullopt hors plage ou pas en jeu |
GetCharm(slot) |
std::optional<Charm> |
Charme par emplacement de ceinture (0..2) ; nullopt hors plage ou pas en jeu |
AllFlasks() |
std::vector<Flask> |
Tous les emplacements de fioles, y compris vides (entrées FlaskSlotCount()) |
AllCharms() |
std::vector<Charm> |
Tous les emplacements de charmes, y compris vides (entrées CharmSlotCount()) |
FlaskSlotCount() |
int32_t |
Nombre d'emplacements de fioles (2 sur POE2) |
CharmSlotCount() |
int32_t |
Nombre d'emplacements de charmes (3 sur POE2) |
Voir la section 8 (« Fioles & charmes ») pour les tableaux de champs Flask / Charm et la limitation PerUseEffective.
| Méthode | Renvoie | Objectif |
|---|---|---|
LookupPrice(name) |
PriceResult |
Recherche de prix côté hôte par nom d'affichage (found, chaos, divine, exalt, category) |
GetRates() |
PriceRates |
Taux de conversion Divine / Exalted → Chaos |
GetStatus() |
PriceStatus |
Indicateur loaded + comptages par catégorie (catsOk / catsPending / catsFailed) |
| Méthode | Renvoie | Objectif |
|---|---|---|
Runeshapes() |
std::vector<Runeshape> |
Tous les dispositifs Expedition2Encounter résolus (id, couleur, ancre, bestIndex) |
Rewards(entityId) |
std::vector<RuneshapeReward> |
Emplacements de récompenses par dispositif, chacun tarifé via le service Prices |
| Méthode | Renvoie | Objectif |
|---|---|---|
GetPanel() |
uintptr_t |
Adresse du panneau d'atlas ; 0 = absent |
Nodes(detail) |
std::vector<AtlasNode> |
Tous les nœuds de l'atlas (coordonnées de grille, marker, mapState) |
Connections() |
std::vector<AtlasConnection> |
Adjacence par ancre (dépend du viewport) |
Selection(which) |
std::vector<AtlasGridPoint> |
0 = cartes révélées de la ligne Rite, 1 = ancres choisies |
GetLineSeed() |
uint32_t |
Seed de sélection des récompenses Rite (0 = aucun) |
Weights() |
std::vector<AtlasWeight> |
Lignes de poids d'éligibilité brutes (key, value) |
GetNodeName(uiAddress) |
std::string |
Nom affiché du nœud ("" si non résolu) |
| Méthode | Renvoie | Objectif |
|---|---|---|
GetPanel() |
uintptr_t |
Panneau du trial résolu par l'hôte ; 0 = absent |
ProbeFloor(uiAddress) |
int |
Nombre de couches du FloorData (0 = pas un étage de trial) ; préfiltre peu coûteux |
GetFloor(panel) |
SekhemaFloor |
En-tête d'étage (layerCount, roomCounts, choices, counter) |
Rooms(panel) |
std::vector<SekhemaRoom> |
Graphe statique des salles dans l'ordre (layer, index)
|
Content(panel) |
std::vector<SekhemaContentEntry> |
Lignes de FK de contenu par salle (rowId / rowName) |
GetRoomUsedFlag(sm) |
int |
1=utilisée, 0=active, -1=illisible |
GetStateMachineValue(sm, i, out) |
bool |
Une valeur de shared-state (ordre de définition) |
GetUiStringId(uiAddress) |
std::string |
StringId d'élément UI en UTF-8 (champ numérique du HUD) |
PluginAbi.h définit :
constexpr int PLUGIN_SDK_VERSION = 6;Au chargement, l'hôte appelle plugin->GetSDKVersion() et compare à sa propre PLUGIN_SDK_VERSION. Inadéquation → l'hôte journalise un avertissement et refuse de charger le plugin.
L'hôte vérifie également HostAbi::version et HostAbi::size_bytes à l'intérieur de PluginSDK_AttachHost (défini inline par PluginSDK.h lorsque PLUGIN_EXPORTS est défini). Si l'un ou l'autre champ ne s'accorde pas avec ce contre quoi le plugin a été construit, ctx() est non-fonctionnel. L'accesseur de la classe de base HostCompatible() renvoie false dans ce cas, et tout plugin qui veut être poli devrait refuser d'agir :
void OnEnable(bool) override {
if (!HostCompatible()) {
ctx()->Log.Error("Host ABI mismatch — disable plugin");
return;
}
// ...
}Quatre plugins dans le dépôt sont conçus pour être lus comme de la documentation :
-
Plugins/ExamplePlugin/— vitrine à large surface. Un plugin qui touche presque chaque service, organisé en 11 sous-fichiersexamples/Example*.h(Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) plus une bannière de résumé de couverture. Lisez ceci lorsque vous voulez voir comment un service est utilisé, en contexte. -
Plugins/Radar/— exemple ciblé du monde réel. ~200 lignes. Une superposition radar construite entièrement sur le SDK public — pas d'offsets, pas de lectures mémoire brutes. Rend la carte walkable plus des points par entité viaRender.GridToLargeMap. Lisez ceci lorsque vous voulez voir le minimum de code pour un résultat particulier. -
Plugins/KillCount/— tracker de kills/coffres/morts. SQLite + atlas de sprites + état par zone. Montre comment livrer persistance, fichiers de données embarqués et superposition, le tout dans une seule DLL. -
Plugins/NinjaPricer/— superposition de prix poe.ninja. Récupération HTTP (API Exchange) + scan d'inventaire + tarification par item. Montre du code réseau, l'ingestion de données tierces et l'itération d'inventaire dans un workflow réel.