使用C#读取自定义PAK文件时数据读取错误问题排查
自定义PAK文件读取数据偏差问题排查
自研游戏引擎中,将游戏资产打包为自定义PAK文件后,读取时出现数据偏差,无法正确获取内容。PAK文件结构定义如下:
[Header] [Index Table] [Asset Data]
相关代码
PakEntry 定义
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; } } }
IPakReaderBackend 接口
namespace EngineCore { public interface IPakReaderBackend { byte[] Read(PakEntry entry); void Dispose(); } }
MemoryMappedBackend 实现
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); } } }
SeekAndReadBackend 实现
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() { } } }
PakWriter 实现
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); } } }
PakReader 实现
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); } } }
测试代码
// 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();
输出结果
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. 资产偏移计算错误
PakWriter中记录的Offset是基于dataStream的相对位置,但实际PAK文件里,dataStream的内容是在Header之后写入的。Header的固定长度为16字节(6字节Magic + 2字节Version +4字节EntryCount +4字节IndexOffset占位符),因此资产在PAK文件中的真实偏移应该是Header长度 + dataStream中的相对偏移,而非直接使用dataStream的位置。这会导致读取时偏移量比实际位置少16字节,读取到错误的内容。
2. Seek原点使用错误
SeekAndReadBackend.Read方法中使用SeekOrigin.Current作为Seek的原点,虽然每次打开FileStream时当前位置默认是文件开头,单次调用结果看似正确,但资产偏移是相对于文件开头的,规范用法应该是SeekOrigin.Begin,否则如果流的当前位置被修改,会导致读取完全错误的位置。
3. MemoryMappedBackend冗余字段
MemoryMappedBackend类中声明的accessor字段未被使用,Read方法中重新声明了局部变量accessor,导致Dispose方法中的accessor?.Dispose()无效,属于代码冗余,但不影响读取功能。
修复方案
修复PakWriter的偏移计算
修改PakWriter.Save方法,在写入索引表时修正每个entry的Offset:
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); // 记录Asset Data的起始偏移位置 long assetDataStartOffset = fs.Position; 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); // 修正偏移:Asset Data起始位置 + entry在dataStream中的相对偏移 bw.Write((uint)(assetDataStartOffset + entry.Offset)); bw.Write(entry.Size); } fs.Seek(indexOffsetPos, SeekOrigin.Begin); bw.Write((uint)indexOffset); } } }
修复SeekAndReadBackend的Seek原点
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.Begin); // 改为以文件开头为原点 fs.Read(buffer, 0, buffer.Length); } return buffer; }
清理MemoryMappedBackend的冗余字段
移除未使用的accessor字段:
public class MemoryMappedBackend : IPakReaderBackend, IDisposable { private readonly MemoryMappedFile mmf; 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() { mmf?.Dispose(); GC.SuppressFinalize(this); } }
内容的提问来源于stack exchange,提问作者Koray Üstündağ
相关产品推荐
相关产品推荐

