using Roboto.GameOffsets.States; using Serilog; using Terrain = Roboto.GameOffsets.States.Terrain; namespace Roboto.Memory; /// /// Reads terrain walkability grid from AreaInstance, with caching and loading edge detection. /// public sealed class TerrainReader { private readonly MemoryContext _ctx; private uint _cachedTerrainAreaHash; private WalkabilityGrid? _cachedTerrain; private bool _wasLoading; public TerrainReader(MemoryContext ctx) { _ctx = ctx; } /// /// Invalidates the terrain cache (called when LocalPlayer changes on zone change). /// public void InvalidateCache() { _cachedTerrain = null; _cachedTerrainAreaHash = 0; } /// /// Reads terrain data from AreaInstance into the snapshot. /// Handles both inline and pointer-based terrain layouts. /// public void ReadTerrain(GameStateSnapshot snap, nint areaInstance) { var mem = _ctx.Memory; var offsets = _ctx.Offsets; if (!offsets.TerrainInline) { // 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(dimsPtr); snap.TerrainRows = mem.Read(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; } // Inline mode: TerrainStruct is inline at AreaInstance + TerrainListOffset // Single Read (0x1B0 = 432 bytes) replaces 5 individual reads var terrainBase = areaInstance + offsets.TerrainListOffset; var t = mem.Read(terrainBase); var cols = (int)t.Dimensions.X; var rows = (int)t.Dimensions.Y; 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) { _cachedTerrain = null; _cachedTerrainAreaHash = 0; return; } // Loading just finished — clear cache to force a fresh read if (_wasLoading) { _cachedTerrain = null; _cachedTerrainAreaHash = 0; } // Return cached grid if same area if (_cachedTerrain != null && _cachedTerrainAreaHash == snap.AreaHash) { snap.Terrain = _cachedTerrain; snap.TerrainWalkablePercent = CalcWalkablePercent(_cachedTerrain); return; } // Grid vector pointers already available from the Terrain struct read var gridBegin = t.WalkableGrid.First; var gridEnd = t.WalkableGrid.Last; if (gridBegin == 0 || gridEnd <= gridBegin) return; var gridDataSize = (int)(gridEnd - gridBegin); if (gridDataSize <= 0 || gridDataSize > 16 * 1024 * 1024) return; var bytesPerRow = t.BytesPerRow; 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) return; // Unpack 4-bit nibbles: each byte → 2 cells var data = new byte[gridWidth * gridHeight]; for (var row = 0; row < gridHeight; row++) { var rowStart = row * bytesPerRow; for (var col = 0; col < gridWidth; col++) { var byteIndex = rowStart + col / 2; if (byteIndex >= rawData.Length) break; data[row * gridWidth + col] = (col % 2 == 0) ? (byte)(rawData[byteIndex] & 0x0F) : (byte)((rawData[byteIndex] >> 4) & 0x0F); } } 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); } /// /// Updates the loading edge detection state. Call after ReadTerrain. /// public void UpdateLoadingEdge(bool isLoading) { _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; } }