using Serilog;
namespace Roboto.Memory;
///
/// Generic wrapper for a single .dat table. Bulk-reads all rows in one RPM call,
/// parses them via , and caches the results.
/// Supports O(1) FK resolution via .
///
public sealed class DatFile where TRow : class
{
private readonly string _fileName;
private readonly IDatRowParser _parser;
private List? _rows;
private Dictionary? _byAddress;
private bool _loadAttempted;
public DatFile(string fileName, IDatRowParser parser)
{
_fileName = fileName;
_parser = parser;
}
/// All parsed rows, in order. Empty if not loaded or load failed.
public IReadOnlyList Rows => _rows ?? (IReadOnlyList)[];
/// Number of successfully parsed rows.
public int Count => _rows?.Count ?? 0;
/// True if Load() has been called and produced at least one row.
public bool IsLoaded => _rows is not null && _rows.Count > 0;
///
/// Loads the .dat table from memory. Idempotent — second call is a no-op.
/// Returns true if rows were loaded successfully.
///
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(info.RowCount);
var byAddr = new Dictionary(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;
}
}
///
/// O(1) lookup by row's process-memory address. Used for FK resolution.
/// Returns null if address is not found or table not loaded.
///
public TRow? GetByAddress(nint address)
{
if (_byAddress is null || address == 0) return null;
return _byAddress.GetValueOrDefault(address);
}
///
/// Clears cached data so the next Load() call re-reads from memory.
///
public void InvalidateCache()
{
_rows = null;
_byAddress = null;
_loadAttempted = false;
}
}