I am trying to write my own game engine. I packaged the files such as assets of the games so that the engine can read them faster and easier. However, I encountered a problem. I could not find the source of the problem. When I try to read data from the PAK file I created, there is a deviation in the data I read. I cannot read the data correctly.
File structure:
[Header]
[Index Table]
[Asset Data]
Codes:
namespace EngineCore
{
public class PakEntry
{
public string Name { get; set; }
public byte Type { get; set; }
public uint Offset { get; set; }
public uint Size { get; set; }
}
}
namespace EngineCore
{
public interface IPakReaderBackend
{
byte[] Read(PakEntry entry);
void Dispose();
}
}
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
namespace EngineCore
{
public class MemoryMappedBackend : IPakReaderBackend, IDisposable
{
private readonly MemoryMappedFile mmf;
private MemoryMappedViewAccessor accessor;
public MemoryMappedBackend(string path)
{
mmf = MemoryMappedFile.CreateFromFile(path, FileMode.Open);
}
public byte[] Read(PakEntry entry)
{
using var accessor = mmf.CreateViewAccessor(entry.Offset, entry.Size, MemoryMappedFileAccess.Read);
byte[] buffer = new byte[entry.Size];
accessor.ReadArray(0, buffer, 0, buffer.Length);
return buffer;
}
public void Dispose()
{
accessor?.Dispose();
mmf?.Dispose();
GC.SuppressFinalize(this);
}
}
}
using System.IO;
namespace EngineCore
{
public class SeekAndReadBackend : IPakReaderBackend
{
private readonly string pakFilePath;
public SeekAndReadBackend(string path)
{
pakFilePath = path;
}
public byte[] Read(PakEntry entry)
{
byte[] buffer = new byte[entry.Size];
using (FileStream fs = new FileStream(pakFilePath, FileMode.Open, FileAccess.Read))
{
fs.Seek(entry.Offset, SeekOrigin.Current);
fs.Read(buffer, 0, buffer.Length);
}
return buffer;
}
public void Dispose() { }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace EngineCore
{
public class PakWriter : IDisposable
{
private const string Magic = "TGPAK";
private const ushort Version = 1;
private readonly List<PakEntry> entries;
private readonly MemoryStream dataStream;
public PakWriter()
{
entries = new List<PakEntry>();
dataStream = new MemoryStream();
}
public void AddFile(string filePath, byte type)
{
byte[] fileData = File.ReadAllBytes(filePath);
uint offset = (uint)dataStream.Position;
dataStream.Write(fileData, 0, fileData.Length);
entries.Add(new PakEntry
{
Name = Path.GetFileName(filePath),
Type = type,
Offset = offset,
Size = (uint)fileData.Length
});
}
public void Save(string outputPath)
{
using (FileStream fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(Encoding.ASCII.GetBytes(Magic.PadRight(6, '\0')));
bw.Write(Version);
bw.Write((uint)entries.Count);
long indexOffsetPos = fs.Position;
bw.Write((uint)0);
dataStream.Seek(0, SeekOrigin.Begin);
dataStream.CopyTo(fs);
long indexOffset = fs.Position;
foreach (var entry in entries)
{
byte[] nameBytes = Encoding.UTF8.GetBytes(entry.Name);
bw.Write((byte)nameBytes.Length);
bw.Write(nameBytes);
bw.Write(entry.Type);
bw.Write(entry.Offset);
bw.Write(entry.Size);
}
fs.Seek(indexOffsetPos, SeekOrigin.Begin);
bw.Write((uint)indexOffset);
}
}
}
public void Dispose()
{
entries.Clear();
dataStream?.Dispose();
GC.SuppressFinalize(this);
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace EngineCore
{
public class PakReader : IDisposable
{
public List<PakEntry> Entries { get; private set; } = new List<PakEntry>();
private readonly IPakReaderBackend backend;
public PakReader(string filePath, bool useMemoryMapping = false)
{
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
using (BinaryReader br = new BinaryReader(fs))
{
string magic = Encoding.ASCII.GetString(br.ReadBytes(6)).TrimEnd('\0');
if (magic != "TGPAK") throw new Exception("Invalid pak");
ushort version = br.ReadUInt16();
uint entryCount = br.ReadUInt32();
uint indexOffset = br.ReadUInt32();
fs.Seek(indexOffset, SeekOrigin.Begin);
for (int i = 0; i < entryCount; i++)
{
byte nameLen = br.ReadByte();
string name = Encoding.UTF8.GetString(br.ReadBytes(nameLen));
byte type = br.ReadByte();
uint offset = br.ReadUInt32();
uint size = br.ReadUInt32();
Entries.Add(new PakEntry
{
Name = name,
Type = type,
Offset = offset,
Size = size
});
}
}
}
backend = useMemoryMapping ? new MemoryMappedBackend(filePath) : new SeekAndReadBackend(filePath);
}
public byte[] Read(string name)
{
PakEntry entry = Entries.FirstOrDefault(e => e.Name == name);
if (entry == null)
{
return null;
}
return backend.Read(entry);
}
public void Dispose()
{
backend.Dispose();
GC.SuppressFinalize(this);
}
}
}
Test
// Write
PakWriter pakW = new PakWriter();
pakW.AddFile("Assets/Test1.txt", 7);
pakW.AddFile("Assets/Test2.txt", 7);
pakW.AddFile("Assets/Player.fbx", 3);
pakW.AddFile("Assets/Wall.res", 5);
pakW.Save("Bin/data.pak");
pakW.Dispose();
// Read
PakReader pakR = new PakReader("Bin/data.pak");
Console.WriteLine("Assets:");
foreach (PakEntry entry in pakR.Entries)
{
Console.WriteLine($" - {entry.Name} (Type: {entry.Type}, Size: {entry.Size} bytes)");
}
byte[] data = pakR.Read("Test2.txt");
Console.WriteLine("Test2:" + Encoding.UTF8.GetString(data));
Console.ReadLine();
pakR.Dispose();
Output:
Assets:
- Test1.txt (Type: 7, Size: 20 bytes)
- Test2.txt (Type: 7, Size: 20 bytes)
- Player.fbx (Type: 3, Size: 759512 bytes)
- Wall.res (Type: 5, Size: 13544 bytes)
Test2:AAAAAAAAAAAAAAAABBBB