Skip to content

Plugin Development Guide FR

Lafko edited this page Jun 25, 2026 · 14 revisions

← Home


Guide de Développement de Plugins

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.


1. Vue d'ensemble

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  (10 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 depuis PluginAbi.h en 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/.


2. Plugin Hello-world

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.


3. Configuration du projet

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 ; retourne PluginSDK::Plugin*.
  • DestroyPlugin — destructeur ; prend PluginSDK::Plugin*.
  • PluginSDK_AttachHost — connecte le Context. Défini pour vous à l'intérieur de PluginSDK.h et émis automatiquement lorsque PLUGIN_EXPORTS est 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));

4. Hooks du cycle de vie

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é.


5. Le Contexte

ctx() renvoie const PluginSDK::Context*, un agrégat de 10 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>
    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}

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.


6. Lecture de l'état du jeu

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 transition

Ce 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 (Entity complet), 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();

7. Lecture des composants

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.

Champs Entity

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)

Surveillance d'une entité spécifique

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.

Items au sol (conteneurs WorldItem)

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.


8. Inventaire

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

Mods d'items : deux API, deux portées

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.


9. Arbre UI

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 threshold

Les valeurs StringId sont des identifiants stables côté jeu ; préférez-les aux chemins codés en dur lorsqu'ils existent.


10. Rendu & projection

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.


11. Terrain & grille walkable

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

12. Événements

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.

Groupement d'abonnements

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.


13. Appareils Runeshape

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.


14. Persistance des paramètres

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.


15. Journalisation

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.


16. Mémoire (utilisateur avancé)

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 pattern

Si vous vous retrouvez à les utiliser souvent, demandez-vous si les données dont vous avez besoin appartiennent aux services de plus haut niveau.


17. Pont / sécurité SEH

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.


18. Pièges communs

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.

  1. OnAreaChange se déclenche avant que la grille walkable ne soit re-parsée. Ne rafraîchissez pas WalkableGridHandle depuis l'événement — sondez par frame dans DrawUI et faites le swap quand Data() change. (§11)
  2. Entity::Zone est toujours None pour 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.
  3. Components.ReadMods() ne renvoie que les indicateurs de résumé — pas de listes de mods. Pour les listes de mods par type, appelez Inventory.ReadItemMods(entityAddr). (§8)
  4. Les items lâchés au sol peuvent manquer d'EntitySubtype. Si vous filtrez les items dans le monde, préférez EntityType == Item || EntityType == Chest à une vérification de sous-type plus restrictive.
  5. Directory() renvoie un chemin absolu. N'ajoutez pas vous-même le répertoire de l'EXE — vous obtiendrez EXEDIR\EXEDIR\Plugins\X et vos écritures de config atterriront en dehors du dossier du plugin.
  6. ctx() renvoie const Context*. Les méthodes mutantes comme EventsService::Subscribe nécessitent un const_cast. C'est intentionnel — les services qui ne mutent pas devraient être impossibles à muter par accident.
  7. ImGui::SetCurrentContext est 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.
  8. Les helpers de commodité relisent le composant à chaque appel. GetHealthPercent(addr) effectue un ReadLife(addr) frais en interne. Si vous avez déjà la structure Life d'un appel précédent, accédez directement à ses champs au lieu de cela.

19. Référence rapide des services

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.

GameService

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}

EntitiesService

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

ComponentsService

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
EnumerateStats(addr) std::vector<StatEntry> Stats provenant d'items + buffs
EnumerateItemMods(addr) std::vector<Mod> Mods accessibles depuis un composant Mods
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

InventoryService

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

UiService

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

RenderService

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

TerrainService

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

MemoryService

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é

LogService

Méthode Objectif
Debug / Info / Warn / Error(msg) Émettre au niveau correspondant
Log(level, msg) Chaîne de niveau personnalisé

EventsService

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)

20. Versionnage

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

21. Exemples de plugins

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-fichiers examples/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é via Render.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.


← Home

Clone this wiki locally