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; } }