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:
commit
d1442e3747
1382 changed files with 170725 additions and 0 deletions
213
Managed/Decal.DecalDat/DatFile.cs
Normal file
213
Managed/Decal.DecalDat/DatFile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
Managed/Decal.DecalDat/DatLibraryImpl.cs
Normal file
159
Managed/Decal.DecalDat/DatLibraryImpl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
167
Managed/Decal.DecalDat/DatServiceImpl.cs
Normal file
167
Managed/Decal.DecalDat/DatServiceImpl.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Managed/Decal.DecalDat/DatStreamImpl.cs
Normal file
70
Managed/Decal.DecalDat/DatStreamImpl.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Managed/Decal.DecalDat/Decal.DecalDat.csproj
Normal file
10
Managed/Decal.DecalDat/Decal.DecalDat.csproj
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue