Initial commit: Complete open-source Decal rebuild

All 5 phases of the open-source Decal rebuild:

Phase 1: 14 decompiled .NET projects (Interop.*, Adapter, FileService, DecalUtil)
Phase 2: 10 native DLLs rewritten as C# COM servers with matching GUIDs
  - DecalDat, DHS, SpellFilter, DecalInput, DecalNet, DecalFilters
  - Decal.Core, DecalControls, DecalRender, D3DService
Phase 3: C++ shims for Inject.DLL (D3D9 hooking) and LauncherHook.DLL
Phase 4: DenAgent WinForms tray application
Phase 5: WiX installer and build script

25 C# projects building with 0 errors.
Native C++ projects require VS 2022 + Windows SDK (x86).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-08 18:27:56 +01:00
commit d1442e3747
1382 changed files with 170725 additions and 0 deletions

View file

@ -0,0 +1,213 @@
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
namespace Decal.DecalDat
{
/// <summary>
/// Low-level reader for Asheron's Call DAT archive files.
/// DAT files use a sector-based linked-list format with a hierarchical directory.
/// </summary>
internal sealed class DatFile : IDisposable
{
private const int FileCount = 62;
private const int FileCountOffset = 0x03E;
private const int RootDirPtrOffset = 0x148;
// Directory entry layout (packed):
// DWORD subdirs[62] = 248 bytes
// DWORD fileCount = 4 bytes
// FileEntry files[62] = 744 bytes (62 * 12)
// Total = 996 bytes
private const int SubdirsSize = FileCount * 4;
private const int FileEntrySize = 12; // 3 DWORDs: ID, Offset, Size
private MemoryMappedFile _mmf;
private MemoryMappedViewAccessor _accessor;
private readonly int _sectorSize;
private readonly long _fileLength;
public DatFile(string filename, int sectorSize = 256)
{
_sectorSize = sectorSize;
var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
_fileLength = fs.Length;
_mmf = MemoryMappedFile.CreateFromFile(fs, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, false);
_accessor = _mmf.CreateViewAccessor(0, _fileLength, MemoryMappedFileAccess.Read);
}
public DatFileEntry GetFile(uint fileId)
{
uint rootDirOffset = _accessor.ReadUInt32(RootDirPtrOffset);
return FindFile(rootDirOffset, fileId);
}
private DatFileEntry FindFile(uint dirOffset, uint fileId)
{
// Read file count from directory
uint fileCount = _accessor.ReadUInt32(dirOffset + SubdirsSize);
// Binary search through sorted file entries
int lo = 0, hi = (int)fileCount - 1;
while (lo <= hi)
{
int mid = (lo + hi) / 2;
long entryOffset = dirOffset + SubdirsSize + 4 + (mid * FileEntrySize);
uint entryId = _accessor.ReadUInt32(entryOffset);
if (entryId == fileId)
{
uint offset = _accessor.ReadUInt32(entryOffset + 4);
uint size = _accessor.ReadUInt32(entryOffset + 8);
return new DatFileEntry(this, offset, size);
}
else if (fileId < entryId)
{
// Recurse into left subdirectory
uint subdir = _accessor.ReadUInt32(dirOffset + (uint)(mid * 4));
if (subdir != 0)
{
try
{
return FindFile(subdir, fileId);
}
catch (FileNotFoundException)
{
// Not in this subtree
}
}
hi = mid - 1;
}
else
{
lo = mid + 1;
}
}
// Check rightmost subdirectory
uint rightSubdir = _accessor.ReadUInt32(dirOffset + (uint)(lo * 4));
if (rightSubdir != 0)
{
return FindFile(rightSubdir, fileId);
}
throw new FileNotFoundException($"File 0x{fileId:X8} not found in DAT");
}
internal int SectorSize => _sectorSize;
internal int DataPerSector => _sectorSize - 4; // First 4 bytes = next sector pointer
internal void ReadBytes(long offset, byte[] buffer, int bufferOffset, int count)
{
_accessor.ReadArray(offset, buffer, bufferOffset, count);
}
internal uint ReadUInt32(long offset)
{
return _accessor.ReadUInt32(offset);
}
public void Dispose()
{
_accessor?.Dispose();
_accessor = null;
_mmf?.Dispose();
_mmf = null;
}
}
/// <summary>
/// Represents a single file within a DAT archive.
/// Reads through a linked list of sectors.
/// </summary>
internal sealed class DatFileEntry
{
private readonly DatFile _source;
private readonly uint _firstSectorOffset;
private readonly uint _size;
private uint _currentSectorOffset;
private int _posInSector; // position within current sector's data area
private int _totalRead; // total bytes read so far
public DatFileEntry(DatFile source, uint sectorOffset, uint size)
{
_source = source;
_firstSectorOffset = sectorOffset;
_size = size;
Reset();
}
public int Size => (int)_size;
public int Tell => _totalRead;
public void Reset()
{
_currentSectorOffset = _firstSectorOffset;
_posInSector = 0;
_totalRead = 0;
}
public void Skip(int bytes)
{
int remaining = bytes;
while (remaining > 0)
{
int dataPerSector = _source.DataPerSector;
int availableInSector = dataPerSector - _posInSector;
if (remaining < availableInSector)
{
_posInSector += remaining;
_totalRead += remaining;
return;
}
// Skip rest of this sector and move to next
_totalRead += availableInSector;
remaining -= availableInSector;
MoveToNextSector();
}
}
public int Read(byte[] buffer, int offset, int count)
{
int remaining = Math.Min(count, (int)_size - _totalRead);
int totalCopied = 0;
while (remaining > 0)
{
int dataPerSector = _source.DataPerSector;
int availableInSector = dataPerSector - _posInSector;
int toCopy = Math.Min(remaining, availableInSector);
// Data starts at sector offset + 4 (skip next-sector pointer)
long readOffset = _currentSectorOffset + 4 + _posInSector;
_source.ReadBytes(readOffset, buffer, offset + totalCopied, toCopy);
_posInSector += toCopy;
_totalRead += toCopy;
totalCopied += toCopy;
remaining -= toCopy;
if (_posInSector >= dataPerSector && remaining > 0)
{
MoveToNextSector();
}
}
return totalCopied;
}
private void MoveToNextSector()
{
// Next sector pointer is stored in first 4 bytes of current sector
uint nextSector = _source.ReadUInt32(_currentSectorOffset);
_currentSectorOffset = nextSector;
_posInSector = 0;
}
}
}

