113 lines
3.8 KiB
C#
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;
|
|
}
|
|
}
|