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; } } }