poe2-bot/docs/data-and-memory.md
2026-03-07 09:53:57 -05:00

7.3 KiB
Raw Permalink Blame History

Nexus.Data & Nexus.Memory — The Data Pipeline

Overview

Two-layer architecture: Memory reads raw bytes from the game process; Data interprets, classifies, and caches them. This separation keeps memory reading logic free of business rules and makes the data layer testable independently.

Game Process (RPM)
    │
    ▼
Nexus.Memory (raw reads, no business logic)
    │  GameMemoryReader → hierarchical state tree
    │  EntityList → red-black tree traversal
    │  ComponentReader → ECS component extraction
    │
    ▼
Nexus.Data (interpretation, classification, caching)
    │  MemoryPoller → two-tier event loop
    │  EntityMapper → Memory.Entity → Core.EntitySnapshot
    │  EntityClassifier → path + components → EntityCategory
    │  GameStateEnricher → derived threat/danger metrics
    │
    ▼
GameDataCache (volatile references, lock-free)
    │
    ▼
Bot Systems (read-only consumers)

GameDataCache — Single Source of Truth

Thread-safe, volatile reference holder. Writer: MemoryPoller thread. Readers: bot systems.

Hot fields (updated at 60Hz — 4 lightweight RPM calls):

  • CameraMatrix (64 bytes)
  • PlayerPosition (X, Y, Z)
  • PlayerVitals (HP, mana, ES current/max)
  • IsLoading, IsEscapeOpen

Cold fields (updated at 10Hz — full hierarchical read):

  • Entities, HostileMonsters, NearbyLoot
  • Terrain (WalkabilitySnapshot)
  • AreaHash, AreaLevel, CurrentAreaName
  • Quest data (linked lists, UI groups, state entries)
  • LatestState (complete GameState)

Slow fields (updated at 1Hz):

  • Character name
  • Quest linked lists, quest state entries

No locks — relies on volatile reference semantics for atomic swaps.


MemoryPoller — Two-Tier Event Loop

Owns the memory-reading background thread.

Hot Tick (60Hz)

4 pre-resolved RPM calls using cached addresses:

  1. Camera matrix (64 bytes from cached address)
  2. Player position (12 bytes: X, Y, Z)
  3. Player vitals (24 bytes: HP, mana, ES)
  4. Loading/escape state (pointer dereference + int)

No allocations, no GC. Targeting ~3-5ms per tick.

Cold Tick (10Hz, every 6th hot tick)

Full hierarchical read:

  1. GameMemoryReader.ReadSnapshot() — cascades through state tree
  2. Re-resolve hot addresses via ResolveHotAddresses()
  3. BuildGameState() — map entities, filter lists, process quests
  4. GameStateEnricher.Enrich() — compute derived fields
  5. Update all cache fields

BuildGameState() Flow

ReadSnapshot() → GameStateSnapshot (raw)
    │
    ├── Entity mapping:
    │   for each entity in snapshot:
    │     EntityMapper.MapEntity(entity, playerPos) → EntitySnapshot
    │
    ├── Filter into:
    │   - HostileMonsters (Category==Monster && IsAlive)
    │   - NearbyLoot (Category==WorldItem)
    │   - All entities
    │
    ├── Quest processing:
    │   - Filter active (StateId > 0)
    │   - Resolve state text via QuestStateLookup
    │   - Convert to QuestProgress, QuestInfo
    │
    └── Returns GameState

GameMemoryReader — Hierarchical State Tree

Top-level orchestrator. Creates sub-readers on Attach():

GameStates (top)
└── InGameState
    ├── AreaInstance
    │   ├── EntityList (MSVC std::map red-black tree)
    │   ├── PlayerSkills (Actor component)
    │   ├── QuestStates (dat file entries)
    │   └── Terrain (walkability grid)
    ├── UIElements (quest linked lists, UI tree)
    └── WorldData (camera matrix)

