Skip to content

Plugin Development Guide DE

Lafko edited this page Jun 25, 2026 · 14 revisions

← Home


Plugin-Entwicklungshandbuch

POEFixer-Plugins sind native C++-DLLs, die zur Laufzeit aus Plugins/<PluginName>/<PluginName>.dll geladen werden. Sie lesen den Live-Spielzustand, zeichnen ImGui-Overlays, persistieren ihre eigenen Einstellungen und abonnieren Host-Ereignisse.


1. Überblick

Das Plugin-SDK besitzt eine dreischichtige Architektur:

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
  • Plugin-Autoren binden genau einen Header ein: POEFixer/plugin_sdk/PluginSDK.h.
  • Dieser Header deklariert alles im Namespace PluginSDK:: und zieht darunter das C-ABI aus PluginAbi.h. Dass Letzteres existiert, kann man erwähnen; ein Blick hinein ist fast nie nötig.
  • Alle std::*-Container leben innerhalb der Plugin-DLL. Nur POD-Typen überqueren die Hostgrenze. Damit verheddert sich ein mit einer anderen Toolchain-Version gebautes Plugin nicht im STL des Hosts – die einzigen geteilten Typen sind Ganzzahlen, Gleitkommawerte, Zeiger und kleine Structs.

Die SDK-Header finden Sie hier:

  • POEFixer/plugin_sdk/PluginSDK.h – der C++-Wrapper, den Plugin-Autoren verwenden.
  • POEFixer/plugin_sdk/PluginAbi.h – das reine C-ABI darunter.

Referenz-Plugins im Repository (lesen Sie diese als Dokumentation): Plugins/ExamplePlugin/, Plugins/Radar/, Plugins/KillCount/, Plugins/NinjaPricer/.


2. Hello-World-Plugin

Minimales Plugin, das geladen wird und eine Meldung in das Host-Log schreibt:

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

Als Plugins/Hello/Hello.dll bauen, Host neu starten, im Tab Plugins aktivieren.


3. Projekt-Setup

Verwenden Sie Plugins/ExamplePlugin/ExamplePlugin.vcxproj als kanonische Vorlage. Die wesentlichen Einstellungen:

  • Konfigurationstyp: DynamicLibrary
  • Plattform-Toolset: v143 (Visual Studio 2022)
  • Zeichensatz: Unicode
  • Sprachstandard: stdcpp20
  • Laufzeitbibliothek: MultiThreadedDLL (Release) / MultiThreadedDebugDLL (Debug). MUSS mit dem Host übereinstimmen.
  • Präprozessor-Definitionen: PLUGIN_EXPORTS;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_WARNINGS
  • Zusätzliche Include-Verzeichnisse: $(SolutionDir)POEFixer
  • Ausgabeverzeichnis: $(SolutionDir)x64\Release\Plugins\<YourPlugin>\
  • Zielname: muss mit dem Ordnernamen übereinstimmen (Plugins/MyPlugin/MyPlugin.dll)

Der Host scannt jeden Unterordner in Plugins/ und sucht nach <FolderName>.dll. Die DLL muss drei Symbole exportieren:

  • CreatePlugin – Factory; gibt PluginSDK::Plugin* zurück.
  • DestroyPlugin – Destruktor; nimmt PluginSDK::Plugin*.
  • PluginSDK_AttachHost – verdrahtet den Context. Wird für Sie innerhalb von PluginSDK.h definiert und automatisch emittiert, wenn PLUGIN_EXPORTS gesetzt ist.

Empfohlenes Quellcode-Layout:

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() liefert einen absoluten, UTF-8-codierten Pfad mit Wurzel im Host-EXE-Verzeichnis – stellen Sie den EXE-Pfad nicht selbst voran. Die Verzeichniszeichenkette wird per Wert innerhalb von PluginSDK::Plugin gehalten, sodass sie Container-Reallokationen und Reload-Zyklen des Hosts ohne Lebensdauerprobleme übersteht.

Wenn Sie ImGui zeichnen möchten, fügen Sie zusätzlich diese Dateien in <ClCompile> ein (der Host linkt sie ebenfalls, aber Plugin-seitiges ImGui ist pro-DLL):

..\..\POEFixer\imgui\imgui.cpp
..\..\POEFixer\imgui\imgui_draw.cpp
..\..\POEFixer\imgui\imgui_tables.cpp
..\..\POEFixer\imgui\imgui_widgets.cpp

In OnEnable an den ImGui-Context des Hosts anbinden:

if (ctx()->ImGuiContext)
    ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx()->ImGuiContext));

4. Lebenszyklus-Hooks

PluginSDK::Plugin ist eine virtuelle Basisklasse. Überschreiben Sie diese Methoden in Ihrem Plugin (ungefähr in Aufrufreihenfolge):

Methode Wird aufgerufen Typische Verwendung
const char* GetName() const Einmal, direkt nach der Konstruktion Den Anzeigenamen Ihres Plugins zurückgeben
void OnEnable(bool isGameAttached) Wenn der Benutzer das Plugin aktiviert (oder beim Start, falls persistiert) Einstellungen laden, Events abonnieren, ImGui-Context anbinden
void DrawSettings() Jeden Frame, solange das Einstellungs-Panel des Plugins geöffnet ist ImGui-Bedienelemente für Ihre Konfiguration
void DrawUI() Jeden Frame, solange das Plugin aktiviert ist ImGui-Overlay zeichnen (für ein Spiel-Overlay ImGui::GetBackgroundDrawList() verwenden)
bool WantsOverlay() const Wird jeden Frame abgefragt true zurückgeben, wenn der Host im Overlay-Modus (klickbar durch) laufen soll
void SaveSettings() Periodisch (~5 s) und beim Deaktivieren Konfiguration auf die Festplatte schreiben
void OnDisable() Wenn der Benutzer deaktiviert oder beim Herunterfahren des Hosts Ressourcen freigeben, Event-Abos kündigen

Nur GetName ist verpflichtend; alle übrigen besitzen sichere Defaults.

Der Host ruft außerdem GetSDKVersion() (auf der Basisklasse definiert, NICHT überschreiben) unmittelbar nach CreatePlugin auf, um zu prüfen, dass Plugin und Host übereinstimmen. Bei einer Diskrepanz → Plugin abgelehnt.


5. Der Context

ctx() liefert const PluginSDK::Context*, ein Aggregat aus 14 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
    void* ImGuiContext;            // pass to ImGui::SetCurrentContext
    void* D3DDevice;               // ID3D11Device* for texture loading
};

Auf einen Blick – wofür jeder Service zuständig ist:

Service Wann Sie ihn greifen
GameService Snapshot, Zustands-Flags, Bildschirm- und Fensterinformationen
EntitiesService Enumerieren, Lookup per ID, Lifecycle beobachten
ComponentsService 21 Reader + 4 Enumeratoren + ~10 Komfort-Helfer
InventoryService Scannen, enumerieren, Mod-Reads pro Item
UiService Baum durchlaufen, FindPanelByStringId, ComputeScreenRect
RenderService WorldToScreen, GridTo{Large,Mini}Map, Transforms
TerrainService Begehbarkeits- und Höhen-Grids (RAII), TGT-Standorte
MemoryService RPM-Primitive – nur wenn kein höherer Aufruf passt
LogService Debug / Info / Warn / Error
EventsService Subscribe / Unsubscribe / On{Area,Frame,Attach,Detach}
OverlayService SetIncludeSleepingEntities / SetWantsOverlayInput – Map-Picker-freundlich
FlasksService Lebens-/Mana-Kolben + Talismane – Ladungen, Usable, Active, Verbrauch, Mod-Anzahl
PricesService LookupPrice / GetRates / GetStatus – vom Host geladene poe2scout-Preise, für alle Plugins geteilt
RuneshapeService Runeshapes / Rewards – aufgelöste Expedition2Encounter-Geräte + geräteweise Belohnungen

