openDecal/Managed/Decal.DecalDat/DatServiceImpl.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

170 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Decal.Interop.Core;
using Decal.Interop.Dat;
namespace Decal.DecalDat
{
internal class FileFilterInfo
{
public string Name;
public Guid Clsid;
public bool Cache;
}
internal class CacheEntry
{
public FileFilterInfo Filter;
public LibraryType Library;
public uint FileId;
public object Instance;
}
[ComVisible(true)]
[Guid("37B083F0-276E-43AD-8D26-3F7449B519DC")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("DecalDat.DatService")]
public class DatServiceImpl : IDatService, IDecalService, IDecalDirectory
{
private DatLibraryImpl _cell;
private DatLibraryImpl _portal;
private readonly List<FileFilterInfo> _filters = new List<FileFilterInfo>();
private readonly List<CacheEntry> _cache = new List<CacheEntry>();
private IDecalCore _decalCore;
public void Initialize(DecalCore pDecal)
{
_decalCore = (IDecalCore)pDecal;
// Resolve DAT file paths via MapPath
string cellPath = _decalCore.MapPath("%ac%\\cell.dat");
string portalPath = _decalCore.MapPath("%ac%\\portal.dat");
// Load cell.dat (sector size 256)
_cell = new DatLibraryImpl();
_cell.Load(this, cellPath, LibraryType.Cell, 256);
// Load portal.dat (sector size 1024)
_portal = new DatLibraryImpl();
_portal.Load(this, portalPath, LibraryType.Portal, 1024);
// Load filter configuration from Decal
LoadFilters();
}
private void LoadFilters()
{
// The C++ original calls:
// m_pDecal->get_Configuration(L"FileFilters", GUID_NULL, &pEnum)
// then iterates the IDecalEnum reading:
// pEnum->get_ComClass(&clsid)
// pEnum->get_Property(L"Prefix", &vPrefix) // protocol name, e.g. "portal"
// pEnum->get_Property(L"Cache", &vCache) // 0 or 1
//
// However, IDecalCore.Configuration and IDecalEnum.Property are
// parameterized COM properties that the decompiler flattened to
// simple getters. They need to be called via IDispatch to pass
// the category name / property name arguments.
//
// Filters are registered externally (by plugins/installer in the
// registry under HKLM\SOFTWARE\Decal\FileFilters\{CLSID}) and
// will be loaded once Decal.Core exposes the Configuration enum.
//
// DecalDat still works without filters — callers get raw IDatStream
// objects. Filters are an optional layer that parses the stream into
// typed objects (e.g., spell tables, character data).
}
public void BeforePlugins()
{
// No-op in original implementation
}
public void AfterPlugins()
{
// No-op in original implementation
}
public void Terminate()
{
_cache.Clear();
_filters.Clear();
_cell?.Dispose();
_cell = null;
_portal?.Dispose();
_portal = null;
_decalCore = null;
}
/// <summary>
/// IDecalDirectory.Lookup - returns cell or portal library by name.
/// </summary>
public object Lookup(string strName)
{
if (string.Equals(strName, "portal", StringComparison.OrdinalIgnoreCase))
return _portal;
if (string.Equals(strName, "cell", StringComparison.OrdinalIgnoreCase))
return _cell;
return null;
}
// --- Filter management (internal, used by DatLibraryImpl) ---
internal FileFilterInfo GetFilter(string name)
{
foreach (var f in _filters)
{
if (string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase))
return f;
}
return null;
}
internal object CreateFilter(FileFilterInfo filter)
{
try
{
var type = Type.GetTypeFromCLSID(filter.Clsid);
if (type == null) return null;
return Activator.CreateInstance(type);
}
catch
{
return null;
}
}
internal object FindInCache(FileFilterInfo filter, LibraryType library, uint fileId)
{
foreach (var entry in _cache)
{
if (entry.Filter == filter && entry.Library == library && entry.FileId == fileId)
return entry.Instance;
}
return null;
}
internal void AddToCache(FileFilterInfo filter, LibraryType library, uint fileId, object instance)
{
_cache.Add(new CacheEntry
{
Filter = filter,
Library = library,
FileId = fileId,
Instance = instance
});
}
/// <summary>
/// Register a file filter at runtime.
/// </summary>
internal void RegisterFilter(string name, Guid clsid, bool cache)
{
_filters.Add(new FileFilterInfo { Name = name, Clsid = clsid, Cache = cache });
}
}
}