lots done

This commit is contained in:
Boki 2026-03-02 11:17:37 -05:00
parent 1ba7c39c30
commit fbd0ba445a
59 changed files with 6074 additions and 3598 deletions

View file

@ -2,117 +2,126 @@ using Serilog;
namespace Automata.Memory;
public sealed class WalkabilityGrid
/// <summary>
/// Reads terrain walkability grid from AreaInstance, with caching and loading edge detection.
/// </summary>
public sealed class TerrainReader
{
public int Width { get; }
public int Height { get; }
public byte[] Data { get; }
private readonly MemoryContext _ctx;
private uint _cachedTerrainAreaHash;
private WalkabilityGrid? _cachedTerrain;
private bool _wasLoading;
public WalkabilityGrid(int width, int height, byte[] data)
public TerrainReader(MemoryContext ctx)
{
Width = width;
Height = height;
Data = data;
_ctx = ctx;
}
public bool IsWalkable(int x, int y)
/// <summary>
/// Invalidates the terrain cache (called when LocalPlayer changes on zone change).
/// </summary>
public void InvalidateCache()
{
if (x < 0 || x >= Width || y < 0 || y >= Height)
return false;
return Data[y * Width + x] == 0;
}
}
public sealed class TerrainReader : IDisposable
{
private readonly TerrainOffsets _offsets;
private ProcessMemory? _memory;
private PatternScanner? _scanner;
private nint _gameStateBase;
private bool _disposed;
public bool IsReady => _gameStateBase != 0;
public TerrainReader(TerrainOffsets offsets)
{
_offsets = offsets;
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
}
public bool Initialize()
/// <summary>
/// Reads terrain data from AreaInstance into the snapshot.
/// Handles both inline and pointer-based terrain layouts.
/// </summary>
public void ReadTerrain(GameStateSnapshot snap, nint areaInstance)
{
_memory?.Dispose();
_memory = ProcessMemory.Attach(_offsets.ProcessName);
if (_memory is null)
return false;
var mem = _ctx.Memory;
var offsets = _ctx.Offsets;
if (string.IsNullOrWhiteSpace(_offsets.GameStatePattern))
if (!offsets.TerrainInline)
{
Log.Warning("GameStatePattern is empty — offsets not yet configured for POE2");
return false;
// Pointer-based: AreaInstance → TerrainList → first terrain → dimensions
var terrainListPtr = mem.ReadPointer(areaInstance + offsets.TerrainListOffset);
if (terrainListPtr == 0) return;
var terrainPtr = mem.ReadPointer(terrainListPtr);
if (terrainPtr == 0) return;
var dimsPtr = mem.ReadPointer(terrainPtr + offsets.TerrainDimensionsOffset);
if (dimsPtr == 0) return;
snap.TerrainCols = mem.Read<int>(dimsPtr);
snap.TerrainRows = mem.Read<int>(dimsPtr + 4);
if (snap.TerrainCols > 0 && snap.TerrainCols < 1000 &&
snap.TerrainRows > 0 && snap.TerrainRows < 1000)
{
snap.TerrainWidth = snap.TerrainCols * offsets.SubTilesPerCell;
snap.TerrainHeight = snap.TerrainRows * offsets.SubTilesPerCell;
}
else
{
snap.TerrainCols = 0;
snap.TerrainRows = 0;
}
return;
}
_scanner = new PatternScanner(_memory);
_gameStateBase = _scanner.FindPatternRip(_offsets.GameStatePattern);
// Inline mode: TerrainStruct is inline at AreaInstance + TerrainListOffset
var terrainBase = areaInstance + offsets.TerrainListOffset;
var cols = (int)mem.Read<long>(terrainBase + offsets.TerrainDimensionsOffset);
var rows = (int)mem.Read<long>(terrainBase + offsets.TerrainDimensionsOffset + 8);
if (_gameStateBase == 0)
if (cols <= 0 || cols >= 1000 || rows <= 0 || rows >= 1000)
return;
snap.TerrainCols = cols;
snap.TerrainRows = rows;
snap.TerrainWidth = cols * offsets.SubTilesPerCell;
snap.TerrainHeight = rows * offsets.SubTilesPerCell;
// While loading, clear cached terrain and don't read (data is stale/invalid)
if (snap.IsLoading)
{
Log.Error("Failed to resolve GameState base pointer");
return false;
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
return;
}
Log.Information("GameState base: 0x{Address:X}", _gameStateBase);
return true;
}
public WalkabilityGrid? ReadTerrain()
{
if (_memory is null || _gameStateBase == 0)
return null;
// Follow pointer chain: GameState → InGameState → IngameData → TerrainData
var terrainBase = _memory.FollowChain(_gameStateBase, [
_offsets.InGameStateOffset,
_offsets.IngameDataOffset,
_offsets.TerrainDataOffset
]);
if (terrainBase == 0)
// Loading just finished — clear cache to force a fresh read
if (_wasLoading)
{
Log.Debug("Terrain pointer chain returned null");
return null;
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
}
var numCols = _memory.Read<int>(terrainBase + _offsets.NumColsOffset);
var numRows = _memory.Read<int>(terrainBase + _offsets.NumRowsOffset);
var bytesPerRow = _memory.Read<int>(terrainBase + _offsets.BytesPerRowOffset);
if (numCols <= 0 || numRows <= 0 || bytesPerRow <= 0)
// Return cached grid if same area
if (_cachedTerrain != null && _cachedTerrainAreaHash == snap.AreaHash)
{
Log.Warning("Invalid terrain dimensions: {Cols}x{Rows}, bytesPerRow={Bpr}", numCols, numRows, bytesPerRow);
return null;
snap.Terrain = _cachedTerrain;
snap.TerrainWalkablePercent = CalcWalkablePercent(_cachedTerrain);
return;
}
var gridWidth = numCols * _offsets.SubTilesPerCell;
var gridHeight = numRows * _offsets.SubTilesPerCell;
// Read GridWalkableData StdVector (begin/end/cap pointers)
var gridVecOffset = offsets.TerrainWalkableGridOffset;
var gridBegin = mem.ReadPointer(terrainBase + gridVecOffset);
var gridEnd = mem.ReadPointer(terrainBase + gridVecOffset + 8);
if (gridBegin == 0 || gridEnd <= gridBegin)
return;
// Read melee layer pointer
var layerPtr = _memory.ReadPointer(terrainBase + _offsets.LayerMeleeOffset);
if (layerPtr == 0)
{
Log.Warning("Melee layer pointer is null");
return null;
}
var gridDataSize = (int)(gridEnd - gridBegin);
if (gridDataSize <= 0 || gridDataSize > 16 * 1024 * 1024)
return;
// Read raw terrain data
var rawSize = bytesPerRow * gridHeight;
var rawData = _memory.ReadBytes(layerPtr, rawSize);
var bytesPerRow = mem.Read<int>(terrainBase + offsets.TerrainBytesPerRowOffset);
if (bytesPerRow <= 0 || bytesPerRow > 0x10000)
return;
var gridWidth = cols * offsets.SubTilesPerCell;
var gridHeight = rows * offsets.SubTilesPerCell;
var rawData = mem.ReadBytes(gridBegin, gridDataSize);
if (rawData is null)
{
Log.Warning("Failed to read terrain data ({Size} bytes)", rawSize);
return null;
}
return;
// Unpack 4-bit nibbles: each byte → 2 cells (low nibble = even col, high nibble = odd col)
// Unpack 4-bit nibbles: each byte → 2 cells
var data = new byte[gridWidth * gridHeight];
for (var row = 0; row < gridHeight; row++)
{
@ -128,14 +137,30 @@ public sealed class TerrainReader : IDisposable
}
}
Log.Information("Terrain read: {Width}x{Height} ({Cols}x{Rows} cells)", gridWidth, gridHeight, numCols, numRows);
return new WalkabilityGrid(gridWidth, gridHeight, data);
var grid = new WalkabilityGrid(gridWidth, gridHeight, data);
snap.Terrain = grid;
snap.TerrainWalkablePercent = CalcWalkablePercent(grid);
_cachedTerrain = grid;
_cachedTerrainAreaHash = snap.AreaHash;
Log.Information("Terrain grid read: {W}x{H} ({Cols}x{Rows} cells), {Pct}% walkable",
gridWidth, gridHeight, cols, rows, snap.TerrainWalkablePercent);
}
public void Dispose()
/// <summary>
/// Updates the loading edge detection state. Call after ReadTerrain.
/// </summary>
public void UpdateLoadingEdge(bool isLoading)
{
if (_disposed) return;
_disposed = true;
_memory?.Dispose();
_wasLoading = isLoading;
}
public static int CalcWalkablePercent(WalkabilityGrid grid)
{
var walkable = 0;
for (var i = 0; i < grid.Data.Length; i++)
if (grid.Data[i] != 0) walkable++;
return grid.Data.Length > 0 ? (int)(100L * walkable / grid.Data.Length) : 0;
}
}