You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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ğ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 11:12:04