-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide DE
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.
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 ausPluginAbi.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/.
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.
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; gibtPluginSDK::Plugin*zurück. -
DestroyPlugin– Destruktor; nimmtPluginSDK::Plugin*. -
PluginSDK_AttachHost– verdrahtet denContext. Wird für Sie innerhalb vonPluginSDK.hdefiniert und automatisch emittiert, wennPLUGIN_EXPORTSgesetzt 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));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.
ctx() liefert const PluginSDK::Context*, ein Aggregat aus 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
};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}
|
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.
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 transitionWas 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ändigesEntity),Entities(vollständigerstd::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();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.
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) |
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.
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.
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);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.
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 thresholdStringId-Werte sind stabile spielseitige Identifikatoren; bevorzugen Sie sie gegenüber fest codierten Pfaden, wann immer es welche gibt.
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.
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
});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.
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.
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.
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.
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.
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 patternWenn 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.
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.
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.
-
OnAreaChangefeuert, bevor das begehbare Grid neu geparst ist. Aktualisieren SieWalkableGridHandlenicht aus dem Event – pollen Sie pro Frame inDrawUIund tauschen Sie, wenn sichData()ändert. (§11) -
Entity::Zoneist für den lokalen Spieler immerNone. Es ist eine Entfernungsklassifikation relativ zum Spieler, also liegt der Spieler per Definition bei Entfernung null. Zeigen Sie es nicht in Spieler-Info-Anzeigen. -
Components.ReadMods()liefert nur zusammenfassende Flags – keine Mod-Listen. Für Mod-Listen pro Art rufen SieInventory.ReadItemMods(entityAddr)auf. (§8) -
Auf den Boden gefallene Items können kein
EntitySubtypehaben. Wenn Sie nach Items in der Welt filtern, bevorzugen SieEntityType == Item || EntityType == Chestgegenüber einer engeren Subtype-Prüfung. -
Directory()liefert einen absoluten Pfad. Stellen Sie das EXE-Verzeichnis nicht selbst voran – Sie bekommenEXEDIR\EXEDIR\Plugins\Xund Ihre Konfigurations-Writes landen außerhalb des Plugin-Ordners. -
ctx()liefertconst Context*. Mutierende Methoden wieEventsService::Subscribeerfordern einenconst_cast. Das ist Absicht – Services, die nicht mutieren, sollen sich auch nicht versehentlich mutieren lassen. -
ImGui::SetCurrentContextist 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. -
Die Komfort-Helfer lesen die Komponente bei jedem Aufruf neu.
GetHealthPercent(addr)führt intern ein frischesReadLife(addr)aus. Wenn Sie dasLife-Struct aus einem früheren Aufruf bereits halten, greifen Sie stattdessen direkt auf dessen Felder zu.
Einzeilige Zusammenfassung jeder öffentlichen Methode jedes Services. Verwenden Sie die Fließtext-Abschnitte oben für die vollständigen Typsignaturen und Verwendungshinweise.
| 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 |
| 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 |
| 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 |
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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Methode | Zweck |
|---|---|
Debug / Info / Warn / Error(msg) |
Auf der entsprechenden Ebene emittieren |
Log(level, msg) |
Beliebige Level-Zeichenkette |
| 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) |
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;
}
// ...
}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 11examples/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 überRender.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.