7.8 KiB
POE2 Trade Bot — Architecture
Overview
Automated Path of Exile 2 trade bot with real-time minimap navigation. Monitors the trade site via WebSocket, travels to sellers, buys items from public stash tabs, and stores them. Built with .NET 8.0, Avalonia GUI, Playwright browser automation, and OpenCV screen vision.
Target: net8.0-windows10.0.19041.0 · Resolution: 2560×1440
Project Dependency Graph
Poe2Trade.Ui (Avalonia WinExe)
│
Poe2Trade.Bot
╱ │ │ ╲
Navigation Inventory Trade Log
│ │ │ │ │
Screen Game │ (Playwright)
│ │ │
└────┴───────┘
│
Core
| Project | Purpose |
|---|---|
| Core | Shared types, enums, config persistence, logging setup |
| Game | Win32 P/Invoke — window focus, SendInput, clipboard |
| Screen | DXGI/GDI capture, OpenCV processing, OCR bridge, grid scanning |
| Log | Polls Client.txt for area transitions, whispers, trade events |
| Trade | Playwright browser automation, WebSocket live search |
| Items | Ctrl+C clipboard item parsing |
| Inventory | Grid tracking (5×12), stash deposit, salvage routing |
| Navigation | Minimap capture, wall detection, world map stitching, BFS exploration |
| Bot | Orchestration hub, trade executor state machine, trade queue |
| Ui | Avalonia 11.2 desktop app, MVVM via CommunityToolkit, DI container |
Data Flow
Trading
Trade Site WebSocket ("new" items)
↓ NewListings event
BotOrchestrator
↓ enqueue
TradeQueue (FIFO, deduplicates item IDs)
↓ dequeue
TradeExecutor state machine
├─ ClickTravelToHideout (Playwright)
├─ FocusGame + wait for area transition (Client.txt)
├─ ScanStash (grid template matching)
├─ Ctrl+Right-click items (SendInput)
├─ /hideout → wait for area transition
└─ DepositItemsToStash (inventory grid scan)
States: Idle → Traveling → InSellersHideout → ScanningStash → Buying → WaitingForMore → GoingHome → InHideout
Map Exploration
NavigationExecutor.RunExploreLoop (capture loop)
├─ FramePipeline.ProcessOneFrame (DXGI / GDI)
├─ MinimapCapture.Process (HSV → wall mask, fog mask, player centroid)
├─ WorldMap.MatchAndStitch (template match → blit onto 4000×4000 canvas)
├─ WorldMap.FindNearestUnexplored (BFS → frontier counting)
└─ Post direction to input loop (volatile fields)
RunInputLoop (concurrent)
├─ WASD key holds from direction vector
└─ Periodic combat clicks (left + right)
Key Interfaces
| Interface | Impl | Role |
|---|---|---|
IGameController |
GameController |
Window focus, mouse/keyboard via P/Invoke SendInput |
IScreenReader |
ScreenReader |
Capture, OCR, template match, grid scan |
IScreenCapture |
DesktopDuplication / GdiCapture |
Raw screen frame acquisition |
IFrameConsumer |
MinimapCapture |
Processes frames from FramePipeline |
ITradeMonitor |
TradeMonitor |
Playwright persistent context, WebSocket events |
IClientLogWatcher |
ClientLogWatcher |
Polls Client.txt (200ms), fires area/whisper/trade events |
IInventoryManager |
InventoryManager |
12×5 grid tracking, stash deposit, salvage |
Screen Capture Pipeline
IScreenCapture (DXGI preferred, GDI fallback)
↓ ScreenFrame (Mat BGRA)
FramePipeline.ProcessOneFrame()
↓ dispatches to IFrameConsumer(s)
MinimapCapture.Process()
├─ Mode detection (overlay vs corner minimap)
├─ HSV thresholding → player mask, wall mask, fog mask
├─ Connected component filtering (min area)
├─ Wall color adaptation (per-map tint tracking)
└─ MinimapFrame (classified mat, wall mask, gray for correlation)
DXGI path: AcquireNextFrame → CopySubresourceRegion to staging → Map → memcpy to CPU Mat → Unmap + ReleaseFrame immediately (< 1ms hold).
OCR & Grid Detection
OCR Engine: Python EasyOCR daemon (stdin/stdout JSON IPC via PythonOcrBridge).
Tooltip Detection: Snapshot-diff approach — capture reference frame, hover item, diff to find darkened overlay rectangle via row/column density analysis.
Grid Scanning: Template match cells against empty35.png / empty70.png (MAD threshold=2). Item size detection via border comparison + union-find. Visual matching (NCC ≥ 0.70) to identify identical items.
| Grid | Region | Cell Size |
|---|---|---|
| Inventory (12×5) | 1696, 788, 840×350 | 70×70 |
| Stash (12×12) | 23, 169, 840×840 | 70×70 |
| Seller stash | 416, 299, 840×840 | 70×70 |
| Stash (24×24) | 23, 169, 840×840 | 35×35 |
World Map & Navigation
WorldMap maintains a 4000×4000 byte canvas (MapCell enum: Unknown, Explored, Wall, Fog).
Localization: Template matching (NCC) of current minimap frame against the canvas, plus player-offset correction. Falls back to gray-frame correlation tracking.
Exploration: Full BFS from current position. Propagates first-step direction to every reachable cell. Counts frontier cells (Unknown neighbors of Explored) per first-step direction. Picks the direction with the most frontier cells — prefers corridors over dead ends.
Stuck Recovery: If position unchanged for N frames, re-plan with shorter intervals.
Configuration
ConfigStore persists to config.json (JSON with camelCase enums).
SavedSettings
├── Paused, Headless, Mode (Trading / Mapping)
├── Links[] (Url, Name, Active, Mode, PostAction)
├── Poe2LogPath, Poe2WindowTitle, BrowserUserDataDir
├── TravelTimeoutMs, StashScanTimeoutMs, WaitForMoreItemsMs
├── BetweenTradesDelayMs
└── WindowX/Y/Width/Height (persisted window geometry)
LinkManager manages in-memory trade links with config sync. Extracts search ID and league label from URLs.
UI (Avalonia)
- Theme: Fluent dark
- Pattern: MVVM via
CommunityToolkit.Mvvm([ObservableProperty],[RelayCommand]) - DI:
Microsoft.Extensions.DependencyInjectioninApp.axaml.cs - Views: MainWindow (status + logs + links), Settings, Debug (pipeline stages), Mapping (live minimap)
- Log display: Last 500 entries, color-coded by level
Key Technical Decisions
| Decision | Rationale |
|---|---|
| KEYEVENTF_SCANCODE | Games read hardware scan codes, not virtual key codes |
| Persistent Playwright context | Preserves trade site login across restarts |
| Client.txt polling | More reliable than OCR for detecting area transitions |
| DXGI Desktop Duplication | ~4ms full-screen capture vs ~15ms GDI; early frame release prevents DWM stalls |
| Separate capture + input loops | Capture decisions never blocked by input latency (mouse moves, key holds) |
| Volatile fields for loop communication | Lock-free direction posting between capture and input loops |
| BFS frontier counting | Avoids dead-end rooms by preferring directions with more unexplored cells |
| Wall color adaptation | Each map has slightly different tint; tracks per-frame HSV statistics |
| Snapshot-diff OCR | Detects tooltips/dialogs by comparing against clean reference frame |
External Dependencies
| Package | Purpose |
|---|---|
| Avalonia 11.2 | Desktop UI framework |
| CommunityToolkit.Mvvm | MVVM source generators |
| Microsoft.Playwright | Browser automation + WebSocket |
| OpenCvSharp4 | Image processing, template matching, NCC |
| Vortice.Direct3D11 / DXGI | DXGI Desktop Duplication capture |
| System.Drawing.Common | GDI fallback capture |
| Serilog | Structured logging (console + rolling file) |