Each RemoteObject caches its data and depends on parent for context. Single Update() call cascades through tree.

Infrastructure

  • ProcessMemory: P/Invoke wrapper for ReadProcessMemory. Tracks reads/sec and KB/sec.
  • MemoryContext: Shared state — process handle, offsets, module base, pattern scanner.
  • ComponentReader: Reads ECS components (Life, Render, Mods, etc.) from entities.
  • MsvcStringReader: Reads MSVC std::wstring (SSO-aware: inline if capacity ≤ 8, heap pointer otherwise).
  • PatternScanner: AOB scan for resolving base addresses.

EntityList — Tree Traversal

Reads entities from AreaInstance's MSVC std::map (red-black tree, in-order traversal).

Tree node layout:

+0x00: left child ptr
+0x08: parent ptr
+0x10: right child ptr
+0x28: entity pointer

Optimization: Tree order is cached — re-walked only when entity count changes.

Per-entity reads:

  1. Path (EntityDetails → std::wstring)
  2. Skip low-priority types (effects, terrain, critters — no components read)
  3. Position (Render component: X, Y, Z)
  4. Component lookup (STL hash map: name → index)
  5. Component data:
    • Targetable (bool flag)
    • Mods/ObjectMagicProperties (rarity)
    • Life (HP, dynamic — re-read every frame for monsters)
    • Actor (action ID)
    • WorldItem (inner entity for ground loot)
    • AreaTransition (destination area)

Caching strategy:

  • Stable per entity: path, component list, targetable, rarity, transition name
  • Dynamic (re-read every frame): monster HP, action ID

EntityClassifier — Path + Components → Category

Single source of truth for entity classification.

  1. Path-based (primary): Parses Metadata/[Category]/... path segments
  2. Component override: Monster, Chest, Shrine, Waypoint, AreaTransition, Portal, TownPortal, NPC, Player

Output: EntityCategory (Core enum, 17 types)


EntityMapper — Memory.Entity → Core.EntitySnapshot

Transforms raw memory data to enriched snapshots:

Memory.Entity (raw)
    │
    ├── Copy: ID, path, metadata, position, Z, vitals, components, mods
    ├── Classify: EntityClassifier.Classify(path, components) → EntityCategory
    ├── Threat level: Rarity → MonsterThreatLevel
    ├── Area name: AreaNameLookup.Resolve() for transitions
    ├── Distance: Vector2.Distance(position, playerPos)
    └── Alive state: HasVitals ? LifeCurrent > 0 : true
    │
    ▼
Core.EntitySnapshot (public, classified, enriched)

GameStateEnricher — Derived Metrics

Computed once per cold tick, before systems run.

NearestEnemies: HostileMonsters sorted by distance to player.

ThreatMap:

  • TotalHostiles, CloseRange (<300u), MidRange (300-600u), FarRange (600-1200u)
  • ClosestDistance, ThreatCentroid (position average), HasRareOrUnique

DangerLevel — Weighted threat score:

score = Σ (distance_weight × rarity_multiplier)

Distance weights:  <200u = 3×,  <400u = 2×,  else = 1×
Rarity multipliers: Unique=5×, Rare=3×, Magic=1.5×, White=1×

Life override:  HP < 30% → Critical,  HP < 50% → High
Score thresholds: ≥15 = Critical, ≥8 = High, ≥4 = Medium, >0 = Low, 0 = Safe

Key Architectural Patterns

Pattern Implementation
Lock-free cross-thread Volatile references in GameDataCache; no locks needed
Two-tier polling Hot (4 RPM, 60Hz) + Cold (full read, 10Hz)
Hierarchical caching Each RemoteObject caches data, re-reads only on change
Entity caching Stable data cached per entity/zone; dynamic data (HP) re-read per frame
Separation of concerns Memory: raw bytes. Data: interpretation + classification
Area name resolution AreaNameLookup loads areas.json, caches ID → display name
Area graph BFS pathfinding for quest progression ordering