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>
213 lines
7.1 KiB
C#
213 lines
7.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|