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>
170 lines
5.4 KiB
C#
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 });
|
|
}
|
|
}
|
|
}
|