using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
namespace Decal.DecalDat
{
///
/// Low-level reader for Asheron's Call DAT archive files.
/// DAT files use a sector-based linked-list format with a hierarchical directory.
///
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;
}
}
///
/// Represents a single file within a DAT archive.
/// Reads through a linked list of sectors.
///
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;
}
}
}