ctx() ist gültig ab dem Moment, in dem der Host OnEnable aufruft, bis OnDisable zurückkehrt. Cachen Sie ctx() nicht über Hot-Reloads oder DLL-Entladegrenzen hinweg.


6. Spielzustand lesen

ctx()->Game.GetSnapshot() liefert einen wertbasierten Snapshot – eine vollständige, unveränderliche Sicht auf den aktuellen Frame. GetSnapshot() durchläuft abi->entities.enumerate und befüllt snap.Entities, bevor zurückgekehrt wird, sodass die Kosten mit der Anzahl naher Entities skalieren. Rufen Sie es einmal pro Frame auf und verwenden Sie es wieder.

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

Was der Snapshot direkt mitliefert (keine weiteren Service-Aufrufe nötig):

  • Zustand und Flags: State, IsAttached, IsWindowValid, GameWindowForeground, IsTown, IsHideout, IsPaused, IsSkillTreeVisible.
  • Gebiet: CurrentAreaName, CurrentAreaHash, CurrentAreaLevel, AreaChangeCounter.
  • Welt: Player (vollständiges Entity), Entities (vollständiger std::vector<Entity>), Vitals, LargeMap, MiniMap, WorldToScreenMatrix[16].
  • Fenster: ScreenWidth, ScreenHeight, ProcessId, GameWindow, LastUpdateTime, WorldToGridConvertor.

Was nicht im Snapshot enthalten ist – über Services abrufen: Inventarinhalt (InventoryService), Buffs (ComponentsService::EnumerateBuffs), Mod-Listen pro Item (InventoryService::ReadItemMods), UI-Panels (UiService).

Günstige Helfer, wenn Sie keinen vollen Snapshot benötigen:

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. Komponenten lesen

Entities legen ihre Komponenten über entity.Components offen – ein ComponentAddresses-Struct aus uintptr_t-Adressen. Übergeben Sie jede Adresse an das passende ComponentsService::Read*, um einen wertbasierten Snapshot zu erhalten.

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

Es gibt 21 Komponenten-Reader: ReadLife, ReadRender, ReadPositioned, ReadTargetable, ReadChest, ReadShrine, ReadStack, ReadCharges, ReadPlayer, ReadAnimated, ReadTransitionable, ReadTriggerableBlockage, ReadMinimapIcon, ReadStateMachine, ReadBase, ReadMods, ReadStats, ReadBuffs, ReadActor, ReadNpc, ReadDiesAfterTime.

ComponentAddresses selbst hält 24 Slots: die 21 oben genannten plus drei Marker (Buffs, WorldItem, AreaTransition) und OMP (host-intern). Buffs ist ein Präsenz-Marker – die eigentliche Buff-Liste kommt von EnumerateBuffs. WorldItem / AreaTransition sind Entity-Typ-Marker, keine echten Komponenten. Alle Slots haben passende HasX()-Prädikate auf ComponentAddresses.

Sammlungsbasierte Reader für Komponenten mit variabel großen Daten:

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>

Komfort-Helfer (Einmalaufruf – sie rufen intern Read* für Sie auf):

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)) { ... }

Ein Valid-Flag auf jedem zurückgegebenen Struct erlaubt es Ihnen, „Komponentenadresse war 0 / Lesen fehlgeschlagen” ohne Exceptions zu behandeln. Wenn Sie das übergeordnete Struct (Life, Mods, …) bereits haben, greifen Sie direkt auf dessen Felder zu, anstatt den Helfer erneut aufzurufen – der Helfer liest die Komponente jedes Mal neu.

Bodeneffekte unterscheiden — viele Bodeneffekte teilen sich den einzelnen Entity-Pfad Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect, sodass der Pfad allein nicht zwischen Shocked Ground und Burning Ground unterscheiden kann. ReadGroundEffect löst die GroundEffect-Komponente der Entity und deren groundeffects.datc64-Zeile auf. Übergeben Sie die Entity-Adresse (die GroundEffect-Komponente ist nicht in Components enthalten, daher löst der Host sie für Sie auf — gleiche Konvention wie bei ReadPathfinding), und prüfen Sie dann auf TypeId, den stabilen, patch-unabhängigen Schlüssel:

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 -> Welteinheiten; zeichnen Sie einen Kreis bei (e.WorldX, e.WorldY, e.WorldZ) mit diesem Radius
    if (ge.TypeId == “ShockedGround”) {
        // Hervorhebung gemäß Ihrer Konfiguration (Farbe/Alpha nach ge.TypeId)
    }
}

Das GroundEffect-Struct:

Feld Bedeutung
Valid false, wenn die Entity keine GroundEffect-Komponente hat oder das Lesen fehlgeschlagen ist
TypeId groundeffecttypes-Id — der stabile Schlüssel zum Abgleichen (z. B. ShockedGround)
Radius Effektradius in Welteinheiten; 0, wenn die Variante ihn nicht setzt
EndEffect Endverhalten: fadeout / close / end
BuffVisual1 buffvisuals-Id (z. B. ground_fire_burn_white); leer, wenn nicht gesetzt
BuffVisual2 buffdefinitions-Name (z. B. ground_tar_gold); leer, wenn nicht gesetzt
AoFile Erster .ao/.aoc-Visualisierungspfad; leer, wenn keiner vorhanden
GroundEffectsRowAddr / GroundEffectTypesRowAddr Rohe Dat-Zeilen-Zeiger (sitzungsstabil) für erweitertes Cross-Referencing

Die Weltposition des Effekts stammt von der Entity selbst (Entity.WorldX/Y/Z oder den Render-/Positioned-Komponenten) und wird daher nicht auf dem Struct dupliziert. ReadGroundEffect liest bei jedem Aufruf neu, daher sollten Sie das Ergebnis pro Scan-Intervall cachen. Es gibt ein ungültiges GroundEffect zurück, wenn der Host vor dieser API gebaut wurde (es liegt auf dem append-only Tail von SDK v6 und wird auf Null geprüft).

Entity-Felder

Jede Entity (einschließlich snap.Player und Elementen von snap.Entities) trägt denselben Satz an Feldern:

Gruppe Felder
Identität Id, Address, EntityDetailsAddress, RenderComponentAddress, IsValid
Klassifikation EntityType, EntitySubtype, EntityState, Rarity, Reaction, Zone (NearbyZone: InnerCircle≈60 / OuterCircle≈120 / Far)
Position GridPositionX, GridPositionY, TerrainHeight, WorldX/Y/Z, ModelBoundsZ
Schnelle Vitalwerte CurrentHP, MaxHP, CurrentES, MaxES (vermeidet einen ReadLife, wenn Sie nur die Summen brauchen)
Strings Path (std::wstring, Metadata/...), PlayerName (std::wstring), TgtPath (std::string, Asset-Pfad)
Zustand IsSleeping, IsChestOpened
Komponenten Components (ComponentAddresses-Sub-Struct)

Eine bestimmte Entity beobachten

