CD Code Digest Hub /GTA V · v2

文件系统与 RPF:fiDevice、fiPackfile、fiStream、data 与压缩

33 分钟阅读 9 个章节 1 张图

RAGE 的所有文件访问都经过 fiDevice 抽象:路径前缀(挂载点)决定设备,设备可以是本地文件系统、RPF 包、内存块、开发 PC 上的远程文件服务(RFS)、安装/缓存分区,或其它设备的相对视图。本页说明设备抽象与挂载解析、RPF7 包格式与读取路径、fiStream 缓冲层、异步与缓存设备,以及 data 目录中的通用数据工具(位缓冲、增长缓冲、回调、序列化)和 zlib/LZ4 包装。加密只记录存在与位置。

背景与相关页面#

  • 资源(.y??)条目与 pgStreamer 的关系见 paging-and-resources.md。
  • 包文件的流式管理(strPackfileManager、挂载顺序、DLC)见 04-rage-framework/streaming-engine.md;游戏侧 CFileMgr、安装、平台存储见 22-input-platform/platform-services-and-settings.md。
  • 存档设备 fiSaveGame 的平台实现见 19-persistence-replay/save-load.md。
  • 云文件设备(fiDeviceCloud,在 rline/cloud)见 16-network-online/rline-online-services.md。
  • 文件完整性 / 加密 / 防篡改的架构位置见 23-debug-security/security-overview.md。

相关源码文件#

区域文件(相对 src/dev_ng/rage/base/src/)
设备抽象与挂载file/device.h、file/device_common.cpp、file/device_win32.cpp、file/device_psn.cpp
RPFfile/packfile.h/.cpp、file/packfile_builder.*
派生设备file/device_relative.h、file/device_installer.h/.psc、file/device_crc.h、file/diskcache.h、file/cachepartition.h、file/remote.h/.cpp、file/tcpip.h、file/zipfile.h、file/embedded.h
流与资产路径file/stream.h/.cpp、file/streambuffers.cpp、file/asset.h、file/asynchstream.h、file/token.h
压缩file/compress.h、file/decompress.h、zlib/*(zlib 1.2.3、lz4 1.3.0、inflateClient.h)、system/compressionfactory.h
datadata/base.h、data/callback.h、data/bitbuffer.h、data/growbuffer.h、data/serialize.h、data/rson.h、data/compress.h、data/aes.h 等

1. fiDevice 抽象#

1.1 接口#

fiDevice 是纯虚接口:Open/Create/Read/Write/Seek/Seek64/Close 等流式操作,OpenBulk/ReadBulk/CloseBulk(按绝对偏移的批量读,流式系统使用)、GetFileTime/SetFileTime/GetAttributes/FindFileBegin...、GetRootDeviceId(OPTICAL/HARDDRIVE/USB/NETWORK/MEMORY,用于排序与设备队列选择)、GetPhysicalSortKey(光盘 LSN,硬盘为 HARDDRIVE_LSN = 0x40000000)、IsRpf/GetRpfDevice/GetPackfileIndex、IsCloud 等 src/dev_ng/rage/base/src/file/device.h:78-247、src/dev_ng/rage/base/src/file/device.h:600-631。读错误多次重试后调用 sm_FatalReadError 回调(脏盘提示);非 Final 可用 sm_InjectReadError 注入读错误 src/dev_ng/rage/base/src/file/device.h:101-111。

1.2 设备实现一览#

设备作用证据
fiDeviceLocal / fiDeviceLocalDrm本地文件系统(PS3 有 DRM 变体)src/dev_ng/rage/base/src/file/device.h:661-716
fiDeviceRemoteRFS:经 TCP 访问开发 PC 文件,另提供远程消息框、TTY、exec、环境变量;__ENABLE_RFS = !__FINAL || __FINAL_LOGGINGsrc/dev_ng/rage/base/src/file/device.h:735-760、src/dev_ng/rage/base/src/file/remote.h:20-74
fiDeviceMemorymemory:$<ptr>,<size>,<freeOnClose>:<comment> 形式的文件名直接映射到内存块src/dev_ng/rage/base/src/file/device_common.cpp:1486-1489
fiDeviceGrowBuffer / fiDeviceCount / fiDeviceConsole写入 datGrowBuffer、只计数字节、写控制台src/dev_ng/rage/base/src/file/device.h:830-876
fiDeviceTcpIptcpip: 套接字流(Win32/PPU)src/dev_ng/rage/base/src/file/tcpip.h:13
fiPackfile(fiCollection 派生)RPF 包(见第 2 节)src/dev_ng/rage/base/src/file/packfile.h:106-157
fiDeviceRelative把挂载点映射到另一路径/设备下的子树,如 common:/、platform:/、update:/src/dev_ng/rage/base/src/file/device_relative.h:20-60
fiDeviceCrcfiDeviceRelative 派生,在加载时计算 CRC 并维护文件名→CRC 表(属完整性机制,细节不展开)src/dev_ng/rage/base/src/file/device_crc.h:17-40
fiDeviceInstaller从光盘运行时先指向盘上分区 RPF,安装到硬盘后切换到硬盘副本;配置 InitData 由 .psc 反射(m_sourceFile/m_partitionName/m_mount/m_platform/m_cached/m_readOnly)src/dev_ng/rage/base/src/file/device_installer.h:69-130、src/dev_ng/rage/base/src/file/device_installer.psc:4-18
fiCachedDevice / fiDiskCache360/PS3 时代把光盘数据块缓存到硬盘的设备包装src/dev_ng/rage/base/src/file/diskcache.h:21-80
fiCachePartition / fiPersistentCachePartition平台缓存分区挂载与管理src/dev_ng/rage/base/src/file/cachepartition.h:30-70
fiZipfile、fiEmbeddedFile标准 zip 读取、内嵌文件src/dev_ng/rage/base/src/file/zipfile.h:41、src/dev_ng/rage/base/src/file/embedded.h:16

1.3 挂载表与设备解析#

fiDevice::GetDevice(filename, readOnly) → GetDeviceImpl:

  1. 特殊前缀直达:memory:、count:、growbuffer:、console:(非 __NO_OUTPUT)、tcpip:;以 dlc 开头的路径走“额外挂载”表(atBinaryMap,一个挂载点可挂多个设备)。
  2. 平台原生路径直达本地设备:Xenon game:/cache:/dvd:,PS3 /dev_*、/app_home/,Orbis /app0/、/savedata、/addcont、/download 等,Durango g:、t:、globalroot(非 Final 还有 xg:/xd:)。
  3. 否则在挂载表中按最长前缀匹配(mountcmp 大小写不敏感、\ 视为 /);同一挂载点有多个设备时从最后挂载的开始找存在该文件的设备(非 Final 可选“选最新时间戳”)。
  4. 写访问时若挂载为只读则不返回该设备。

证据:src/dev_ng/rage/base/src/file/device_common.cpp:305-500、src/dev_ng/rage/base/src/file/device_common.cpp:96-107、src/dev_ng/rage/base/src/file/device_common.cpp:156-159。

容量:常规挂载默认上限 sm_MaxMounts = 16,额外挂载 MAX_EXTRA_MOUNTS = 256;挂载名必须以 / 结尾,超过上限直接 Quitf(ERR_FIL_COMMON) src/dev_ng/rage/base/src/file/device_common.cpp:55-88、src/dev_ng/rage/base/src/file/device.h:30、src/dev_ng/rage/base/src/file/device_common.cpp:595-639。游戏在 CSystem 初始化中把上限改为 MAX_ARCHIVES_MOUNTED = 100 与 MAX_EXTRA_MOUNTS src/dev_ng/game/system/system.cpp:463-467、src/dev_ng/rage/framework/src/streaming/streamingdefs.h:134。 fiDevice::InitClass 在 HACK_GTA4 下按 -update 或内置默认把 update/ 目录挂为 update:/(相对设备) src/dev_ng/rage/base/src/file/device_common.cpp:109-149。

1.4 fiAssetManager(ASSET)#

全局 ASSET 维护最多 4 个搜索根(SetPath)与文件夹栈(PushFolder/PopFolder,游戏 8 层、工具 16 层),提供 FullReadPath/FullWritePath/Open/Create/Exists/AddExtension 等,把相对名解析成带设备前缀的完整路径 src/dev_ng/rage/base/src/file/asset.h:25-168、src/dev_ng/rage/base/src/file/asset.h:293-320。资源加载 pgRscBuilder::ConstructName 与 parser 的 LoadObject(file, ext) 都经由它。


2. RPF7 包(fiPackfile)#

2.1 格式#

源码注释给出的布局:

  1. 16 字节头 fiPackHeader(目标平台字节序、不加密):m_Magic = 'RPF7'、m_EntryCount、m_NameHeapSize、m_Encrypted(加密密钥 ID)。魔数的字节序标识创建平台。
  2. 主目录:m_EntryCount × 16 字节的 fiPackEntry 数组,随后是名称堆;两者都加密。
  3. 数据体:从目录与名称堆之后的下一个 512 字节边界开始;所有文件按 512 字节对齐;整个包以 2 KB 对齐结束;最后一个文件必须在 0xFFFFFE00 之前开始,因此包最大约 4 GB。

证据:src/dev_ng/rage/base/src/file/packfile.cpp:39-54、src/dev_ng/rage/base/src/file/packfile.h:28-33。

m_NameHeapSize 的高位被复用:bit31 表示用 XCompress 而非 zlib,bits 28–30 为 m_NameShift(名称偏移的左移位数),低 28 位才是名称堆大小 src/dev_ng/rage/base/src/file/packfile.cpp:476-478。魔数不匹配时尝试字节交换后再判断(跨平台工具场景) src/dev_ng/rage/base/src/file/packfile.cpp:456-470。

2.2 条目编码 fiPackEntry(16 字节)#

字段位宽含义
m_NameOffset16名称堆偏移,实际偏移 << m_NameShift
m_ConsumedSize24压缩后大小;0 表示未压缩文件或目录;资源总是压缩。对 ≥ MaxConsumedSize (0xFFFFFF) 的超大资源(仅 PC),此字段填满值,真实长度藏在资源前 16 个(通常未用的)字节中
m_FileOffset23文件偏移 >> 9;等于 FileOffset_IsDir ((1<<23)-1) 表示目录
m_IsResource1是否资源
联合体(8 字节)—文件:m_UncompressedSize + m_Encrypted(密钥 ID 或 0);目录:m_DirectoryIndex + m_DirectoryCount;资源:datResourceInfo m_Info(虚拟/物理段描述与 8 位版本)

证据:src/dev_ng/rage/base/src/file/packfile.h:35-99。由此:未压缩文件最大 4 GB,压缩文件与资源最大 16 MB;资源版本 GetVersion() 来自 m_Info(见 paging-and-resources.md)。

2.3 目录查找#

条目 0 是根目录;每个目录的子条目在数组中连续且按名称排序。FindEntry(name) 把路径逐段转小写,在当前目录的 [DirectoryIndex, DirectoryIndex+Count) 区间内对名称堆字符串做二分 strcmp,逐级下降 src/dev_ng/rage/base/src/file/packfile.cpp:881-925。父索引表 m_ParentIndices(u16)附加在名称堆分配之后,用于由条目反推完整路径 src/dev_ng/rage/base/src/file/packfile.cpp:579-612、src/dev_ng/rage/base/src/file/packfile.cpp:1198-1207。名称堆可选择不保留(keepNameTable),此时 GetEntryName 返回 "<rpf>:hash_XXXXXXXX" 形式的占位名 src/dev_ng/rage/base/src/file/packfile.cpp:1188-1196。

2.4 初始化与读取#

  • Init(filename, keepNameTable, cacheMode, data):通过底层设备 OpenBulkDrm 打开包本体(嵌套包用 m_ArchiveBias 记录在父包中的偏移);CACHE_MEMORY 时整包读入内存并改用 memory: 设备;位于硬盘且 CACHE_NONE 时提升为 CACHE_INSTALLER_FILE src/dev_ng/rage/base/src/file/packfile.cpp:284-330、src/dev_ng/rage/base/src/file/packfile.h:164-176。
  • 头部加密时按密钥 ID 解密条目表与名称堆(存在面向不同平台的多密钥/TransformIT 分支;本页不记录算法与密钥) src/dev_ng/rage/base/src/file/packfile.cpp:496-535、src/dev_ng/rage/base/src/file/packfile.cpp:593-594。
  • Open 只能打开普通文件(不能写、不能打开目录或资源);压缩文件为每个打开句柄建立 raw-inflate(inflateInit2(-MAX_WBITS))或 XCompress 上下文;同时打开的包内文件数受静态池 s_States 限制(4 或 16),耗尽时列出已打开文件并 Quitf(ERR_FIL_PACK_2) src/dev_ng/rage/base/src/file/packfile.cpp:719-782。
  • ReadInternal 按需读取压缩块、(若条目加密)解密,再 inflate(Z_SYNC_FLUSH);Seek 向后时需 inflateReset 并重新解压 src/dev_ng/rage/base/src/file/packfile.cpp:967-1115。
  • 资源条目不走 Open:GetResourceInfo(name) 返回条目中的 datResourceInfo 与版本;OpenBulk 返回包句柄与文件偏移,资源数据由 pgStreamer 以 bulk 读取并在流式线程中解压 src/dev_ng/rage/base/src/file/packfile.cpp:1165-1185。

2.5 包索引与流式句柄#

fiCollection 维护全局表 sm_Collections[MaxPackfiles](0 号保留给 pgRawStreamer);MaxPackfiles 在源码中以历次 DLC 递增的和式定义(1024+718+650+256×5 = 3672)。流式句柄是 (archiveIndex << 16) | entryIndex,因此单包最多 65536 条目 src/dev_ng/rage/base/src/file/packfile.cpp:59-64、src/dev_ng/rage/base/src/file/packfile.h:106-137。 pgStreamer::Open 即用此句柄定位条目并校验资源版本(见 paging-and-resources.md 第 5 节)。

图证据:设备解析 src/dev_ng/rage/base/src/file/device_common.cpp:329-500;包内普通文件 src/dev_ng/rage/base/src/file/packfile.cpp:726-782;资源 src/dev_ng/rage/base/src/file/packfile.cpp:1165-1185;流式 src/dev_ng/rage/base/src/paging/streamer.cpp:2074-2117。


3. fiStream 与异步流#

  • fiStream 是对“设备 + 句柄”的带缓冲包装:静态对象池 sm_Streams[] 与每流 4 KB 缓冲 sm_Buffers[][4096](128 字节对齐),m_Offset/m_Length/m_Size 记录缓冲状态;提供 Read/Write/Flush/Seek/Size、FastGetCh/FastPutCh 以及 fread/fwrite/fprintf/fgets 风格的自由函数 src/dev_ng/rage/base/src/file/stream.h:46-347。
  • 池大小由 DECLARE_STREAM_BUFFERS(count) 决定:Durango/Orbis 32、其他主机 __DEV 20 / 否则 16、PC 64;耗尽时 DumpOpenFiles 并 Quitf(ERR_SYS_NOFILEHANDLES_1) src/dev_ng/rage/base/src/file/streambuffers.cpp:17-28、src/dev_ng/rage/base/src/file/stream.cpp:103-133。
  • fiSafeStream 为 RAII 包装;AllowBlockingLoads 可在运行期禁止阻塞加载(用于捕获主线程意外同步 IO) src/dev_ng/rage/base/src/file/stream.h:293-296、src/dev_ng/rage/base/src/file/stream.h:363-378。
  • fiAsynchStream:按 ASSET.Open 语义异步把文件读进内存,完成后提供 memory: 文件名以便用普通 fiStream 打开(使用 RPF 时保证不阻塞) src/dev_ng/rage/base/src/file/asynchstream.h:15-40。
  • fiTokenizer(file/token.h)是文本分词器,供旧式文本格式与 XmlLog 使用。

4. data:通用数据工具#

组件职责证据
datBase只有虚析构的基类,datCallback 成员回调要求目标类派生自它src/dev_ng/rage/base/src/data/base.h:15-37
datCallback统一封装 0/1/2 参数的静态函数或 datBase 成员函数回调(CFA/MFA 宏),bank 控件回调使用;Win32 上补齐 4 字节使各平台大小一致(以便放入资源)src/dev_ng/rage/base/src/data/callback.h:20-110
datBitBuffer位级读写缓冲(网络同步序列化的基础);位 0 是首字节的 MSB;所有实际读写只经 ReadUnsigned/WriteUnsigned/CopyBits 三个静态函数,便于实现只计数的“空缓冲”src/dev_ng/rage/base/src/data/bitbuffer.h:7-20、src/dev_ng/rage/base/src/data/bitbuffer.h:180-260;用法见 17-network-sync/sync-trees-and-serialisation.md
datGrowBuffer可增长内存缓冲(NULL_BUFFER 只算尺寸、FIXED_BUFFER、NULL_TERMINATE、READ_ONLY;默认增长步长 256,必须为 2 的幂)src/dev_ng/rage/base/src/data/growbuffer.h:17-60
datSerialize / datLabel / datString旧式二进制/文本序列化接口(fiSerialize 为文件实现)src/dev_ng/rage/base/src/data/serialize.h:22-257、src/dev_ng/rage/base/src/file/serialize.h:18
Rson* / Brson*JSON 风格读写器(在线服务数据交换)src/dev_ng/rage/base/src/data/rson.h:58-924
datSaxReaderSAX 风格 XML 读取src/dev_ng/rage/base/src/data/sax.h:15
AutoIdDescriptor 等自动 ID 注册(Final 中剥离名字)src/dev_ng/rage/base/src/data/autoid.h:21-62
marker.hRAGE_FUNC 等函数级标记(RAGE_ENABLE_MARKERS = __DEV)src/dev_ng/rage/base/src/data/marker.h:19-35
datCompress/datDecompress滑动窗口压缩(datCompressionData 注册到 sysCompressionFactory,供 RAG 通信等)src/dev_ng/rage/base/src/data/compress.h:29-56、src/dev_ng/rage/base/src/data/datcompressiondata.h:17
资源辅助resource.h/resourceheader.h/struct.h(见 paging-and-resources.md)—
安全相关aes.*(存在密钥 ID 常量与 TransformIT 开关)、sha1、pbkdf2、certificateverify、datprotect、TFIT_keys_*、multi_keys_*:仅记录存在,不展开src/dev_ng/rage/base/src/data/aes.h:14-39、src/dev_ng/rage/base/src/data/datprotect.h:22、src/dev_ng/rage/base/src/data/certificateverify.h:22-43

5. 压缩库与包装#

组件说明证据
zlib 1.2.3资源与 RPF 压缩文件均为 raw deflate(-MAX_WBITS)src/dev_ng/rage/base/src/zlib/zlib.h:47、src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1388、src/dev_ng/rage/base/src/file/packfile.cpp:758
zlibInflater流式系统专用解压抽象:PS3 走 SPURS 任务(inflateServer.task)、Xenon 走 XCompress、其他平台直接 zlib;还提供(PS3 上经 SPU 的)显存拷贝src/dev_ng/rage/base/src/zlib/inflateClient.h:6-80
LZ4 1.3.0游戏在存档缓冲、存档迁移、回放文件、商店目录、玩家卡等处使用(lz4hc 源文件存在但未列入 makefile)src/dev_ng/rage/base/src/zlib/lz4.h:44-46、zlib/makefile.txt;使用者由 rg LZ4_compress 得出(如 game/control/replay/File/ReplayFileManager.cpp、game/SaveLoad/SavegameBuffers/savegame_buffer.cpp)
fiCompress/fiDecompressRAGE 自有滑动窗口压缩(lookback 可调),解压保证对畸形输入不崩溃src/dev_ng/rage/base/src/file/compress.h:11-26、src/dev_ng/rage/base/src/file/decompress.h:11-18
sysCompressionFactory把多段不同压缩格式的数据串成一个流,与 C# ragCompression 镜像,作为游戏与 RAG 的接口src/dev_ng/rage/base/src/system/compressionfactory.h:15-27

约束与边界行为#

  • 挂载点名必须以 / 结尾;常规挂载数超过上限直接退出进程。dlc* 路径总是走额外挂载表,与常规表不混用。
  • 包内文件只读;同时打开的包内压缩文件句柄数很小(4/16),长期持有 fiStream 会耗尽描述符。
  • 压缩文件/资源最大 16 MB(PC 超大资源例外);包最大约 4 GB;单包最多 65536 条目;包数量上限 3672。
  • 包内压缩文件不支持高效随机访问:向后 Seek 需要从头重新解压。
  • 未保留名称堆的包无法按名字枚举/反查(只剩哈希占位名)。
  • fiStream 池全局共享且有限,耗尽是致命错误。

未决问题 / 证据不足#

  • packfile_builder / zip2rpf 打包器的写出细节、超大资源“隐藏长度”的编码方式未展开(后者涉及加密/混淆,按安全边界有意略过)。
  • fiDeviceRemote(remote.cpp)完整协议帧格式、fiDeviceInstaller 安装状态机、fiDiskCache 的块管理只做了接口级确认。
  • 游戏启动时实际的挂载序列(common:/、platform:/、update:/、dlcpacks 等)由游戏/framework 代码决定,本页未追踪;见 22-input-platform/platform-services-and-settings.md 与 04-rage-framework/streaming-engine.md。