-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Guide DE
- MSVC v143 (Visual Studio 2022)
-
C++20 (
/std:c++20) - x64 Release-Build
-
Laufzeitbibliothek:
/MD(Multi-threaded DLL) — muss mit dem Host übereinstimmen
- Erstellen Sie ein neues C++ DLL-Projekt in Visual Studio
- Setzen Sie den Include-Pfad auf das POEFixer-Quellverzeichnis (für SDK- und ImGui-Header)
- Fügen Sie die ImGui-Quelldateien zu Ihrem Projekt hinzu:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Binden Sie die Plugin-SDK-Header in Ihren Plugin-Quellcode ein:
Oder verwenden Sie den Komfort-Header aus dem ExamplePlugin:
#include "plugin_sdk/PluginAPI.h" #include "plugin_sdk/PluginContext.h" #include "imgui/imgui.h"
#include "sdk/PluginHelpers.h" // Includes all SDK headers + MemoryReader + utilities
- Definieren Sie
PLUGIN_EXPORTSund_CRT_SECURE_NO_WARNINGSin den Präprozessordefinitionen Ihres Projekts
Plugins/
YourPlugin/
YourPlugin.dll <-- DLL-Name MUSS mit dem Ordnernamen übereinstimmen
config/
settings.txt <-- Optionale Einstellungsdatei
data/
... <-- Optionales Datenverzeichnis (Datenbanken, Caches usw.)
Das ExamplePlugin demonstriert die empfohlene Projektstruktur:
Plugins/ExamplePlugin/
ExamplePlugin.cpp <-- Haupt-Einstiegspunkt des Plugins + Factory-Exporte
sdk/
PluginHelpers.h <-- MemoryReader, WideToNarrow, Entity/Seltenheits-Hilfsfunktionen
examples/
ExampleBuffs.h <-- Buff-Liste mit Filterung und Fortschrittsbalken
ExampleEntities.h <-- Debug-Entity-Liste mit Überwachungsmechanismus, Komponentenbäumen, JSON-Dump
ExampleInventory.h <-- ServerData, Inventarauswahl, Slot-Raster, Item-Mods mit Seltenheit
ExampleMemory.h <-- Hex-Viewer, Read<T>-Demo, Pattern-Scanner
ExampleUiExplorer.h <-- Vollständiger UI-Element-Explorer mit Suche, Navigation, Hervorhebung
Ein umfangreicheres Plugin-Beispiel mit SQLite3, Icon-Atlas und Overlay-Rendering:
Plugins/KillCount/
KillCount.cpp <-- Haupt-Einstiegspunkt, IPlugin-Lebenszyklus, Einstellungs-UI
KillCount.h <-- Plugin-Klassendeklaration
KillTracker.cpp/h <-- Kill/Truhen/Tod-Zählwerk
OverlayRenderer.cpp/h <-- ImGui-Overlay mit Drag-to-Reposition-Muster
IconAtlas.cpp/h <-- Spritesheet-Texturladung (D3D11 + stb_image)
Database.cpp/h <-- SQLite3-Wrapper für persistente Statistiken
DisplaySettings.h <-- Einstellungsstruktur
sdk/
PluginHelpers.h <-- Kopiert aus ExamplePlugin
lib/
sqlite3.c/h <-- SQLite3-Amalgamation (als C kompiliert)
sqlite3-vcpkg-config.h <-- Lokale Überschreibung für statisches Linken
Der DLL-Dateiname muss exakt mit dem Ordnernamen übereinstimmen:
- Ordner:
Plugins/MyPlugin/→ DLL:MyPlugin.dll - Der Host durchsucht jeden Unterordner in
Plugins/und sucht nach<FolderName>.dll
Load DLL (LoadLibrary)
→ CreatePlugin() -- Factory: IPlugin instanziieren
→ SetContext(ctx) -- Host-Dienste empfangen
→ SetPluginDirectory(dir) -- Ordnerpfad empfangen
→ GetSDKVersion() -- Kompatibilitätsprüfung
→ GetName() -- Anzeigename für UI
→ [if enabled] OnEnable() -- Ressourcen initialisieren
↓
Hauptschleife (jeden Frame):
→ DrawUI() -- Overlay rendern (nur wenn aktiviert)
→ DrawSettings() -- Einstellungen im Plugins-Tab rendern
→ WantsOverlay() -- Host prüft, ob Plugin Overlay-Modus wünscht
↓
Periodisch / beim Beenden:
→ SaveSettings() -- Einstellungen speichern
↓
→ OnDisable() -- Ressourcen freigeben
→ DestroyPlugin(plugin) -- Factory: IPlugin löschen
→ FreeLibrary -- DLL entladen
- Alle
Draw*-Methoden werden im Haupt-/Render-Thread aufgerufen -
GetSnapshot()und andere PluginContext-Funktionen sind thread-sicher - Erstellen Sie KEINE Threads, die ImGui aufrufen — ImGui ist nicht thread-sicher
Jedes Plugin muss die IPlugin-Schnittstelle implementieren (definiert in plugin_sdk/PluginAPI.h):
- Wann aufgerufen: Einmal, direkt nach der Erstellung
-
Parameter: Relativer Pfad wie
"Plugins/YourPlugin" - Zweck: Speichern Sie diesen Pfad zum Laden von Einstellungen/Ressourcen
-
Wann aufgerufen: Einmal, nach
SetPluginDirectory -
Parameter: Zeiger auf den
PluginContextdes Hosts (gültig für die gesamte Lebensdauer des Plugins) - Zweck: Speichern Sie diesen Zeiger — er ist Ihr Zugang zu allen Spieldaten
-
Wichtig: Rufen Sie
ImGui::SetCurrentContext(ctx->ImGuiContext)hier auf
- Wann aufgerufen: Wenn der Benutzer das Plugin aktiviert oder beim Start, wenn es zuvor aktiviert war
-
Parameter:
true, wenn der Spielprozess aktuell angebunden ist - Zweck: Einstellungen laden, Ressourcen zuweisen, Zustand initialisieren
- Wann aufgerufen: Wenn der Benutzer das Plugin deaktiviert
- Zweck: Ressourcen freigeben, Hintergrundarbeit stoppen
- Wann aufgerufen: Jeden Frame, nur wenn das Plugin aktiviert ist
- Zweck: Overlay mit ImGui rendern
-
Hinweis: Verwenden Sie eindeutige Fenster-IDs wie
"MyWindow##MyPlugin", um Konflikte zu vermeiden
- Wann aufgerufen: Jeden Frame, im Plugins-Einstellungstab (nur wenn aktiviert)
- Zweck: Plugin-Konfiguration mit ImGui rendern
- Wann aufgerufen: Periodisch und beim Beenden der Anwendung
-
Zweck: Einstellungen auf Festplatte speichern (z.B.
Plugins/YourPlugin/config/settings.txt)
-
Gibt zurück: Anzeigename im Plugins-Tab (z.B.
"My Plugin")
-
Gibt zurück:
PLUGIN_SDK_VERSION(aktuell 4) - Zweck: Host prüft dies auf Kompatibilität — muss übereinstimmen
-
Gibt zurück:
true, wenn das Plugin im Overlay-Modus rendern möchte (transparentes Overlay über dem Spiel) -
Standard:
false— Plugin rendert nur im normalen Einstellungsfenster -
Zweck: Wenn ein Plugin
truezurückgibt, wechselt der Host in den Overlay-Modus, auch wenn keine eingebauten Funktionen dies erfordern
Ihre DLL muss diese zwei C-Funktionen exportieren:
extern "C" PLUGIN_API IPlugin* CreatePlugin() {
return new MyPlugin();
}
extern "C" PLUGIN_API void DestroyPlugin(IPlugin* plugin) {
delete plugin;
}Die PluginContext-Struktur (definiert in plugin_sdk/PluginContext.h) stellt Funktionszeiger für den Zugriff auf Spieldaten bereit. Alle Typen befinden sich im Namespace PluginSDK.
Gibt einen vollständigen Schnappschuss des Spielzustands zurück. Wird einmal pro Frame aktualisiert. Enthält:
| Feld | Typ | Beschreibung |
|---|---|---|
CurrentState |
GameStateTypes |
Aktueller Spielzustand |
CurrentAreaName |
string |
Gebietsname (z.B. "The Riverways") |
CurrentAreaHash |
string |
Eindeutiger Gebietsinstanz-Hash |
CurrentAreaLevel |
uint8_t |
Monsterlevel des aktuellen Gebiets |
IsTown |
bool |
True, wenn in der Stadt |
IsHideout |
bool |
True, wenn im Versteck |
IsPaused |
bool |
True, wenn das Spiel pausiert ist |
IsSkillTreeVisible |
bool |
True, wenn das Skilltree-Panel geöffnet ist |
WorldToGridConvertor |
float |
Umrechnungsfaktor für Welt→Raster |
Player |
RadarEntity |
Daten der lokalen Spieler-Entity |
Entities |
vector<RadarEntity> |
Alle nahegelegenen Entitäten |
LargeMap / MiniMap
|
MapData |
Karten-Overlay-Daten |
Vitals |
PlayerVitals |
Spieler-HP/ES/MP + Buffs |
ScreenWidth / ScreenHeight
|
int |
Spielfenster-Dimensionen |
ProcessId |
DWORD |
Spielprozess-ID |
GameWindow |
HWND |
Spielfenster-Handle |
GameWindowForeground |
bool |
True, wenn Spielfenster im Vordergrund |
IsAttached |
bool |
True, wenn an Spielprozess angebunden |
IsWindowValid |
bool |
True, wenn Spielfenster gültig |
LastUpdateTime |
uint64_t |
Zeitstempel der letzten Datenaktualisierung |
AreaChangeCounter |
uint64_t |
Erhöht sich bei Gebietswechsel |
Inventories |
vector<InventoryInfo> |
Spieler-Inventarinhalt |
CurrencyTotals |
map<string,int> |
Währungszählung nach Pfad |
InventoryGrid |
InventoryGridInfo |
Inventar-UI-Rasterinformation |
WorldToScreenMatrix |
XMFLOAT4X4 |
3D→2D-Projektionsmatrix |
Wichtig: Entity-Filterung Tote Entitäten (mit
EntityState == Useless) werden aus dem Schnappschuss gefiltert, bevor Plugins ihn erhalten. Das bedeutet, dass Sie niemals einen HP-Übergang von lebendig zu tot beobachten werden. Wenn Sie Kills erkennen müssen, verwenden Sie stattdessen verschwindungsbasierte Erkennung — verfolgen Sie Entity-IDs nach Zone und zählen Sie sie als getötet, wenn sie aus der Entity-Liste verschwinden, während sie sich inInnerCircle- oderOuterCircle-Nähe befinden. Siehe Abschnitt 8: Häufige Rezepte für Details.
Komfortfunktion für Spieler-Vitalwerte.
Gibt das aktuelle Spielzustands-Enum zurück.
True, wenn der Spielprozess angebunden und lesbar ist.
True, wenn aktuell im Spiel (nicht beim Laden, nicht auf dem Login-Bildschirm).
True, wenn das Spielfenster das Vordergrundfenster ist.
Gibt die Spielprozess-ID zurück.
Liest alle Modifikatoren eines Items.
Gibt zurück: 0=Normal, 1=Magisch, 2=Selten, 3=Einzigartig
Gibt die Stapelanzahl für Währungs-/stapelbare Items zurück.
Gibt den Basistypnamen des Items zurück.
Gibt den Metadaten-Pfad des Items zurück.
Gibt den Basistypnamen des Items zurück (z.B. "Divine Orb", "Chaos Orb"). Im Gegensatz zu ReadItemName, das den Metadaten-Pfad zurückgibt, liest diese Funktion den tatsächlichen Basistypnamen aus BaseItemTypeData.BaseTypeName.
Gibt den einzigartigen Itemnamen aus Words.dat zurück (z.B. "Headhunter", "Brimstone Call"). Gibt einen leeren String für nicht-einzigartige Items zurück.
Gibt true zurück, wenn der Host sich aktuell im Overlay-Modus befindet (transparentes Overlay über dem Spielfenster). Verwenden Sie dies, um Ihr Rendering anzupassen — z.B. auf dem Spiel-Overlay oder in einem Einstellungsfenster zeichnen.
Gibt true zurück, wenn das Einstellungsmenü des Hosts sichtbar ist (Overlay ist interaktiv). Wenn das Menü verborgen ist, ist das Overlay-Fenster klickdurchlässig (WS_EX_TRANSPARENT), sodass ImGui-Fenster keine Mauseingaben empfangen können.
Verwenden Sie dies zur Implementierung des ziehbaren Overlay-Musters:
- Menü sichtbar: Ziehgriff anzeigen, Interaktion ermöglichen (Tabs, Buttons)
-
Menü verborgen: Ziehgriff entfernen,
ImGuiWindowFlags_NoInputshinzufügen, um das Fenster nicht-interaktiv zu machen
Siehe Abschnitt 6: Ziehbares Overlay-Muster für die vollständige Implementierung.
Direkter Zugriff auf den Spielprozessspeicher. Alle Lesevorgänge sind sicher (geben 0/leer bei Fehler zurück).
Gibt die Basisadresse des Spielmoduls zurück. Gibt 0 zurück, wenn nicht angebunden.
Gibt die Größe des Spielmoduls in Bytes zurück. Gibt 0 zurück, wenn nicht angebunden.
Liest einen Block von Roh-Bytes aus dem Spielprozess. Der buffer muss mindestens size Bytes zugewiesen haben. Gibt true bei Erfolg zurück.
// Example: Read a 4-byte integer from game memory
uint32_t value = 0;
m_Context->ReadProcessMemory(address, &value, sizeof(value));
// Example: Read a struct
MyStruct data{};
m_Context->ReadProcessMemory(structAddress, &data, sizeof(data));Liest einen nullterminierten ASCII-String aus dem Spielspeicher (max. 128 Zeichen).
Liest einen nullterminierten Unicode-(Wide-)String aus dem Spielspeicher (max. 128 wchars).
Gibt eine aufgelöste Pattern-Scan-Adresse nach Name zurück. Gibt 0 zurück, wenn nicht gefunden.
Standard-Patterns:
| Name | Beschreibung |
|---|---|
"Game States" |
GameStates-Vektor-Wurzel |
"File Root" |
Dateiregistrierung |
"AreaChangeCounter" |
Gebietsübergangszähler |
"Terrain Rotator Helper" |
Rotationsdaten |
"Terrain Rotation Selector" |
Rotationsauswahl |
"GameCullSize" |
Bildschirm-Culling-Wert |
Konvertiert eine Weltraumposition in Bildschirmkoordinaten. Gibt true zurück, wenn die Position auf dem Bildschirm sichtbar ist.
float screenX, screenY;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &screenX, &screenY)) {
ImGui::GetBackgroundDrawList()->AddText(ImVec2(screenX, screenY), IM_COL32_WHITE, "Label");
}Fordert den Host auf, Inventare zu scannen. Übergeben Sie -1 zum Scannen aller Inventare oder eine bestimmte Inventar-ID. Inventardaten im Schnappschuss werden nach Abschluss des Scans befüllt (nächster Frame).
Hinweis: Inventardaten werden nicht automatisch aktualisiert — Sie müssen diese Funktion aufrufen, um einen Scan auszulösen. Rufen Sie sie periodisch auf (z.B. alle 2 Sekunden), wenn Sie kontinuierlich aktualisierte Inventardaten benötigen.
Gibt einen Zeiger auf die Begehbarkeitsrasterdaten zurück. Das Raster ist ein 2D-Array, wobei 0 = nicht begehbar, ungleich Null = begehbar. Gibt nullptr zurück, wenn keine Daten verfügbar sind.
Gibt die Geländehöhe an einer Rasterposition zurück. Gibt 0 zurück, wenn außerhalb der Grenzen oder Daten nicht verfügbar.
Diese Funktionen lesen C++-Standardbibliotheks-Container direkt aus dem Spielspeicher und spiegeln die Core::Process-Methoden des Hosts wider.
Liest einen StdVector (24-Byte-Struktur: {First, Last, End}) aus dem Spielspeicher. Gibt einen mit malloc zugewiesenen Elementpuffer zurück. Der Aufrufer muss den zurückgegebenen Zeiger mit free() freigeben. Gibt nullptr bei Fehler zurück.
// Example: Read a vector of uint32_t
int count = 0;
void* data = m_Context->ReadStdVector(vectorAddr, sizeof(uint32_t), &count);
if (data && count > 0) {
uint32_t* values = static_cast<uint32_t*>(data);
for (int i = 0; i < count; i++) { /* values[i] */ }
free(data);
}Liest eine StdList (16-Byte-Struktur: {Head, Size}) aus dem Spielspeicher. Durchläuft die verkettete Liste und gibt einen zusammenhängenden Puffer zurück. Aufrufer muss free() aufrufen.
Liest einen StdBucket aus dem Spielspeicher (liest den eingebetteten StdVector). Aufrufer muss free() aufrufen.
Durchläuft eine StdMap (16-Byte-Struktur: {Head, Size}) und ruft callback für jedes Schlüssel-Wert-Paar auf. Gibt die Anzahl der besuchten Knoten zurück.
// Example: Read a map<uint32_t, float>
struct MapResult { std::vector<std::pair<uint32_t, float>> entries; };
MapResult result;
m_Context->ReadStdMap(mapAddr, sizeof(uint32_t), sizeof(float),
[](const void* key, const void* value, void* userData) {
auto* r = static_cast<MapResult*>(userData);
uint32_t k; float v;
memcpy(&k, key, sizeof(k));
memcpy(&v, value, sizeof(v));
r->entries.push_back({k, v});
}, &result);Liest einen StdWString (32-Byte-Struktur mit Inline-/Heap-Puffer) aus dem Spielspeicher.
Gibt den menschenlesbaren Namen für eine Inventar-ID zurück (z.B. 1 → "MainInventory1", 3 → "Weapon1", 64 → "Currency1").
SDK v4 bietet direkten Zugriff auf die Debug-Daten des Hosts — Entity-Komponenten, Inventardetails und UI-Elementbaum — entsprechend den eingebauten Debug-Tabs.
Gibt eine Liste aller Entitäten mit Debug-Metadaten zurück (Id, Address, Path, Type, SubType, State, Rarity, Zone). Dies entspricht dem Debug→Entity-Liste-Tab.
Beginnt die Überwachung der Komponenten einer Entity. Der Worker-Thread des Hosts liest jeden Frame vollständige Komponentendaten für diese Entity.
Beendet die Überwachung der Komponenten einer Entity. Rufen Sie dies auf, wenn der Benutzer den Entity-Baumknoten einklappt, um Ressourcen freizugeben.
Gibt die vollständigen Komponentendaten für eine überwachte Entity zurück. Enthält Unterstrukturen für alle 8 erkannten Komponenten (Life, Render, Positioned, Targetable, Animated, Stats, Actor, Buffs) plus die Liste aller Komponentenadressen.
// Example: Watch entity on expand, read components
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
if (ImGui::TreeNode(e.Path.c_str())) {
m_Context->WatchEntity(e.Id);
auto data = m_Context->GetWatchedEntityData(e.Id);
if (data.HasLife) {
ImGui::Text("HP: %d / %d ES: %d / %d",
data.Life.Health.Current, data.Life.Health.Total,
data.Life.EnergyShield.Current, data.Life.EnergyShield.Total);
}
ImGui::TreePop();
} else {
m_Context->UnwatchEntity(e.Id);
}
}Gibt die Basisadresse der ServerData-Komponente zurück.
Gibt alle Spieler-Inventar-IDs und deren Adressen zurück (aus ServerData).
Beginnt die Überwachung eines Inventars für detaillierte Debug-Inspektion. Der Host liest Slot-Belegung, Itemdetails und Modifikatoren.
Gibt vollständige Daten für das aktuell überwachte Inventar zurück: Rasterabmessungen, Slot-Belegung, Items mit Seltenheit und Modifikatoren.
// Example: Inventory inspector
auto invList = m_Context->GetPlayerInventoryList();
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
for (auto& item : inv.Items) {
ImGui::Text("[%s] Rarity=%d Mods=%d", item.Path.c_str(), item.Rarity,
(int)(item.ImplicitMods.size() + item.ExplicitMods.size()));
}Gibt die Adresse des Wurzel-Spiel-UI-Elements zurück (für die Navigation im In-Game-UI-Baum).
Gibt die Adresse des obersten UI-Wurzelelements zurück.
Gibt den aktuellen GameCullSize-Wert zurück, der für UI-Skalierungsberechnungen verwendet wird. In Kombination mit den Bildschirmabmessungen ermöglicht dies eine genaue Berechnung der UI-Element-Position/Größe.
// Example: UI scale calculation (matching host logic)
int cullValue = m_Context->GetGameCullValue();
auto snapshot = m_Context->GetSnapshot();
// Scale for index 1 (width): screenWidth / (cullValue / baseWidth)
// Scale for index 2 (height): screenHeight / (cullValue / baseHeight)Der Header sdk/PluginHelpers.h (im ExamplePlugin enthalten) stellt eine typsichere MemoryReader-Klasse bereit, die die rohen PluginContext-Funktionen umhüllt:
PluginSDK::MemoryReader mem(m_Context);
// Read a single struct
auto data = mem.Read<MyStruct>(address);
// Read an array
auto arr = mem.ReadArray<uint32_t>(address, count);
// Read native containers — returns std::vector<T>
auto vec = mem.ReadStdVector<uint32_t>(vectorAddr);
auto list = mem.ReadStdList<MyNode>(listAddr);
auto bucket = mem.ReadStdBucket<MyEntry>(bucketAddr);
auto map = mem.ReadStdMap<uint32_t, float>(mapAddr);
auto wstr = mem.ReadStdWString(wstringAddr);Der MemoryReader bietet auch Komfort-Wrapper für ReadString(), ReadUnicodeString(), GetBaseAddress(), GetModuleSize() und GetPatternAddress().
Zusätzliche Hilfsfunktionen in PluginHelpers.h:
-
WideToNarrow(wstring)— sichere wstring→string-Konvertierung (ASCII-verlustbehaftet) -
GetEntityTypeName(type)— Enum zu Anzeigename (einschließlich ExpeditionMarker/ExpeditionRemnant) -
GetNearbyZoneName(zone)— Zone zu Anzeigename -
GetRarityName(rarity)/GetRarityColor(rarity)— Seltenheits-Anzeige-Hilfsfunktionen
In das Protokollierungssystem des Hosts schreiben. Stufen: "Debug", "Info", "Warning", "Error"
Der ImGui-Kontext des Hosts. Rufen Sie ImGui::SetCurrentContext() damit in SetContext() auf.
Das ID3D11Device* des Hosts. Casten und zum Laden von Texturen verwenden.
Alle Typen befinden sich im Namespace PluginSDK. Plugins fügen normalerweise using namespace PluginSDK; hinzu.
Daten pro Entity, verfügbar in snapshot->Entities:
| Feld | Typ | Beschreibung |
|---|---|---|
Id |
uint32_t |
Eindeutige Entity-ID |
Address |
uintptr_t |
Speicheradresse (für Item-API-Aufrufe) |
EntityDetailsAddress |
uintptr_t |
Adresse der Entity-Details-Struktur |
RenderComponentAddress |
uintptr_t |
Adresse der Render-Komponente (Abkürzung) |
IsValid |
bool |
Entity-Gültigkeitsflag |
entityType |
EntityTypes |
Entity-Kategorie |
entitySubtype |
EntitySubtypes |
Entity-Unterkategorie |
entityState |
EntityStates |
Entity-Zustand |
Rarity |
int |
0=Normal, 1=Magisch, 2=Selten, 3=Einzigartig |
Reaction |
uint8_t |
0=Feindlich, 1=Neutral, 2=Freundlich |
GridPositionX/Y |
float |
Position im Geländeraster |
TerrainHeight |
float |
Geländehöhe an der Entity-Position |
WorldX/Y/Z |
float |
Weltraum-Position |
ModelBoundsZ |
float |
Modellhöhe |
Path |
wstring |
Entity-Metadaten-Pfad |
PlayerName |
wstring |
Spielername (wenn Spieler-Entity) |
TgtPath |
string |
Zielpfad (schmaler String) |
CurrentHP/MaxHP |
int |
Entity-Gesundheit |
CurrentES/MaxES |
int |
Entity-Energieschild |
IsSleeping |
bool |
Flag für weit entfernte Entity |
IsChestOpened |
bool |
Truhen-Öffnungszustand |
Zone |
NearbyZone |
Nähe zum Spieler |
ComponentCache |
EntityComponentCache |
Komponentenadressen |
Wichtig: Tote Entitäten (
EntityState::Useless) werden aus dem Schnappschuss entfernt, bevor er die Plugins erreicht. Sie werden nie sehen, wie die HP eines Monsters auf Null fallen — es verschwindet einfach aus der Liste. Verwenden Sie dasZone-Feld, um Kills (Entity verschwand aus InnerCircle/OuterCircle) von Entitäten außerhalb der Reichweite (Entity war in der Far-Zone) zu unterscheiden.
Aktiver Buff/Debuff:
| Feld | Typ | Beschreibung |
|---|---|---|
Name |
string |
Interner Buff-Name (z.B. "flask_effect_life") |
TimeLeft |
float |
Verbleibende Sekunden |
Charges |
short |
Stapelanzahl |
TotalTime |
float |
Gesamtdauer |
Minikarten-/Großkartenzustand:
| Feld | Typ | Beschreibung |
|---|---|---|
CenterX/Y |
float |
Kartenmitte |
SizeX/Y |
float |
Kartenabmessungen |
ShiftX/Y |
float |
Aktueller Schwenkversatz |
DefaultShiftX/Y |
float |
Standard-Versatzwerte |
Zoom |
float |
Zoomstufe |
Scale |
float |
Kartenskalierungsfaktor |
IsVisible |
bool |
Karte wird aktuell angezeigt |
| Feld | Typ | Beschreibung |
|---|---|---|
Id |
int |
Inventar-ID |
TotalBoxesX/Y |
int |
Rasterabmessungen |
Ptr |
uintptr_t |
Inventar-Speicheradresse |
Items |
vector<InventoryItemInfo> |
Items im Inventar |
Item-Felder: Address, Name (Metadaten-Pfad), Path (gleich wie Name), BaseTypeName (Basistypname, z.B. "Divine Orb"), UniqueName (einzigartiger Itemname aus Words.dat, z.B. "Headhunter", leer bei nicht-einzigartigen), SlotX/Y, Width/Height, StackCount, IsCurrency
Wird von ReadExtendedItemMods() zurückgegeben:
| Feld | Typ | Beschreibung |
|---|---|---|
ImplicitMods |
vector<ItemModData> |
Implizite Modifikatoren |
ExplicitMods |
vector<ItemModData> |
Explizite Modifikatoren |
EnchantMods |
vector<ItemModData> |
Verzauberungs-Modifikatoren |
HellscapeMods |
vector<ItemModData> |
Hellscape-Modifikatoren |
CrucibleMods |
vector<ItemModData> |
Tiegel-Modifikatoren |
Rarity |
int |
0=Normal, 1=Magisch, 2=Selten, 3=Einzigartig |
| Feld | Typ | Beschreibung |
|---|---|---|
Key |
string |
Mod-Stat-Schlüssel |
Values |
vector<float> |
Mod-Würfelwerte |
Komponentenadressen, pro Entity zwischengespeichert (verfügbar über RadarEntity.ComponentCache):
| Feld | Typ | Has-Methode |
|---|---|---|
RenderAddr |
uintptr_t |
HasRender() |
PositionedAddr |
uintptr_t |
HasPositioned() |
ChestAddr |
uintptr_t |
HasChest() |
PlayerAddr |
uintptr_t |
HasPlayer() |
ShrineAddr |
uintptr_t |
HasShrine() |
LifeAddr |
uintptr_t |
HasLife() |
TargetableAddr |
uintptr_t |
HasTargetable() |
OMPAddr |
uintptr_t |
HasOMP() |
NPCAddr |
uintptr_t |
HasNPC() |
TriggerableBlockageAddr |
uintptr_t |
HasTriggerableBlockage() |
DiesAfterTimeAddr |
uintptr_t |
HasDiesAfterTime() |
BuffsAddr |
uintptr_t |
HasBuffs() |
WorldItemAddr |
uintptr_t |
HasWorldItem() |
AreaTransitionAddr |
uintptr_t |
HasAreaTransition() |
MinimapIconAddr |
uintptr_t |
HasMinimapIcon() |
StatsAddr |
uintptr_t |
HasStats() |
Entity-Metadaten aus GetEntityDebugList():
| Feld | Typ | Beschreibung |
|---|---|---|
Id |
uint32_t |
Entity-ID |
Address |
uintptr_t |
Speicheradresse |
Path |
string |
Metadaten-Pfad |
EntityType |
int |
Entity-Typ (zu EntityTypes casten) |
EntitySubType |
int |
Entity-Untertyp (zu EntitySubtypes casten) |
EntityState |
int |
Entity-Zustand (zu EntityStates casten) |
Rarity |
int |
0=Normal, 1=Magisch, 2=Selten, 3=Einzigartig |
Zone |
NearbyZone |
Nähe zum Spieler |
ComponentAddresses |
vector<pair<string,uintptr_t>> |
Alle Komponentenname→Adresse-Paare |
Vollständige Komponentendaten aus GetWatchedEntityData():
| Feld | Typ | Beschreibung |
|---|---|---|
EntityId |
uint32_t |
Für welche Entity diese Daten sind |
Valid |
bool |
Ob die Daten erfolgreich gelesen wurden |
HasLife / Life
|
bool / DebugLifeComp
|
Life-Komponente — Life.Health, Life.EnergyShield, Life.Mana (jeweils ein DebugVital mit .Current, .Total, .Regeneration, .ReservedFlat, .ReservedPercent) |
HasRender / Render
|
bool / DebugRenderComp
|
Position (WorldX/Y/Z, GridX/Y), TerrainHeight, ModelBounds (X/Y/Z) |
HasPositioned / Positioned
|
bool / DebugPositionedComp
|
Reaction-Wert, IsFriendly-Flag |
HasTargetable / Targetable
|
bool / DebugTargetableComp
|
IsTargetable, IsHighlightable, IsTargettedByPlayer, HiddenFromPlayer, MeetsQuestState, MeetsItemRequirements |
HasAnimated / Animated
|
bool / DebugAnimatedComp
|
Animations-Path (string), Id (uint32) |
HasStats / Stats
|
bool / DebugStatsComp
|
CurrentWeaponIndex, IsShapeshifted, StatsItems/StatsBuff (Vektoren von Stat-ID→Wert-Paaren) |
HasActor / Actor
|
bool / DebugActorComp
|
AnimationId, AnimationName, ActiveSkills (Vektor von DebugActiveSkill), DeployedCounts[256] |
HasBuffs / Buffs
|
bool / vector<DebugBuff>
|
Aktive Buffs: Name, TotalTime, TimeLeft, Charges, FlaskSlot, Effectiveness, SourceEntityId |
Inventardetails aus GetWatchedInventoryData():
| Feld | Typ | Beschreibung |
|---|---|---|
InventoryId |
int |
Inventar-ID (-1 wenn keines) |
Address |
uintptr_t |
Inventaradresse |
TotalBoxesX/Y |
int |
Rasterabmessungen |
ServerRequestCounter |
int |
Server-Synchronisationszähler |
GridScreenX / GridScreenY
|
float |
Raster-UI-Bildschirmposition |
CellSize |
float |
Rasterzellengröße in Pixeln |
GridValid |
bool |
Ob die Raster-UI-Daten gültig sind |
SlotOccupied |
vector<bool> |
Belegung pro Slot |
Items |
vector<DebugInventoryItem> |
Items mit Pfad, Seltenheit und Modifikatoren |
| Feld | Typ | Beschreibung |
|---|---|---|
Address |
uintptr_t |
Item-Entity-Adresse |
Path |
string |
Item-Metadaten-Pfad |
BaseTypeName |
string |
Basistypname (z.B. "Divine Orb") |
UniqueName |
string |
Einzigartiger Itemname aus Words.dat (leer bei nicht-einzigartigen) |
SlotX / SlotY
|
int |
Rasterposition im Inventar |
Rarity |
int |
0=Normal, 1=Magisch, 2=Selten, 3=Einzigartig |
ItemLevel |
int |
Itemstufe |
RequiredLevel |
int |
Erforderliche Charakterstufe |
IsIdentified |
bool |
Ob das Item identifiziert ist |
IsCorrupted |
bool |
Ob das Item verdorben ist |
CraftedModCount |
int |
Anzahl der hergestellten Modifikatoren |
ImplicitMods |
vector<DebugModInfo> |
Implizite Modifikatoren |
ExplicitMods |
vector<DebugModInfo> |
Explizite Modifikatoren |
EnchantMods |
vector<DebugModInfo> |
Verzauberungs-Modifikatoren |
HellscapeMods |
vector<DebugModInfo> |
Hellscape-Modifikatoren |
| Feld | Typ | Beschreibung |
|---|---|---|
Name |
string |
Skillname |
UseStage |
int |
Aktuelle Nutzungsstufe |
CastType |
int |
Wirkungstyp |
TotalUses |
int |
Gesamte Nutzungsanzahl |
TotalCooldownTimeInMs |
int |
Abklingzeit in Millisekunden |
CanBeUsed |
bool |
Ob der Skill aktuell verwendet werden kann |
| Feld | Typ | Beschreibung |
|---|---|---|
Name |
string |
Interner Buff-Name |
TotalTime |
float |
Gesamtdauer |
TimeLeft |
float |
Verbleibende Sekunden |
Charges |
short |
Stapelanzahl |
FlaskSlot |
short |
Fläschchen-Slot-Index |
Effectiveness |
short |
Buff-Effektivität |
SourceEntityId |
uint32_t |
Entity, die diesen Buff angewendet hat |
| Feld | Typ | Beschreibung |
|---|---|---|
Name |
string |
Mod-Anzeigename |
StatKey |
string |
Stat-Schlüssel-Bezeichner |
AffixName |
string |
Affixname |
GenerationType |
int |
1=Präfix, 2=Suffix, 3=Implizit |
Value0 |
float |
Erster Wert (NaN wenn keiner) |
Value1 |
float |
Zweiter Wert (NaN wenn keiner) |
EntityTypes: Unidentified(0), Chest(1), NPC(2), Player(3), Shrine(4), Monster(5), DeliriumBomb(6), DeliriumSpawner(7), OtherImportantObjects(8), Item(9), Renderable(10), AreaTransition(11), ExpeditionMarker(12), ExpeditionRemnant(13)
EntitySubtypes: _Unidentified(0), _None(1), PlayerSelf(2), PlayerOther(3), ChestWithMagicRarity(4), ChestWithRareRarity(5), ExpeditionChest(6), BreachChest(7), Strongbox(8), SpecialNPC(9), POIMonster(10), PinnacleBoss(11), WorldItem(12), InventoryItem(13)
EntityStates: None(0), Useless(1), PlayerLeader(2), MonsterFriendly(3), PinnacleBossHidden(4)
NearbyZone: None(0), InnerCircle(1, ~60 Rastereinheiten), OuterCircle(2, ~120 Rastereinheiten), Far(3)
GameStateTypes: AreaLoadingState(0), ChangePasswordState(1), CreditsState(2), EscapeState(3), InGameState(4), PreGameState(5), LoginState(6), WaitingState(7), CreateCharacterState(8), SelectCharacterState(9), DeleteCharacterState(10), LoadingState(11), GameNotLoaded(12)
Host und Plugin teilen sich denselben ImGui-Kontext. Sie müssen aufrufen:
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(m_Context->ImGuiContext));in Ihrer SetContext()-Methode.
Verwenden Sie immer eindeutige Fenster-IDs, um Konflikte mit dem Host oder anderen Plugins zu vermeiden:
ImGui::Begin("My Window##MyPluginName", &showWindow);- Fenster, Tabs, Bäume, Tabellen, Zeichenlisten
- Texturladung über D3D11-Gerät
- Overlay-Rendering über
ImGui::GetBackgroundDrawList() - FontAwesome-6-Icons über
#include "imgui/IconsFontAwesome6.h"(z.B.ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT)
Verwenden Sie WorldToScreen(), um Beschriftungen/Formen an Entity-Positionen zu zeichnen:
float sx, sy;
if (m_Context->WorldToScreen(entity.WorldX, entity.WorldY, entity.WorldZ, &sx, &sy)) {
auto* drawList = ImGui::GetBackgroundDrawList();
drawList->AddText(ImVec2(sx, sy - 20), IM_COL32(255, 255, 0, 255), "Monster");
drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, IM_COL32(255, 0, 0, 255));
}Überschreiben Sie WantsOverlay(), um true zurückzugeben und den Host aufzufordern, in den Overlay-Modus zu wechseln:
bool WantsOverlay() override { return m_OverlayEnabled; }Im Overlay-Modus ist das Host-Fenster transparent und über dem Spiel positioniert. Ihre DrawUI()-Aufrufe rendern direkt auf dem Spielbildschirm.
Das Host-Overlay verwendet WS_EX_TRANSPARENT, um das Fenster klickdurchlässig zu machen, wenn das Menü verborgen ist. Das bedeutet, ImGui-Fenster können keine Mauseingaben empfangen, es sei denn, das Menü ist sichtbar. Um ein ziehbares Overlay-Fenster zu erstellen (wie das eingebaute Vitals-Overlay), verwenden Sie dieses Zweimodus-Muster:
#include "imgui/IconsFontAwesome6.h"
void MyPlugin::RenderOverlay() {
bool menuVisible = m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false;
if (menuVisible) {
// === DRAGGABLE MODE ===
// Window with background, drag hint, interactive controls
ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY), ImGuiCond_Appearing);
ImGui::SetNextWindowBgAlpha(m_Alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing;
ImGui::Begin("##MyOverlay", nullptr, flags);
// Drag hint (the entire window is draggable since there's no title bar)
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT " Drag to reposition");
ImGui::Spacing();
// ... your content here (tabs, text, icons, buttons — all interactive) ...
// Persist position when user drags the window
ImVec2 pos = ImGui::GetWindowPos();
if (pos.x != m_PosX || pos.y != m_PosY) {
m_PosX = pos.x;
m_PosY = pos.y;
// Save to settings on next SaveSettings() call
}
ImGui::End();
}
else {
// === NON-INTERACTIVE MODE ===
// Static overlay — no drag, no mouse interaction
ImGui::SetNextWindowPos(ImVec2(m_PosX, m_PosY));
ImGui::SetNextWindowBgAlpha(m_Alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoInputs;
ImGui::Begin("##MyOverlay", nullptr, flags);
// ... your content here (display-only, no interactive controls) ...
ImGui::End();
}
}Wichtige Punkte:
-
ImGuiCond_Appearingsetzt die Position nur beim ersten Anzeigen; ImGui verfolgt dann die Ziehposition -
NoTitleBar+ keinNoMove= Fenster ist von jedem leeren Bereich ziehbar (ImGui-Standardverhalten) -
NoInputsim nicht-interaktiven Modus verhindert, dass das Overlay den Fokus durchWS_EX_TRANSPARENTstiehlt - Prüfen Sie den
IsMenuVisible-Zeiger immer auf Null für Abwärtskompatibilität:m_Context->IsMenuVisible ? m_Context->IsMenuVisible() : false - Speichern Sie die Position in Ihrer Einstellungsdatei, damit sie zwischen Sitzungen erhalten bleibt
void OnEnable(bool isGameOpened) override {
LoadSettings(); // Load from Plugins/YourPlugin/config/settings.txt
}
void SaveSettings() override {
// Write to Plugins/YourPlugin/config/settings.txt
// The host calls this periodically and on shutdown
}Speichern Sie Einstellungen in <PluginDirectory>/config/:
std::filesystem::path settingsPath =
std::filesystem::path(m_Directory) / "config" / "settings.txt";Für Plugins mit vielen Einstellungen funktioniert ein einfaches Schlüssel=Wert-Textformat gut:
// Save
std::ofstream file(configDir / "settings.txt");
file << "ShowOverlay=" << (m_Settings.ShowOverlay ? 1 : 0) << "\n";
file << "WindowAlpha=" << m_Settings.WindowAlpha << "\n";
file << "PosX=" << m_Settings.PosX << "\n";
file << "PosY=" << m_Settings.PosY << "\n";
// Load
std::ifstream file(settingsPath);
std::string line;
while (std::getline(file, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "ShowOverlay") m_Settings.ShowOverlay = (val == "1");
else if (key == "WindowAlpha") m_Settings.WindowAlpha = std::stof(val);
else if (key == "PosX") m_Settings.PosX = std::stof(val);
else if (key == "PosY") m_Settings.PosY = std::stof(val);
}auto vitals = m_Context->GetPlayerVitals();
int hpPercent = vitals.HPPercent; // 0-100auto snapshot = m_Context->GetSnapshot();
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster &&
e.Zone == NearbyZone::InnerCircle) {
// e.CurrentHP, e.Path, e.WorldX/Y/Z...
}
}auto vitals = m_Context->GetPlayerVitals();
for (auto& buff : vitals.Buffs) {
if (buff.Name == "flask_effect_life") {
// buff.TimeLeft, buff.Charges...
}
}auto snapshot = m_Context->GetSnapshot();
std::string area = snapshot->CurrentAreaName;
bool isTown = snapshot->IsTown;
int level = snapshot->CurrentAreaLevel;for (auto& e : snapshot->Entities) {
if (e.entityType != EntityTypes::Monster) continue;
float sx, sy;
if (m_Context->WorldToScreen(e.WorldX, e.WorldY, e.WorldZ, &sx, &sy)) {
auto* dl = ImGui::GetBackgroundDrawList();
dl->AddText(ImVec2(sx, sy - 15), IM_COL32(255, 255, 0, 255), "Monster");
}
}// First, request an inventory scan (call periodically, e.g. every 2s)
m_Context->RequestInventoryScan(-1);
// Then read from snapshot (next frame)
auto snapshot = m_Context->GetSnapshot();
for (auto& inv : snapshot->Inventories) {
for (auto& item : inv.Items) {
auto mods = m_Context->ReadExtendedItemMods(item.Address);
// mods.ExplicitMods, mods.ImplicitMods...
}
}auto snapshot = m_Context->GetSnapshot();
int monsters = 0, items = 0;
for (auto& e : snapshot->Entities) {
if (e.entityType == EntityTypes::Monster) monsters++;
if (e.entityType == EntityTypes::Item) items++;
}static uint64_t lastAreaChange = 0;
auto snapshot = m_Context->GetSnapshot();
if (snapshot->AreaChangeCounter != lastAreaChange) {
lastAreaChange = snapshot->AreaChangeCounter;
// Area changed! Reset state...
}if (m_Context->GetCurrentState() == GameStateTypes::AreaLoadingState) {
// Currently loading...
}Tote Entitäten werden aus dem Schnappschuss gefiltert (EntityState::Useless), sodass Sie keinen HP-Abfall auf 0 erkennen können. Stattdessen verfolgen Sie Entitäten nach ID und erkennen, wann sie aus nahegelegenen Zonen verschwinden:
// In your tracker class:
struct TrackedEntity {
uint32_t Id;
int Rarity;
PluginSDK::NearbyZone Zone;
};
std::unordered_map<uint32_t, TrackedEntity> m_PrevEntities;
void DetectKills(const std::vector<PluginSDK::RadarEntity>& entities) {
std::unordered_set<uint32_t> currentIds;
// Update tracking map with current monsters
for (const auto& e : entities) {
if (e.entityType != EntityTypes::Monster) continue;
if (e.entityState == EntityStates::MonsterFriendly) continue;
currentIds.insert(e.Id);
m_PrevEntities[e.Id] = { e.Id, e.Rarity, e.Zone };
}
// Disappeared from InnerCircle/OuterCircle = killed
for (auto it = m_PrevEntities.begin(); it != m_PrevEntities.end(); ) {
if (currentIds.count(it->first) == 0) {
if (it->second.Zone == NearbyZone::InnerCircle ||
it->second.Zone == NearbyZone::OuterCircle) {
OnMonsterKilled(it->second.Rarity);
}
it = m_PrevEntities.erase(it);
} else {
++it;
}
}
}Warum das funktioniert: Entitäten innerhalb von ~120 Rastereinheiten, die plötzlich verschwinden, wurden fast sicher getötet (sind nicht einfach aus der Reichweite gelaufen). Entitäten in der Far-Zone tauchen natürlich in der Entity-Liste auf und verschwinden wieder — zählen Sie diese nicht.
Wichtig: Leeren Sie m_PrevEntities beim Gebietswechsel (wenn AreaChangeCounter sich ändert), um Fehlalarme zu vermeiden.
// Read a struct from a known address
struct MyGameStruct { int field1; float field2; };
MyGameStruct data{};
if (m_Context->ReadProcessMemory(someAddress, &data, sizeof(data))) {
// data.field1, data.field2 are now populated
}
// Read a string from memory
std::string str = m_Context->ReadString(stringAddress);uintptr_t gameStatesAddr = m_Context->GetPatternAddress("Game States");
if (gameStatesAddr != 0) {
// Read data at the resolved pattern address
uint64_t value = 0;
m_Context->ReadProcessMemory(gameStatesAddr, &value, sizeof(value));
}int gridW = 0, gridH = 0;
const uint8_t* grid = m_Context->GetWalkableGrid(&gridW, &gridH);
if (grid && gridW > 0 && gridH > 0) {
int x = (int)snapshot->Player.GridPositionX;
int y = (int)snapshot->Player.GridPositionY;
if (x >= 0 && x < gridW && y >= 0 && y < gridH) {
bool walkable = grid[y * gridW + x] != 0;
}
}PluginSDK::MemoryReader mem(m_Context);
// Read a struct from a known address
struct GameData { int level; float health; };
auto data = mem.Read<GameData>(address);
// Read a StdVector of pointers
auto ptrs = mem.ReadStdVector<uintptr_t>(vectorAddr);
for (auto ptr : ptrs) { /* process each pointer */ }
// Read a StdMap<int, float>
auto entries = mem.ReadStdMap<int, float>(mapAddr);
for (auto& [key, value] : entries) { /* key, value */ }for (auto& inv : snapshot->Inventories) {
const char* name = m_Context->GetInventoryName(inv.Id);
// name is e.g. "MainInventory1", "Weapon1", "Currency1"
}for (auto& e : snapshot->Entities) {
auto& cc = e.ComponentCache;
if (cc.HasLife()) {
// cc.LifeAddr contains the Life component address
// Use MemoryReader to read component structs
}
if (cc.HasRender()) {
// cc.RenderAddr has the Render component address
}
}// Get all entities with debug info
auto entities = m_Context->GetEntityDebugList();
for (auto& e : entities) {
bool open = ImGui::TreeNode(e.Path.c_str());
if (open) {
m_Context->WatchEntity(e.Id);
auto comp = m_Context->GetWatchedEntityData(e.Id);
if (comp.HasLife) {
ImGui::Text("HP: %d/%d ES: %d/%d MP: %d/%d",
comp.Life.Health.Current, comp.Life.Health.Total,
comp.Life.EnergyShield.Current, comp.Life.EnergyShield.Total,
comp.Life.Mana.Current, comp.Life.Mana.Total);
}
if (comp.HasActor) {
ImGui::Text("Animation: %s (%d) Skills: %d",
comp.Actor.AnimationName.c_str(), comp.Actor.AnimationId,
(int)comp.Actor.ActiveSkills.size());
}
ImGui::TreePop();
} else {
m_Context->UnwatchEntity(e.Id);
}
}auto invList = m_Context->GetPlayerInventoryList();
if (!invList.empty()) {
m_Context->WatchInventory(invList[0].first);
auto inv = m_Context->GetWatchedInventoryData();
if (inv.InventoryId >= 0) {
ImGui::Text("Grid: %dx%d Items: %d",
inv.TotalBoxesX, inv.TotalBoxesY, (int)inv.Items.size());
for (auto& item : inv.Items) {
ImGui::Text("[R%d iLvl%d] %s (%s) Mods: %d/%d/%d/%d",
item.Rarity, item.ItemLevel,
item.BaseTypeName.c_str(), item.Path.c_str(),
(int)item.ImplicitMods.size(), (int)item.ExplicitMods.size(),
(int)item.EnchantMods.size(), (int)item.HellscapeMods.size());
}
}
}uintptr_t uiRoot = m_Context->GetGameUiRootAddress();
if (uiRoot) {
PluginSDK::MemoryReader mem(m_Context);
// Read children vector at offset 0x010
auto children = mem.ReadStdVector<uintptr_t>(uiRoot + 0x010);
for (auto childAddr : children) {
// Read StringId at offset 0x448
uintptr_t strPtr = mem.Read<uintptr_t>(childAddr + 0x448);
if (strPtr) {
std::string name = m_Context->ReadString(strPtr);
ImGui::Text("Child: %s (0x%llX)", name.c_str(), childAddr);
}
}
}auto mods = m_Context->ReadExtendedItemMods(item.Address);
ImGui::TextColored(
PluginSDK::GetRarityColor(mods.Rarity),
"Rarity: %s", PluginSDK::GetRarityName(mods.Rarity));
for (auto& mod : mods.ExplicitMods) {
ImGui::BulletText("%s", mod.Key.c_str());
}| Einstellung | Wert |
|---|---|
| Configuration | Release |
| Platform | x64 |
| C++ Standard | /std:c++20 |
| Runtime Library |
/MD (Multi-threaded DLL) |
| Configuration Type | DLL |
- Ihre Plugin-
.cpp-Datei(en) - ImGui-Quelldateien:
imgui.cpp,imgui_draw.cpp,imgui_tables.cpp,imgui_widgets.cpp - Include-Pfad zum POEFixer-Stammverzeichnis (für SDK- und ImGui-Header)
- Optional: Kopieren Sie
Plugins/ExamplePlugin/sdk/PluginHelpers.hfür denMemoryReader-Wrapper und Hilfsfunktionen
Ihr .vcxproj sollte diese zusätzlichen Include-Verzeichnisse haben:
<AdditionalIncludeDirectories>$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>Wenn Sie lokale Drittanbieter-Bibliotheken verwenden (z.B. SQLite3 in einem lib/-Unterordner), fügen Sie $(ProjectDir)lib vor dem Lösungspfad hinzu, damit lokale Header Vorrang haben:
<AdditionalIncludeDirectories>$(ProjectDir)lib;$(SolutionDir)POEFixer;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>Um SQLite3 in einem Plugin zu verwenden, müssen Sie den Amalgamation-Quellcode direkt in Ihre DLL kompilieren — Windows LoadLibrary durchsucht nicht das eigene Verzeichnis der DLL nach Abhängigkeiten, sodass das dynamische Linken von sqlite3.dll mit Fehler 126 fehlschlagen wird.
Schritte:
- Kopieren Sie
sqlite3.cundsqlite3.hin daslib/-Verzeichnis Ihres Plugins - Erstellen Sie
lib/sqlite3-vcpkg-config.h, umSQLITE_APIzu überschreiben (verhindert__declspec(dllimport)-Fehler):#ifndef SQLITE_API #define SQLITE_API #endif #define SQLITE_ENABLE_UNLOCK_NOTIFY 1 #define SQLITE_OS_WIN 1 #define SQLITE_ENABLE_COLUMN_METADATA 1
- Fügen Sie
sqlite3.cals C-Datei mit deaktivierten Warnungen zu Ihrem.vcxprojhinzu:<ClCompile Include="lib\sqlite3.c"> <CompileAs>CompileAsC</CompileAs> <WarningLevel>TurnOffAllWarnings</WarningLevel> <SDLCheck>false</SDLCheck> <PreprocessorDefinitions>SQLITE_THREADSAFE=1;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile>
Zum Laden von Texturen aus Bilddateien (PNG, JPG) binden Sie stb_image in einer .cpp-Datei ein:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"Verwenden Sie dann das D3D11-Gerät aus m_Context->D3DDevice, um GPU-Texturen zu erstellen.
Setzen Sie Ihr Ausgabeverzeichnis auf:
$(SolutionDir)x64\Release\Plugins\YourPlugin\
Kopieren Sie Ihre erstellte DLL nach Plugins/YourPlugin/YourPlugin.dll neben die Hauptanwendung.
- Erstellen Sie Ihre Plugin-DLL im Debug-Modus
- Starten Sie die Host-Anwendung
- In Visual Studio: Debug → An Prozess anhängen → Host-.exe auswählen
- Setzen Sie Haltepunkte in Ihrem Plugin-Quellcode
- Der Debugger wird anhalten, wenn Ihr Code aufgerufen wird
| Problem | Lösung |
|---|---|
| Plugin not loading | Stellen Sie sicher, dass der DLL-Name exakt mit dem Ordnernamen übereinstimmt |
| "SDK version mismatch" | Erstellen Sie das Plugin mit den neuesten SDK-Headern neu (aktuelle Version: 4) |
| LoadLibrary error 126 | DLL hat unaufgelöste Abhängigkeiten. Für Drittanbieter-Bibliotheken wie SQLite3 kompilieren Sie diese statisch in die DLL (siehe Abschnitt 9). Verwenden Sie dumpbin /dependents YourPlugin.dll zur Überprüfung. |
| Crash on load | Prüfen Sie CRT-Inkompatibilität — beide müssen /MD verwenden |
| ImGui not rendering | Stellen Sie sicher, dass ImGui::SetCurrentContext() in SetContext() aufgerufen wird |
| Data is empty/zero | Prüfen Sie IsAttached() und IsInGame() vor dem Lesen von Daten |
| Inventory is empty | Rufen Sie RequestInventoryScan(-1) auf — Inventardaten werden bei Bedarf geladen |
| "Missing exports" error | Stellen Sie sicher, dass CreatePlugin und DestroyPlugin mit extern "C" exportiert werden |
| Plugin crashes host | Dies sollte nicht passieren — alle Plugin-Aufrufe sind SEH-geschützt. Prüfen Sie die Logs. |
| Stale data |
GetSnapshot() gibt die Daten des aktuellen Frames zurück. Cachen Sie den Zeiger nicht. |
| Memory read returns 0 | Stellen Sie sicher, dass IsAttached() true zurückgibt und die Adresse gültig ist |
| WorldToScreen returns false | Die Position kann hinter der Kamera oder außerhalb des Bildschirms sein |
| Overlay window not clickable | Der Host verwendet WS_EX_TRANSPARENT, wenn das Menü verborgen ist. Verwenden Sie IsMenuVisible(), um interaktive Steuerungen nur bei aktivem Menü anzuzeigen. Siehe das ziehbare Overlay-Muster in Abschnitt 6. |
| Kill/death detection doesn't work | Tote Entitäten werden aus dem Schnappschuss entfernt. Verwenden Sie verschwindungsbasierte Erkennung anstelle von HP-Übergangsbeobachtung. Siehe Abschnitt 8. |
| C2491 "dllimport function" errors | Die Header Ihrer Drittanbieter-Bibliothek definieren __declspec(dllimport). Erstellen Sie einen lokalen Überschreibungs-Header, der das API-Makro leer setzt (siehe SQLite3-Beispiel in Abschnitt 9). |