Wenn Sie eine Entity über mehrere Frames hinweg verfolgen müssen (z. B. eine Truhe, die der Spieler gerade öffnet) und nicht jeden Frame die gesamte Entity-Liste durchscannen möchten, registrieren Sie einen Watch:

ctx()->Entities.Watch(entityId);
// ...later:
if (auto opt = ctx()->Entities.GetWatchedComponents(entityId)) {
    PluginSDK::ComponentAddresses comps = *opt;
    PluginSDK::Life l = ctx()->Components.ReadLife(comps.Life);
}
bool active = ctx()->Entities.IsWatched(entityId);
ctx()->Entities.Unwatch(entityId);

FindById(id) liefert ein std::optional<Entity> für Einmal-Lookups, und GetPlayer() gibt immer den lokalen Spieler zurück.

Bodengegenstände (WorldItem-Container)

Auf den Boden geworfene Items erscheinen in snap.Entities als EntityType::Item-Entities am Pfad Metadata/MiscellaneousObjects/WorldItem. Dies sind Container-Entities — sie tragen Mods / Base / Stack / Sockets nicht direkt. Die eigentliche Item-Entity liegt eine Indirektion weiter.

Um die innere Item-Entity als regulären Entity-Snapshot zu erhalten, verwenden Sie 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 ist nur bei echten WorldItem-Containern erfolgreich — der Aufruf mit einer Inventar-Item-Adresse liefert std::nullopt. Wenn Sie dieselbe Datenform wie bei Inventar-Items wünschen (ohne manuelles Durchwandern der Komponenten), löst die Inventory.ReadItem*-Familie aus dem nächsten Abschnitt WorldItem-Container transparent auf.


8. Inventar

ctx()->Inventory.Scan(inventoryId) löst auf der Host-Seite einen erneuten Scan aus. Verwenden Sie -1, um alle Inventare zu scannen.

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

Jedes Inventory legt zudem ein Grid-Struct offen, das beschreibt, wo das Inventar auf dem Bildschirm gezeichnet wird:

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

Um ein einzelnes Inventar per ID zu greifen (liefert dasselbe Struct mit bereits gefüllten Items):

PluginSDK::Inventory backpack = ctx()->Inventory.Get(/*inventoryId=*/0);

Oder, wenn Sie nur den Item-Vektor ohne das umschließende Struct wollen:

std::vector<PluginSDK::InventoryItem> items = ctx()->Inventory.GetItems(0);

Item-Mods: zwei APIs, zwei Reichweiten

ComponentsService::ReadMods(addr) liefert nur zusammenfassende Flags (IsCorrupted, IsRelic, IsSplit, IsMirrored, IsSynthesised, IsIdentified, Rarity, ItemLevel, RequiredLevel, CraftedModCount). Die einzelnen Mod-Listen pro Art sind nicht enthalten.

Für das vollständige Bild (Zusammenfassung + Mod-Listen) verwenden Sie 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)  { ... }

Weitere direkte Reads pro Entity (günstiger als ein erneuter Scan, wenn Sie bereits eine Item-Adresse halten):

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

Bodengegenstände über die Inventory-API. Alle sieben Inventory.ReadItem*-Reads oben (und ReadItemMods) akzeptieren SOWOHL Inventar-Item-Adressen ALS AUCH WorldItem-Container-Adressen. Container-Adressen werden vor dem Lesen automatisch zum inneren Item aufgelöst, sodass derselbe Plugin-Code-Pfad sowohl für Items in Taschen als auch für Items auf dem Boden funktioniert:

// `addr` kann entweder eine Inventar-Item-Adresse oder ein WorldItem-Container sein.
PluginSDK::ItemMods im = ctx()->Inventory.ReadItemMods(addr);
int          rarity   = ctx()->Inventory.ReadItemRarity(addr);
std::string  baseName = ctx()->Inventory.ReadItemBaseTypeName(addr);

Wenn Sie die Komponenten-Adressen des inneren Items direkt benötigen (z. B. um ctx()->Components.ReadStack(...) aufzurufen oder Sockets zu durchlaufen), verwenden Sie stattdessen Entities.GetWorldItemInner aus Abschnitt 7.

Mod-Text im Spielstil + Basis- und aggregierte Stats (v6, 2026-06-24). Formatieren Sie einen beliebigen Stat-Schlüssel in denselben Text, den der Spieltooltip anzeigt, und lesen Sie die defensiven Basiswerte eines Items sowie aggregierte Karten-/Wegstein-Eigenschaften:

// Einen Mod so darstellen, wie das Spiel es tut ("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()) { /* `text` zeichnen */ }
}

// Defensive Basiswerte des Items. EnergyShield ist der im Spiel angezeigte (berechnete) Wert;
// Ward/Armour/Evasion sind die Basiswerte des Items. Valid == false, wenn das Item
// keine Armour-Komponente hat (Währung, Gems, Schmuck, Wegsteine, ...).
PluginSDK::ItemBaseStats bs = ctx()->Inventory.ReadItemBaseStats(item.Address);
if (bs.Valid) { /* bs.EnergyShield, bs.Ward, bs.Armour, bs.Evasion */ }

// Aggregierte Stats nach Stat-ID — z. B. Item-Seltenheit eines Wegsteins (8205),
// Packungsgröße (8206), Monster-Seltenheit (8207), Monster-Effektivität (8208),
// Wegstein-Dropp-Chance (8209).
for (const auto& [statId, value] : ctx()->Inventory.ReadItemAggregatedStats(item.Address)) {
    // statId selbst einer Beschriftung zuordnen; Werte sind vorzeichenbehaftete Prozentwerte
}

FormatStat verwendet den .csd-Stat-Beschreibungssatz des Hosts (wird beim ersten Aufruf heruntergeladen) und gibt daher einen leeren String zurück, bis diese Daten bereit sind — greifen Sie in diesem Fall auf die rohen Mod-Felder zurück. ReadItemBaseStats / ReadItemAggregatedStats akzeptieren jeweils Inventar-Item- ODER WorldItem-Container-Adressen.


9. UI-Baum

Der UI-Baum des Spiels wird als uintptr_t-Element-Adressen offengelegt. Starten Sie an einer Wurzel, durchlaufen Sie die Kinder und lesen Sie Elementfelder.

Der saubere Weg, ein bekanntes Panel über seine StringId zu finden:

uintptr_t gameUiRoot = ctx()->Ui.GetGameUiRoot();
uintptr_t invPanel   = ctx()->Ui.FindPanelByStringId(gameUiRoot, "Inventory");
if (invPanel && ctx()->Ui.IsVisible(invPanel)) {
    // panel is on-screen
}

Manuelles Durchlaufen des Baums, wenn Sie die StringId nicht kennen:

uintptr_t root = ctx()->Ui.GetUiRoot();
PluginSDK::UiElement e = ctx()->Ui.Read(root);
ctx()->Log.Info(("children=" + std::to_string(e.ChildCount)).c_str());

for (uintptr_t child : ctx()->Ui.GetChildren(root)) {
    std::string sid = ctx()->Ui.GetStringId(child);
    if (sid == "InventoriesPanel") { /* found it */ }
}

// Or use a known index path:
int path[] = { 5, 1, 2, 0 };
uintptr_t logInButton = ctx()->Ui.FollowPath(root, path, 4);

// Compute screen-space rect (post-scale, post-transform):
float x, y, w, h;
if (ctx()->Ui.ComputeScreenRect(invPanel, x, y, w, h)) {
    // draw an overlay box at (x,y,w,h)
}