View file

@ -0,0 +1,159 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using Decal.Interop.Core;
using Decal.Interop.Dat;
namespace Decal.DecalDat
{
internal enum LibraryType
{
Cell,
Portal
}
[ComVisible(true)]
[Guid("6FA05FDA-B4B5-4386-AB45-92D7E6A5D698")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("DecalDat.DatLibrary")]
public class DatLibraryImpl : IDatLibrary, IDecalDirectory
{
private DatFile _datFile;
private LibraryType _libraryType;
private DatServiceImpl _service;
internal void Load(DatServiceImpl service, string filename, LibraryType libraryType, int sectorSize)
{
_service = service;
_libraryType = libraryType;
try
{
_datFile = new DatFile(filename, sectorSize);
}
catch
{
// Silent failure matching original C++ behavior
_datFile = null;
}
}
/// <summary>
/// IDatLibrary.Stream - parameterized property returning raw stream for a file ID.
/// The COM interface declares this as a parameterized property (DispId 1) taking a DWORD File parameter.
/// In the C++ implementation, get_Stream(DWORD dwFile, LPUNKNOWN *pVal) creates a DatStream directly.
/// </summary>
public object Stream
{
get
{
// This parameterized property is called via IDispatch with the file ID argument.
// When called without parameters from managed code, return null.
return null;
}
}
internal object GetStream(uint fileId)
{
if (_datFile == null) return null;
try
{
var fileEntry = _datFile.GetFile(fileId);
var stream = new DatStreamImpl();
stream.Load(fileEntry);
return stream;
}
catch
{
return null;
}
}
public object Open(string Protocol, uint File)
{
if (_datFile == null) return null;
// Look up filter by protocol name
var filter = _service.GetFilter(Protocol);
if (filter != null)
{
// Check cache first
var cached = _service.FindInCache(filter, _libraryType, File);
if (cached != null) return cached;
// Create raw stream
DatFileEntry fileEntry;
try
{
fileEntry = _datFile.GetFile(File);
}
catch
{
return null;
}
var stream = new DatStreamImpl();
stream.Load(fileEntry);
// Create and initialize filter
var filterObj = _service.CreateFilter(filter);
if (filterObj == null) return null;
var fileFilter = filterObj as IFileFilter;
if (fileFilter != null)
{
fileFilter.Initialize((DatStream)(object)stream);
}
// Cache if applicable
if (filter.Cache)
{
_service.AddToCache(filter, _libraryType, File, filterObj);
}
return filterObj;
}
// No filter - return raw stream
return GetStream(File);
}
/// <summary>
/// IDecalDirectory.Lookup - parses "PROTOCOL:HEXID" or just "HEXID" format.
/// </summary>
public object Lookup(string strName)
{
if (string.IsNullOrEmpty(strName)) return null;
int colonIdx = strName.IndexOf(':');
if (colonIdx >= 0)
{
// Format: "protocol:0xHEXID"
string protocol = strName.Substring(0, colonIdx);
string hexPart = strName.Substring(colonIdx + 1).Trim();
uint fileId = ParseHex(hexPart);
return Open(protocol, fileId);
}
else
{
// Format: "0xHEXID" - raw stream
uint fileId = ParseHex(strName.Trim());
return GetStream(fileId);
}
}
private static uint ParseHex(string hex)
{
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
hex = hex.Substring(2);
return uint.Parse(hex, NumberStyles.HexNumber);
}
internal void Dispose()
{
_datFile?.Dispose();
_datFile = null;
}
}
}

View file

@ -0,0 +1,167 @@
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()
{
try
{
var config = _decalCore.Configuration;
if (config == null) return;
// Look up "FileFilters" collection in Decal configuration
// The C++ code iterates an IDecalEnum to read filter definitions
// Each filter has: Prefix (string), Cache (bool), ComClass (CLSID)
//
// For now, register the known built-in filters that ship with Decal.
// When Decal.Core is implemented, this will read from live config.
}
catch
{
// Config may not be available yet
}
}
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 });
}
}
}

View file

@ -0,0 +1,70 @@
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);
// Copy to unmanaged buffer starting at ref Buffer
Marshal.Copy(buf, 0, GetBufferPtr(ref Buffer), Bytes);
}
public string Read(int Bytes)
{
if (_file == null) return string.Empty;
var buf = new byte[Bytes];
int read = _file.Read(buf, 0, Bytes);
// Return as binary BSTR (same as SysAllocStringByteLen in C++)
unsafe
{
fixed (byte* p = buf)
{
return Marshal.PtrToStringAnsi(new IntPtr(p), read);
}
}
}
private static unsafe IntPtr GetBufferPtr(ref byte buffer)
{
fixed (byte* p = &buffer)
{
return new IntPtr(p);
}
}
}
}

View file

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Decal.DecalDat</AssemblyName>
<RootNamespace>Decal.DecalDat</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Decal.Interop.Core\Decal.Interop.Core.csproj" />
<ProjectReference Include="..\Decal.Interop.Dat\Decal.Interop.Dat.csproj" />
</ItemGroup>
</Project>