poe2-bot/src/Roboto.Memory/Files/DatFile.cs
2026-03-05 16:44:30 -05:00

113 lines
3.8 KiB
C#

using Serilog;
namespace Roboto.Memory;
/// <summary>
/// Generic wrapper for a single .dat table. Bulk-reads all rows in one RPM call,
/// parses them via <see cref="IDatRowParser{T}"/>, and caches the results.
/// Supports O(1) FK resolution via <see cref="GetByAddress"/>.
/// </summary>
public sealed class DatFile<TRow> where TRow : class
{
private readonly string _fileName;
private readonly IDatRowParser<TRow> _parser;
private List<TRow>? _rows;
private Dictionary<nint, TRow>? _byAddress;
private bool _loadAttempted;
public DatFile(string fileName, IDatRowParser<TRow> parser)
{
_fileName = fileName;
_parser = parser;
}
/// <summary>All parsed rows, in order. Empty if not loaded or load failed.</summary>
public IReadOnlyList<TRow> Rows => _rows ?? (IReadOnlyList<TRow>)[];
/// <summary>Number of successfully parsed rows.</summary>
public int Count => _rows?.Count ?? 0;
/// <summary>True if Load() has been called and produced at least one row.</summary>
public bool IsLoaded => _rows is not null && _rows.Count > 0;
/// <summary>
/// Loads the .dat table from memory. Idempotent — second call is a no-op.
/// Returns true if rows were loaded successfully.
/// </summary>
public bool Load(FileRootScanner scanner, MemoryContext ctx, MsvcStringReader strings)
{
if (_loadAttempted) return IsLoaded;
_loadAttempted = true;
try
{
var tableInfo = scanner.FindDatFile(_fileName);
if (tableInfo is null)
{
Log.Debug("DatFile<{Type}>: {File} not found in FileRoot", typeof(TRow).Name, _fileName);
return false;
}
var info = tableInfo.Value;
var totalBytes = info.RowSize * info.RowCount;
if (totalBytes <= 0 || totalBytes > 10_000_000)
{
Log.Warning("DatFile<{Type}>: {File} has invalid size ({Bytes} bytes)", typeof(TRow).Name, _fileName, totalBytes);
return false;
}
var bulkData = ctx.Memory.ReadBytes(info.FirstRecord, totalBytes);
if (bulkData is null)
{
Log.Warning("DatFile<{Type}>: failed to bulk-read {File}", typeof(TRow).Name, _fileName);
return false;
}
var rows = new List<TRow>(info.RowCount);
var byAddr = new Dictionary<nint, TRow>(info.RowCount);
for (var i = 0; i < info.RowCount; i++)
{
var rowAddr = info.FirstRecord + i * info.RowSize;
var offset = i * info.RowSize;
var row = _parser.Parse(bulkData, offset, rowAddr, ctx, strings);
if (row is null) continue;
rows.Add(row);
byAddr[rowAddr] = row;
}
_rows = rows;
_byAddress = byAddr;
Log.Information("DatFile<{Type}>: loaded {Count} rows from {File}", typeof(TRow).Name, rows.Count, _fileName);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "DatFile<{Type}>: error loading {File}", typeof(TRow).Name, _fileName);
return false;
}
}
/// <summary>
/// O(1) lookup by row's process-memory address. Used for FK resolution.
/// Returns null if address is not found or table not loaded.
/// </summary>
public TRow? GetByAddress(nint address)
{
if (_byAddress is null || address == 0) return null;
return _byAddress.GetValueOrDefault(address);
}
/// <summary>
/// Clears cached data so the next Load() call re-reads from memory.
/// </summary>
public void InvalidateCache()
{
_rows = null;
_byAddress = null;
_loadAttempted = false;
}
}