// Get displayed text:
std::string label = ctx()->Ui.GetText(child);

int cull = ctx()->Ui.GetCullValue();  // host's UI cull threshold

StringId-Werte sind stabile spielseitige Identifikatoren; bevorzugen Sie sie gegenüber fest codierten Pfaden, wann immer es welche gibt.


10. Rendering und Projektion

Drei Projektions-Helfer, zwei Koordinatensysteme.

Perspektivisch (3D-Welt → Bildschirm) – dieselbe Projektion, mit der das Spiel Dinge in der Welt zeichnet. Gut für Namensschilder, Debug-Marker, Zielindikatoren:

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

Isometrisch (Grid → Minimap) – für radarartige Overlays, die auf der großen oder kleinen Karte gezeichnet werden. Diese respektieren Zoom, Pan und Rotation der sichtbaren Karte:

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)

Für gebündelte Mathematik (Funktionsaufrufe pro Entity vermeiden) holen Sie die Transformation einmal und führen die Projektion inline durch:

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.

Siehe Plugins/Radar/src/Radar.cpp für ein funktionierendes Radar, das vollständig auf diesen Aufrufen aufbaut.


11. Terrain und begehbares Grid

Das begehbare Grid ist eine 4-Bit-pro-Kachel-Bitmap, die angibt, welche Terrain-Zellen der Spieler betreten kann. Der Host aktualisiert es bei jedem Gebietswechsel; Plugins erhalten ein stabiles Handle, das überlebt, bis das Plugin es freigibt (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 spiegelt die gleiche Form, hält aber ein float pro Kachel (Data() ist const float*, ergänzt um ElementCount() und SizeBytes()).

Abonnieren Sie OnAreaChange nicht, um das Handle zu aktualisieren. Das Event feuert, wenn der Host-Worker einen Gebietswechsel erkennt, aber das neue begehbare Grid ist möglicherweise noch nicht geparst – Sie würden für ein oder zwei Frames einen veralteten Zeiger halten. Pollen Sie stattdessen pro Frame in DrawUI:

auto current = ctx()->Terrain.GetWalkableGrid();
if (current.Data() != m_walkable.Data()) {
    m_walkable = std::move(current);   // swap when the host re-parses
}

Das ist günstig (ein ABI-Aufruf plus ein Zeiger-Vergleich). Siehe Plugins/Radar/src/Radar.cpp für die produktive Version.

Weitere Terrain-Accessoren:

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

Abonnieren Sie vom Host emittierte Ereignisse. Jedes Subscribe liefert ein Token, das Sie später an Unsubscribe übergeben können. Der Destruktor von EventsService (ausgelöst beim Deaktivieren oder Entladen des Plugins) gibt alles noch Ausstehende automatisch frei – Sie müssen also nicht zwingend manuell abbestellen, es ist aber höflich.

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

Die vier Event-Arten sind AreaChange, Frame, GameAttached, GameDetached. Es gibt zudem ein generisches Subscribe(EventKind, callback), falls Sie lieber eine Dispatch-Tabelle aufbauen.

Der const_cast ist erforderlich, weil Events seine interne Token-Map mutiert. Die Basisklasse liefert const Context*, um versehentliches Mutieren anderer Services zu erschweren.

Abonnements gruppieren

Wenn Ihr Plugin mehrere Abonnements besitzt, ist das Muster aus ExamplePlugin eine saubere Methode, Aktivieren und Deaktivieren symmetrisch zu halten – bündeln Sie Tokens und Zähler in ein einziges Status-Struct und leiten Sie alles über ein einziges SubscribeAll/UnsubscribeAll-Paar:

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

Siehe Plugins/ExamplePlugin/examples/ExampleEvents.h für das vollständige Muster.


13. OverlayService — Eingabeerfassung und schlafende Entities

ctx()->Overlay stellt zwei pluginspezifische „Request-Flags" bereit, die das overlay-weite Verhalten ändern. Der Host speichert den Zustand pro Plugin, indiziert über Ihren this-Zeiger, und OR-aggregiert ihn mit den eigenen internen Flags des Hosts (Sichtbarkeit des Hauptmenüs, AutoCraft-Sperre, der eingebaute Entity-aus-Map-Picker des Hosts sowie die Anfragen aller anderen Plugins). Beim Deaktivieren oder Entladen des Plugins werden die Flags automatisch zurückgesetzt – ein abgestürztes oder fehlerhaftes Plugin kann das Overlay nicht dauerhaft in einem ungewöhnlichen Zustand feststecken.

SetIncludeSleepingEntities

Standardmäßig blenden EntitiesService.Enumerate und Snapshot.Entities Entities mit EntityState::Useless aus (der „schlafende"-Filter des Hosts, der weit entfernte, inaktive Monster/NPCs/Truhen verwirft). Dadurch bleibt der Snapshot-Aufwand pro Frame begrenzt – ein typisches Gebiet enthält hunderte Useless-Entities, die das Plugin nicht interessieren.

Für Map-Picker-UIs und Debug-Viewer, die den vollständigen Entity-Pool des Gebiets benötigen (damit der Benutzer auf eine noch inaktive Entity klicken kann), schalten Sie den Filter ab:

ctx()->Overlay.SetIncludeSleepingEntities(true);
// Jetzt sieht ctx()->Entities.Enumerate auch Useless-Entities.
// Entity::IsSleeping kennzeichnet speziell jene, die aus der separaten
// SleepingEntities-Sammlung des Hosts stammen (orthogonal zu EntityState::Useless).

Kosten: ca. +5–15 % Snapshot-CPU pro Frame während der Aktivierung. Lassen Sie es AUS, sofern Sie es nicht benötigen.

SetWantsOverlayInput

Das Overlay-Fenster ist normalerweise klickdurchlässig (WS_EX_TRANSPARENT): Jeder Mausklick wird direkt an das darunterliegende Spiel weitergegeben. Dies ist der richtige Standard für rein lesende Overlays (Radar, Lebensbalken, DPS-Anzeigen) – der Spieler spielt weiter, ohne das Overlay zu bemerken.

Sobald Ihr Plugin möchte, dass der Benutzer auf etwas innerhalb des Overlays klickt – ein Popup bestätigt, eine Entity auf der Karte auswählt, einen Marker zieht – funktioniert dieser Standard nicht mehr. SetWantsOverlayInput(true) weist den Host an, Mausklicks dort zu empfangen, wo Ihre ImGui-Fenster sie abdecken:

ctx()->Overlay.SetWantsOverlayInput(true);
// ...
// Wenn fertig (Benutzer hat ausgewählt, Popup geschlossen, Escape gedrückt):
ctx()->Overlay.SetWantsOverlayInput(false);

Die Pro-Frame-Logik des Hosts beansprucht Klicks NUR dort, wo sich der Cursor über einem sichtbaren ImGui-Fenster befindet – überall sonst bleibt die Klickdurchlässigkeit erhalten, sodass der Spieler sich weiterhin bewegen, angreifen und plündern kann.

Reichweite (wichtig):

  • Maustasten (LMB/RMB): JA – durch dieses Flag gesteuert.
  • Mausposition / Hover: Funktioniert IMMER, unabhängig vom Flag. Hover-Tooltips benötigen dieses Flag nicht.
  • Tastatur: Erreicht das Plugin IMMER über den WindowProc des Hosts, unabhängig von diesem Flag. ImGui::IsKeyPressed(ImGuiKey_Escape) funktioniert in beiden Fällen.

Hintergrund-Draw-List-Hinweis — LESEN SIE DIES für Map-Picker-Plugins

Wenn Sie klickbare Marker über ImGui::GetBackgroundDrawList() zeichnen (typisch für Radar-/Großkarten-Overlays), besitzt die Hintergrund-Draw-List kein darunterliegendes ImGui-Fenster. Der Hit-Test des Hosts durchläuft ctx->Windows, findet nichts unter dem Cursor und aktiviert die Klickdurchlässigkeit erneut – Ihre Marker sind sichtbar, aber nicht anklickbar.

Die Lösung: Öffnen Sie ein echtes ImGui-Fenster, das Ihren Picker-Bereich abdeckt, und platzieren Sie darin einen ImGui::InvisibleButton. Das Fenster ist das, was der Host per Hit-Test prüft; der InvisibleButton liefert Ihnen ImGui::IsItemClicked() zur Klick-Erkennung. Skizze:

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

// Marker über ImGui::GetForegroundDrawList() (oder GetWindowDrawList()) zeichnen:
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) {
            // Benutzer hat diesen POI ausgewählt
        }
    }
    return true;
});
ImGui::End();

