openDecal/Managed/Decal.DecalDat/DatStreamImpl.cs
erik f0b6fedc9b Fix DecalDat to work with real AC DAT files
The DAT file reader had several bugs inherited from the old C++ reference
code, which targeted an older format version. Verified and fixed against
real client_portal.dat and client_cell_1.dat files:

- Fix header offset: BTree root is at 0x160, not 0x148 (file size field)
- Fix BTree entry size: 24 bytes (flags+id+offset+size+timestamp), not 12
- Fix sector-chain node reading: BTree nodes span multiple sectors via
  linked-list headers; must assemble node data across sector boundaries
- Fix DatStreamImpl.Read() BSTR handling: use Buffer.BlockCopy to match
  C++ SysAllocStringByteLen instead of Marshal.PtrToStringAnsi
- Fix DatStreamImpl.ReadBinary() pointer lifetime: inline fixed block to
  keep destination buffer pinned during Marshal.Copy
- Document LoadFilters() dependency on parameterized COM properties in
  IDecalCore.Configuration that need IDispatch to call correctly

Add smoke test project (13/13 tests pass against real DAT files).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:12:04 +01:00

67 lines
1.8 KiB
C#

using System;
using System.Runtime.InteropServices;
using Decal.Interop.Dat;
namespace Decal.DecalDat
{
[ComVisible(true)]
[Guid("9F7F6CD9-D164-418D-8CB5-3B9ACD70BEAF")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("DecalDat.DatStream")]
public class DatStreamImpl : IDatStream
{
private DatFileEntry _file;
internal void Load(DatFileEntry file)
{
_file = file;
}
public int Size => _file?.Size ?? 0;
public int Tell => _file?.Tell ?? 0;
public void Skip(int Bytes)
{
_file?.Skip(Bytes);
}
public void Restart()
{
_file?.Reset();
}
public void ReadBinary(int Bytes, ref byte Buffer)
{
if (_file == null) return;
var buf = new byte[Bytes];
_file.Read(buf, 0, Bytes);
// Pin the destination buffer for the duration of the copy
unsafe
{
fixed (byte* p = &Buffer)
{
Marshal.Copy(buf, 0, new IntPtr(p), Bytes);
}
}
}
public string Read(int Bytes)
{
if (_file == null) return string.Empty;
var buf = new byte[Bytes];
int read = _file.Read(buf, 0, Bytes);
// Match C++ SysAllocStringByteLen: pack raw bytes into WCHAR pairs.
// When the CLR marshals this string to BSTR, the raw bytes are preserved
// byte-for-byte (each char = 2 raw bytes, little-endian).
int charCount = (read + 1) / 2;
var chars = new char[charCount];
Buffer.BlockCopy(buf, 0, chars, 0, read);
return new string(chars, 0, charCount);
}
}
}