0

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
1
  • Please point out what the problem/deviation is, not just leaving behind a large amount of codes and an opaque output. And the file structure is unclear too, I couldn't find the keyword header in your code.
    – shingo
    Commented Apr 14 at 12:58

1 Answer 1

1

The issue is where you add your files.

        public void AddFile(string filePath, byte type)
        {
            byte[] fileData = File.ReadAllBytes(filePath);
            uint offset = (uint)dataStream.Position; // this is initially 0 as the header hasn't been written when addfile is called


            dataStream.Write(fileData, 0, fileData.Length);

            entries.Add(new PakEntry
            {
                Name = Path.GetFileName(filePath),
                Type = type,
                Offset = offset,  
                Size = (uint)fileData.Length
            });
        }

when you add your first file offset is 0, because the datastream hasn't written anything until you call save, so the file header itself isn't considered. But, when you write your file you are adding a bunch of fields to it that will change the offset:

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();

The easiest way to demonstrate this is to change your Save Method to consider the size of the header - please see below - note where I've put in comments

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

             var headerSize = (uint)fs.Position; 

             dataStream.Seek(0, SeekOrigin.Begin);
             dataStream.CopyTo(fs);

             long indexOffset = fs.Position; // *** this is the offset of where the data will start

             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 + indexOffset); // *** factor in the size of the header
                  bw.Write(entry.Size);
             }

             // go back and write our index position in
             fs.Seek(indexOffsetPos, SeekOrigin.Begin);
             bw.Write((uint)indexOffset);
         }
    }
}
1
  • You are the man! 👑 Thanks to you, I solved the problem. Commented Apr 14 at 19:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.