Kleinere Fenster, die nur den Picker-Bereich abdecken, funktionieren genauso – dem Host ist die Größe egal, er prüft nur, ob irgendein Fenster unter dem Cursor liegt.

Anwendungsbeispiel — POI von der Karte hinzufügen

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

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

        // Hotkey-Umschalten. Tastatureingaben erreichen das Plugin unabhängig vom
        // Capture-Zustand, sodass dies auch bei klickdurchlässigem Overlay funktioniert.
        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;
        // ... Picker-UI (siehe Hintergrund-Draw-List-Hinweis oben für das
        //     ImGui::Begin + InvisibleButton-Muster).
    }

    void OnDisable() override {
        // Zur Sicherheit. Der Host löscht Flags auch beim Deaktivieren, aber
        // explizites Aufräumen hält den Zustand konsistent, falls ein synchroner
        // Frame zwischen OnDisable und dem PluginManager-Bookkeeping stattfindet.
        ctx()->Overlay.SetIncludeSleepingEntities(false);
        ctx()->Overlay.SetWantsOverlayInput     (false);
    }
};

Lebenszyklus und Aggregation

  • Beide Flags sind idempotent – ein zweimaliger Aufruf von Set(true) hintereinander ist beim zweiten Mal ein No-op; der Zähler wird nicht doppelt inkrementiert.
  • Beide werden OR-aggregiert mit dem eigenen Zustand des Hosts und den Flags aller anderen Plugins. Mehrere Plugins gleichzeitig im Picker-Modus koexistieren problemlos.
  • Der Host löscht automatisch alle Flags eines Plugins, wenn das Plugin deaktiviert wird (über den Plugins-Tab, Crash-Deaktivierung oder Herunterfahren). Ein Absturz mitten in einem Frame steckt das Overlay nicht dauerhaft im Capture-Modus fest – gut verhaltene Plugins paaren ihre Ein-/Aus-Aufrufe jedoch trotzdem, damit andere Plugins und das Spiel dazwischen reaktionsfähig bleiben.
  • Latenz: Ein Set-Aufruf aktualisiert das Flag synchron, aber die tatsächliche Verhaltensänderung zeigt sich erst im nächsten Host-Frame (Overlay-Eingabe) oder nächsten GameClient-Worker-Tick (schlafende Entities). Unter einem Frame, nicht wahrnehmbar.
  • Alle Methoden sind thread-sicher aus beliebigen Threads aufrufbar.

14. Prices — vom Host geladene Item-Preise

Der Host lädt Marktpreise einmal pro Sitzung von poe2scout in einem Hintergrund-Thread und stellt sie jedem Plugin über ctx()->Prices zur Verfügung. Plugins rufen Preise nicht selbst ab – hinter dem eingebauten Radar, den Host-Overlays und allen Plugins liegt eine einzige gemeinsame Preisdatenbank, sodass die API einmal statt einmal pro Verbraucher abgefragt wird.

  • Die Liga wird vom Benutzer unter Konfiguration → Einstellungen ausgewählt (Standard: Runes of Aldur) und host-seitig gespeichert. Plugins lesen stets die vom Benutzer gewählte Liga; sie wählen sie nicht selbst.
  • Das Laden erfolgt einmalig mit kategoriespezifischem Backoff (Wiederholung nach 1 → 5 → 10 → 20 → 30 → 60 Min. bei Fehler, dann Aufgabe bis zum App-Neustart). Es gibt keine periodische Aktualisierung – Preise sind für die gesamte Sitzung stabil.
  • Jeder Preis ist in Chaos denominiert. GetRates() liefert die Divine/Exalted-Umrechnung, wenn Sie in diesen Einheiten anzeigen möchten.

Einen Preis nachschlagen

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 — welcher poe2scout-Bucket übereinstimmte (currency, fragments, runes,
    // …, oder eine Unique-Item-Kategorie). Hilfreich, um zwischen Währung und Unique zu unterscheiden.
}

LookupPrice nimmt den Anzeigenamen eines Items (Währungsname, Unique-Name oder Basistyp) und führt eine unscharfe Host-seitige Suche über alle geladenen Kategorien durch. Bei keiner Übereinstimmung wird found == false zurückgegeben und alle Preise sind null.

PriceResult-Feld Typ Bedeutung
found bool Ein Preis wurde gefunden
chaos float Preis in Chaos Orbs (die kanonische Einheit)
divine float Derselbe Preis in Divine Orbs ausgedrückt
exalt float Derselbe Preis in Exalted Orbs ausgedrückt
category std::string Übereinstimmende poe2scout-Kategorie (currency / fragments / runes / … / eine Unique-Kategorie)

Umrechnungskurse und Ladestatus

PluginSDK::PriceRates  r = ctx()->Prices.GetRates();   // divineInChaos, exaltedInChaos
PluginSDK::PriceStatus s = ctx()->Prices.GetStatus();
if (!s.loaded) {
    // Noch nicht bereit (wird noch geladen oder alle Kategorien fehlgeschlagen).
    // s.catsOk / s.catsPending / s.catsFailed zeigen den Fortschritt des Laders.
}

GetStatus().loaded ist der zu prüfende Schalter, bevor Preise angezeigt werden – er wechselt auf true erst, wenn die Umrechnungskurse und mindestens die erste Kategorie eingetroffen sind. Zeigen Sie bis dahin einen „Preise werden geladen…"-Zustand anstelle von Nullen.


15. Runeshape-Geräte

ctx()->Runeshape stellt die Expedition2Encounter-Geräte ("Runeshape") bereit, die der Host im aktuellen Gebiet aufgelöst hat, zusammen mit der Belohnung, die jedes Rezept gewähren würde. Der Host führt den Geräteketten-Durchlauf und den Offline-Rezept-Abgleich durch; Ihr Plugin liest lediglich das Ergebnis. Dies ist dasselbe, was das integrierte Radar-Belohnungs-Tag und das Runeshape-Fenster von NinjaPricer antreibt — ein Drittanbieter-Plugin kann dieselben Daten rendern.

for (const PluginSDK::Runeshape& rs : ctx()->Runeshape.Runeshapes()) {
    // rs.color weist jedem Gerät eine eindeutige Farbe zu (zum Gruppieren/Einfärben verwenden).
    // rs.bestIndex ist der Index der Belohnung mit dem höchsten Preis (oder -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(n), die zum nächsten Überrest weitergegeben werden
            ctx()->Log.Info(("  propagates: " + rw.propagatingRunes).c_str());
    }
}
Runeshape-Feld Typ Bedeutung
entityId uint64_t Entity-ID des Geräts — an Rewards() übergeben
color uint32_t Gepacktes RGBA, stabil pro Gerät (zum Gruppieren/Einfärben)
isUnique bool Gerät bietet ein Unique-Item-Rezept an
holeCount int Anzahl der Runen-Schlitze am Anker
anchorName std::string Name der Anker-Rune
rewardCount int Anzahl der Belohnungs-Slots
bestIndex int Index der Belohnung mit dem höchsten totalChaos, oder -1
propagatingSlots std::vector<int> Slot-Index(e) der Runen-Schlitze, deren Rune zum nächsten Überrest weitergegeben wird (0.5.4 Carryover); üblicherweise 1, manchmal 2
RuneshapeReward-Feld Typ Bedeutung
name std::string Name des Belohnungs-Items
count int Gewährte Menge
unitChaos float Chaos-Preis pro Einheit (aus dem Prices-Service)
totalChaos float unitChaos × count
priced bool Ein Preis wurde für diese Belohnung gefunden
propagatingRunes std::string Rune(n) an den weiterzugebenden Slot(s) dieses Rezepts — was übertragen wird, wenn Sie dieses Rezept abschließen; z. B. "Power" oder "Cold, Time"; leer, wenn das Rezept den Slot nicht abdeckt
propagatingCount int Anzahl der weiterzugebenden Runen für diese Belohnung
propagatingHasRare bool Mindestens eine weiterzugebende Rune ist selten ("lila"/wertvoll)

Belohnungspreise stammen aus derselben ctx()->Prices-Datenbank, daher bedeutet eine nicht bepreiste Belohnung (priced == false) in der Regel, dass die Preise noch nicht geladen wurden oder das Item nicht auf poe2scout gelistet ist.

Runen-Weitergabe (0.5.4). Jeder Überrest wählt zufällig einen Runen-Slot, dessen Rune zum nächsten Überrest weitergegeben wird (im Spiel: die goldfarbene Hervorhebung in der Runeshape Recipes-Liste). Runeshape::propagatingSlots ist die rohe Slot-Liste; da es sich um eine Slot-Position handelt, unterscheidet sich die weiterzugebende Rune je nach Rezept, sodass RuneshapeReward::propagatingRunes dies pro Belohnung auflöst. Dies treibt den gelben Slot-Punkt und den Pro-Belohnungs-Marker von NinjaPricer an.


16. Einstellungen persistieren

Konvention: <plugin directory>/config/settings.json. Directory() liefert den absoluten UTF-8-Pfad zu Ihrem Plugin-Ordner.

Für triviale Einstellungen funktioniert ein selbstgeschriebener JSON-Writer einwandfrei und hält die DLL selbstgenügsam. Siehe Plugins/Radar/src/RadarSettings.h für ein funktionierendes Beispiel. Das Grundgerüst:

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

Für strukturierte Daten (verschachtelte Objekte, Arrays) binden Sie eine echte JSON-Bibliothek in Ihrem Plugin-Ordner ein. Der Host gibt keine Wahl vor.

SaveSettings wird periodisch (~5 s) und beim Deaktivieren aufgerufen; Sie müssen es nicht selbst aufrufen.


17. Logging

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

Alle vier Ebenen werden in den zentralen Logger des Hosts geleitet. Meldungen erscheinen im Logs-Tab des Hosts und in der Logdatei auf der Festplatte. Formatieren Sie selbst vor dem Aufruf; der Host akzeptiert keine printf-artigen Variadic-Argumente.

Intern emittieren die Komfortmethoden die Strings "Debug", "Info", "Warning" und "Error" (Warn wird auf "Warning" abgebildet). Die Host-Bridge führt einen Vergleich ohne Berücksichtigung der Groß-/Kleinschreibung durch, sodass ein Plugin, das Log("warn", "msg") aufruft, ebenfalls korrekt geleitet wird – die Komfortmethoden sind aber klarer.


18. Memory (Power-User)

Direkte Speicher-Primitive. Bevorzugen Sie die High-Level-Services, wo immer möglich – sie kennen Offsets, behandeln ABI-Änderungen und sind SEH-sicher. Direkte Memory-Reads sind nur dann angemessen, wenn es keinen höheren Aufruf für das gibt, was Sie benötigen.

// 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

Wenn Sie sich öfter dabei ertappen, dass Sie zu diesen Aufrufen greifen, fragen Sie sich, ob die Daten, die Sie brauchen, eigentlich in die höheren Services gehören.


19. Bridge / SEH-Sicherheit

Jeder DLL-übergreifende Aufruf zwischen Host und Plugin läuft auf der Host-Seite innerhalb eines __try / __except-Blocks. Ein sich fehlverhaltendes Plugin, das einen veralteten Zeiger dereferenziert, durch Null teilt oder anderweitig innerhalb eines SDK-Aufrufs faultet, bekommt einen geloggten Fehler – der Host-Prozess stürzt nicht ab, das Spiel läuft weiter und der Benutzer kann andere Plugins weiter benutzen.

Das heißt nicht, dass Plugins schludrig sein dürfen. SEH fängt das Symptom, nicht die Ursache. Wenn Ihr Plugin in jedem Frame Faults wirft, sieht der Benutzer eine Flut von Fehlerlogs und Ihre Daten sind faktisch unverfügbar. Behandeln Sie Null-Rückgaben aus SDK-Aufrufen, prüfen Sie Valid-Flags auf Komponentendaten und dereferenzieren Sie uintptr_t-Adressen nicht direkt – führen Sie sie durch die ComponentsService / Ui / Memory-Aufrufe, die RPM bereits korrekt einwickeln.

Der Host kommt mit einem fehlerhaften Plugin zurecht. Mit einer hängenden Plugin-DLL nicht – ein DrawSettings, das 100 ms braucht, blockiert den gesamten UI-Thread. Halten Sie die Arbeit pro Frame günstig.


20. Häufige Fallstricke

Eine kurze Liste mit Dingen, auf die Plugin-Autoren bei der ersten Integration stoßen. Das meiste davon ist oben inline dokumentiert; hier als Checkliste gesammelt.

  1. OnAreaChange feuert, bevor das begehbare Grid neu geparst ist. Aktualisieren Sie WalkableGridHandle nicht aus dem Event – pollen Sie pro Frame in DrawUI und tauschen Sie, wenn sich Data() ändert. (§11)
  2. Entity::Zone ist für den lokalen Spieler immer None. Es ist eine Entfernungsklassifikation relativ zum Spieler, also liegt der Spieler per Definition bei Entfernung null. Zeigen Sie es nicht in Spieler-Info-Anzeigen.
  3. Components.ReadMods() liefert nur zusammenfassende Flags – keine Mod-Listen. Für Mod-Listen pro Art rufen Sie Inventory.ReadItemMods(entityAddr) auf. (§8)
  4. Auf den Boden gefallene Items können kein EntitySubtype haben. Wenn Sie nach Items in der Welt filtern, bevorzugen Sie EntityType == Item || EntityType == Chest gegenüber einer engeren Subtype-Prüfung.
  5. Directory() liefert einen absoluten Pfad. Stellen Sie das EXE-Verzeichnis nicht selbst voran – Sie bekommen EXEDIR\EXEDIR\Plugins\X und Ihre Konfigurations-Writes landen außerhalb des Plugin-Ordners.
  6. ctx() liefert const Context*. Mutierende Methoden wie EventsService::Subscribe erfordern einen const_cast. Das ist Absicht – Services, die nicht mutieren, sollen sich auch nicht versehentlich mutieren lassen.
  7. ImGui::SetCurrentContext ist pro DLL. Rufen Sie es an jedem Einstiegspunkt auf, der zeichnet (OnEnable, DrawUI, DrawSettings), weil die Plugin-DLL standardmäßig ihren eigenen ImGui-Zustand besitzt.
  8. Die Komfort-Helfer lesen die Komponente bei jedem Aufruf neu. GetHealthPercent(addr) führt intern ein frisches ReadLife(addr) aus. Wenn Sie das Life-Struct aus einem früheren Aufruf bereits halten, greifen Sie stattdessen direkt auf dessen Felder zu.

21. Service-Schnellreferenz

Einzeilige Zusammenfassung jeder öffentlichen Methode jedes Services. Verwenden Sie die Fließtext-Abschnitte oben für die vollständigen Typsignaturen und Verwendungshinweise.

GameService

Methode Liefert Zweck
GetSnapshot() Snapshot Vollständige Sicht pro Frame, einschließlich Entities
GetState() GameState Enum: InGame, Login, Loading, …
IsAttached() bool Spielprozess angebunden
IsInGame() bool State == InGame
IsForeground() bool Spielfenster hat den Fokus
IsMenuVisible() bool ESC-Menü / Einstellungen offen
IsOverlayMode() bool Host ist im Overlay (klickbar durch)
GetProcessId() DWORD PID des Spiels
GetGameWindow() HWND Handle des Spielfensters
GetScreenSize() ScreenSize {Width, Height} als Floats

EntitiesService

Methode Liefert Zweck
Enumerate(cb) Jede nahe Entity besuchen (false zum Stoppen zurückgeben)
GetPlayer() Entity Die lokale Spieler-Entity
FindById(id) std::optional<Entity> Lookup per Entity-ID
GetWorldItemInner(addr) std::optional<Entity> Innere Item-Entity für einen WorldItem-Container (Bodengegenstände)
Watch(id) Eine Entity anheften, damit ihre Komponenten lesbar bleiben
Unwatch(id) Einen Watch freigeben
IsWatched(id) bool Watch-Zustand
GetWatchedComponents(id) std::optional<ComponentAddresses> Angeheftete Komponenten lesen

ComponentsService

Methode Liefert Zweck
ReadLife / ReadRender / ReadPositioned / ReadTargetable / ReadChest / ReadShrine / ReadStack / ReadCharges / ReadPlayer / ReadAnimated / ReadTransitionable / ReadTriggerableBlockage / ReadMinimapIcon / ReadStateMachine / ReadBase / ReadMods / ReadStats / ReadBuffs / ReadActor / ReadNpc / ReadDiesAfterTime Komponenten-Struct 21 Reader, einer pro Komponententyp
EnumerateBuffs(addr) std::vector<Buff> Aktive Buffs auf der Entity
EnumerateActiveSkills(addr) std::vector<ActiveSkill> Skills aus einer Actor-Komponente
EnumerateStats(addr) std::vector<StatEntry> Aus Items + Buffs bezogene Stats
EnumerateItemMods(addr) std::vector<Mod> Über eine Mods-Komponente erreichbare Mods
ReadGroundEffect(entityAddr) GroundEffect Bodeneffekt-Typ + Radius aus einer VisibleServerGroundEffect-Entity — ENTITY-Adresse übergeben; auf TypeId abgleichen (ShockedGround/IgnitedGround/…). Unterscheidet Effekte, die sich einen Entity-Pfad teilen
GetHealthPercent / GetEsPercent / GetManaPercent float Komfort-Prozent-Helfer
IsAlive(addr) bool Gesundheit > 0
GetItemRarity(addr) int Seltenheit aus einer Mods-Komponente
IsItemIdentified(addr) bool Identifiziert-Flag
GetStackCount(addr) int Aktueller Stapelzähler
IsChestOpened(addr) bool Truhen-Offen-Flag
GetPlayerName(addr) std::string Spielername aus einer Player-Komponente
GetWorldPosition(renderAddr, x, y, z) bool Komfortzugriff auf Weltkoordinaten

InventoryService

Methode Liefert Zweck
Scan(inventoryId) Host-seitigen Rescan auslösen (-1 = alle)
Get(inventoryId) Inventory Ein Inventar, Items bereits gefüllt
GetItems(inventoryId) std::vector<InventoryItem> Nur Items
GetAll() std::vector<Inventory> Alle gescannten Inventare
GetName(inventoryId) const char* Anzeigename ("Backpack", "Stash", …)
ReadItemRarity(addr) int Seltenheit pro Entity (löst WorldItem-Container automatisch auf)
ReadItemStackCount(addr) int Stapel pro Entity (löst WorldItem-Container automatisch auf)
ReadItemBaseTypeName(addr) std::string Basistyp, löst WorldItem-Container automatisch auf
ReadItemUniqueName(addr) std::string Unique-Name, löst WorldItem-Container automatisch auf
ReadItemPath(addr) std::string Metadata/Items/...-Pfad, löst WorldItem-Container automatisch auf
ReadItemMods(addr) ItemMods Zusammenfassungs-Flags + 5 Mod-Vektoren pro Art, löst WorldItem-Container automatisch auf
FormatStat(statKey, v0, v1) std::string Mod-Text im Spielstil für einen Stat-Schlüssel + Wert(e) via den Host-.csd-Formatierer; leer, bis die Beschreibungen geladen sind
ReadItemBaseStats(addr) ItemBaseStats Defensive Basiswerte (berechnetes Energy Shield; Basis-Ward/Armour/Evasion); Valid false ohne Armour-Komponente; löst WorldItem-Container automatisch auf
ReadItemAggregatedStats(addr) std::vector<std::pair<int,int>> Aggregierte {statId, value}-Paare (Wegstein-Item-Seltenheit 8205 / Packungsgröße 8206 / Monster-Seltenheit 8207 / Monster-Effektivität 8208 / Wegstein-Dropp-Chance 8209); löst WorldItem-Container automatisch auf

UiService

Methode Liefert Zweck
Read(addr) UiElement Element-Felder (Rechteck, Flags, Kinderanzahl)
GetChildren(addr) std::vector<uintptr_t> Adressen der Kindelemente
GetChildAt(addr, index) uintptr_t Einzelnes Kind per Index
FollowPath(root, indices, count) uintptr_t Einen bekannten Indexpfad durchlaufen
IsVisible(addr) bool Element ist auf dem Bildschirm
GetStringId(addr) std::string Stabiler spielseitiger Identifikator
GetText(addr) std::string Gerenderter Text
ComputeScreenRect(addr, x, y, w, h) bool Finales Rechteck im Bildschirmraum
GetGameUiRoot() uintptr_t Wurzel der In-Game-UI
GetUiRoot() uintptr_t Oberste UI-Wurzel
GetCullValue() int UI-Cull-Schwellwert des Hosts
FindPanelByStringId(parent, stringId) uintptr_t Gezieltes Nachfahren-Lookup

RenderService

Methode Liefert Zweck
WorldToScreen(wx, wy, wz, sx, sy) bool Perspektivische Projektion
GridToLargeMap(gx, gy, worldZ, sx, sy) bool Projektion auf das Large-Map-Overlay
GridToMiniMap(gx, gy, worldZ, sx, sy) bool Projektion auf die Minimap
GetLargeMapTransform() MapTransform Vorberechnete Transformation für gebündelte Mathematik
GetMiniMapTransform() MapTransform Dasselbe, für die Minimap

TerrainService

Methode Liefert Zweck
GetWalkableGrid() WalkableGridHandle RAII-Handle auf die 4-Bit-pro-Kachel-Begehbarkeits-Bitmap
GetHeightGrid() HeightGridHandle RAII-Handle auf die Terrain-Höhen pro Kachel
IsWalkable(gx, gy) bool Einzelkachel-Prädikat
GetTerrainHeight(gx, gy) float Z-Wert im Weltraum
GetWorldToGridConvertor() float Umrechnungsfaktor Welt → Grid
EnumerateTgtLocations(cb) Jede TGT-Instanz im aktuellen Gebiet besuchen

MemoryService

Methode Liefert Zweck
Read(addr, buf, size) bool Roher RPM
ReadString(addr) std::string Nullterminierter schmaler String
ReadWString(addr) std::wstring Nullterminierter Wide-String
ReadStdWString(addr) std::wstring Liest einen spielseitigen std::wstring-Container (mit SSO-Unterstützung)
ReadStdVector(addr, elemSize, maxElems) std::vector<uint8_t> Rohbytes; reinterpretieren als Ihr Typ
GetBaseAddress() uintptr_t Basis des Spielmoduls
GetModuleSize() uintptr_t Größe des Spielmoduls
GetPatternAddress(name) uintptr_t Lookup eines benannten Patterns

LogService

Methode Zweck
Debug / Info / Warn / Error(msg) Auf der entsprechenden Ebene emittieren
Log(level, msg) Beliebige Level-Zeichenkette

EventsService

Methode Liefert Zweck
Subscribe(kind, cb) Token Generischer Dispatch
OnAreaChange / OnFrame / OnGameAttached / OnGameDetached(cb) Token Einzeilen-Abonnement-Helfer
Unsubscribe(token) Manuelle Freigabe (Destruktor gibt sowieso automatisch frei)

OverlayService

Methode Liefert Zweck
SetIncludeSleepingEntities(enable) Opt-in, um EntityState::Useless-Entities in EntitiesService.Enumerate zu empfangen
SetWantsOverlayInput(enable) Overlay auffordern, Mausklicks abzufangen, anstatt klick-transparent zu sein

Beide sind idempotent, plugin-spezifisch, OR-aggregiert mit Host + anderen Plugins, werden bei Deaktivierung/Entladen automatisch zurückgesetzt. Siehe Abschnitt 13 für das vollständige Muster (einschließlich des Hintergrund-Draw-List-Vorbehalts für Map-Picker-Plugins).

FlasksService

Methode Liefert Zweck
GetFlask(slot) std::optional<Flask> Lebens-/Mana-Kolben nach Gürtel-Slot (0=Lebenskolben, 1=Manakolben); nullopt außerhalb des Bereichs oder nicht im Spiel
GetCharm(slot) std::optional<Charm> Talisman nach Gürtel-Slot (0..2); nullopt außerhalb des Bereichs oder nicht im Spiel
AllFlasks() std::vector<Flask> Alle Kolben-Slots inkl. leerer (FlaskSlotCount() Einträge)
AllCharms() std::vector<Charm> Alle Talisman-Slots inkl. leerer (CharmSlotCount() Einträge)
FlaskSlotCount() int32_t Anzahl der Kolben-Slots (2 in POE2)
CharmSlotCount() int32_t Anzahl der Talisman-Slots (3 in POE2)

Siehe Abschnitt 8 („Kolben & Talismane") für die Flask- / Charm-Feldtabellen und die Einschränkung von PerUseEffective.

PricesService

Methode Liefert Zweck
LookupPrice(name) PriceResult Unscharfe host-seitige Preisabfrage nach Anzeigename (found, chaos, divine, exalt, category)
GetRates() PriceRates Divine / Exalted → Chaos-Umrechnungskurse
GetStatus() PriceStatus loaded-Tor + Zähler pro Kategorie (catsOk / catsPending / catsFailed)

RuneshapeService

Methode Liefert Zweck
Runeshapes() std::vector<Runeshape> Alle aufgelösten Expedition2Encounter-Geräte (id, Farbe, Anker, bestIndex)
Rewards(entityId) std::vector<RuneshapeReward> Belohnungsslots pro Gerät, jeweils über den Prices-Service bewertet

22. Versionierung

PluginAbi.h definiert:

constexpr int PLUGIN_SDK_VERSION = 6;

Beim Laden ruft der Host plugin->GetSDKVersion() auf und vergleicht es mit seinem eigenen PLUGIN_SDK_VERSION. Bei einer Diskrepanz → der Host loggt eine Warnung und lehnt das Laden des Plugins ab.

Der Host prüft außerdem HostAbi::version und HostAbi::size_bytes innerhalb von PluginSDK_AttachHost (durch PluginSDK.h inline definiert, wenn PLUGIN_EXPORTS gesetzt ist). Stimmt eines der Felder nicht mit dem überein, gegen das das Plugin gebaut wurde, ist ctx() funktionslos. Der Basisklassen-Accessor HostCompatible() meldet in diesem Fall false, und jedes Plugin, das sich höflich verhalten will, sollte den Dienst verweigern:

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

23. Beispiel-Plugins

Vier Plugins im Repository sind dafür gedacht, als Dokumentation gelesen zu werden:

  • Plugins/ExamplePlugin/ – Breitenschau über die gesamte Oberfläche. Ein Plugin, das nahezu jeden Service berührt, organisiert in 11 examples/Example*.h-Unterdateien (Area & Vitals, Buffs, Entities, Inventory, Memory, UI Explorer, Component Reader, Render, Terrain, Events, Log) plus ein Abdeckungs-Übersichts-Banner. Lesen Sie dies, wenn Sie sehen möchten, wie ein Service in seinem Kontext verwendet wird.

  • Plugins/Radar/ – fokussiertes Praxisbeispiel. ~200 Zeilen. Ein Radar-Overlay, das vollständig auf dem öffentlichen SDK aufbaut – keine Offsets, keine rohen Memory-Reads. Zeichnet die begehbare Karte sowie Punkte pro Entity über Render.GridToLargeMap. Lesen Sie dies, wenn Sie den minimalen Code für ein bestimmtes Ergebnis sehen möchten.

  • Plugins/KillCount/ – Tracker für Kills, Truhen und Tode. SQLite + Sprite-Atlas + Zustand pro Gebiet. Zeigt, wie man Persistenz, mitgelieferte Datendateien und ein Overlay in einer einzigen DLL ausliefert.

  • Plugins/NinjaPricer/ – poe.ninja-Preis-Overlay. HTTP-Abruf (Exchange API) + Inventar-Scan + Preisermittlung pro Item. Zeigt Netzwerkcode, Ingestion von Drittanbieter-Daten und Inventar-Iteration in einem realen Arbeitsablauf.


← Home

Clone this wiki locally