CD Code Digest Hub /DOOM 3

虚拟文件系统、资源容器与声明

10 分钟阅读 7 个章节 1 张图

本页追踪一个逻辑资源名如何穿过搜索路径和 .resources 容器,随后如何被声明管理器扫描、惰性解析并供材质、声音或实体使用。下游机制见材质与图像、实体与地图和音频。

关键源码#

  • neo/framework/FileSystem.cpp:路径、容器查找、读写和关卡资源生命周期。
  • neo/framework/File_Resource.*:.resources 的索引、内部文件与构建工具。
  • neo/framework/DeclManager.cpp:声明类型注册、文件扫描、默认化和重载。
  • neo/idlib/Lexer.* / Parser.*:声明文本的词法与语法基础。

启动时建立搜索空间#

文件系统在一般命令行 CVar 处理之前单独读取 fs_basepath、fs_savepath、fs_game、fs_game_base 和 fs_copyfiles;未指定的基础/保存路径由 Sys 默认值补齐。Startup 先设置 BASE_GAMEDIR,再按条件叠加两个 mod 路径。neo/framework/FileSystem.cpp:2594-2613 neo/framework/FileSystem.cpp:2545-2563

SetupGameDirectories 对每个游戏目录同时添加 basepath 和 savepath。AddGameDirectory 保存搜索路径,并扫描该目录下排序后的 .resources 文件;每个有效容器被追加到 resourceFiles。neo/framework/FileSystem.cpp:2433-2467 neo/framework/FileSystem.cpp:2477-2485

初始化最后尝试读取 default.cfg,读不到就触发致命错误。这份源码不能作为“零资源专用服务器”直接运行,启动资源仍需实际安装数据支持。neo/framework/FileSystem.cpp:2612-2621 README.txt:15-18

一次 OpenFileRead 的查找顺序#

图依据:资源优先和目录优先的两个阶段位于 FileSystem.cpp:2813-2833,目录未命中后的资源回退位于 FileSystem.cpp:2922-2937。fs_resourceLoadPriority 默认是 1。neo/framework/FileSystem.cpp:288-290

读取前会去掉一个开头斜杠,并拒绝包含 .. 或 :: 的相对路径;空路径同样失败。目录搜索从 searchPaths 尾部向前,资源索引查找也从容器尾部向前,因此后加入的路径/容器具有覆盖机会。neo/framework/FileSystem.cpp:2780-2807 neo/framework/FileSystem.cpp:2821-2844 neo/framework/FileSystem.cpp:2688-2715

ReadFile 是更高层的所有权包装:找不到时返回 -1;只查询长度时关闭文件并返回长度;要求缓冲时分配 len+1 字节、读入并补 NUL。调用方之后用 FreeFile 释放该缓冲,而不是直接 delete 文件对象或 CRT free。neo/framework/FileSystem.cpp:1653-1692 neo/framework/FileSystem.cpp:1700-1709

.resources 是带索引的容器#

容器以 0xD000000D magic 开头,后接索引表偏移和长度。初始化时读取整个表,每项包含规范化文件名、数据 offset、length 和所属容器索引,并建立哈希索引。neo/framework/File_Resource.h:48-70 neo/framework/File_Resource.cpp:55-110

命中后,文件系统先构造引用原容器区段的 idFile_InnerResource;满足内存读取、关卡预分配缓冲或小于 8 MiB 等条件时,再把内容读入内存并返回内存文件。调用方看到的是统一 idFile 接口,而不必知道 backing store 是磁盘、容器区段还是内存。neo/framework/FileSystem.cpp:2726-2763

BeginLevelLoad 会记录 manifest 名、清空本关文件/预载列表并尝试加入 maps/<name>.resources;EndLevelLoad 在 fs_buildresources 开启时输出 manifest 和 preload 信息。这把“实际触碰了哪些资源”与资源打包流程连接起来,但仓库中的构建开关默认是 0,正常游戏不会每次启动都重写资源包。neo/framework/FileSystem.cpp:514-538 neo/framework/FileSystem.cpp:574-590 neo/framework/FileSystem.cpp:283-290

声明的两阶段注册#

DeclManager::Init 把类型枚举映射到具体分配器:material、skin、sound、entityDef/mapDef、FX、particle、AF、PDA、email、video 和 audio 等;同时注册 materials/*.mtr 目录。Init2 再注册 skins/*.skin 和 sound/*.sndshd。neo/framework/DeclManager.cpp:802-832 neo/framework/DeclManager.cpp:877-880

RegisterDeclFolder 通过文件系统枚举目录,把每个文件包装为 idDeclFile 并调用 LoadAndParse。文件级扫描用 idLexer 找出各个声明和文本范围,同时记录时间戳、校验和及重载状态;真正的类型特定解析仍可推迟。neo/framework/DeclManager.cpp:992-1037 neo/framework/DeclManager.cpp:613-646

FindType 是惰性解析边界#

FindType 在互斥区内找到或创建声明元数据,通过该类型 allocator 分配 self。如果状态为 DS_UNPARSED,它要求当前是主线程,再调用 ParseLocal;后台游戏线程首次加载声明会报错,源码特别指出 OpenGL 图像加载需要主线程解析。neo/framework/DeclManager.cpp:1130-1164

ParseLocal 先释放旧类型数据;没有源文本时尝试生成默认文本,仍无文本则 MakeDefault。有文本时先标记 DS_PARSED 再调用具体类型的 Parse,但管理器没有检查其返回值并在这里统一默认化。neo/framework/DeclManager.cpp:2365-2404 MakeDefault 把状态设为 DS_DEFAULTED,解析类型提供的 DefaultDefinition,并用递归层级防止坏默认定义无限递归。neo/framework/DeclManager.cpp:2252-2278

这形成三类重要边界行为:

场景结果修改时的注意点
首次查找主线程惰性解析并标记引用不要在 Game/Draw 线程引入未预解析资源
无源或默认文本生成后仍无源走 MakeDefault 并标记 DS_DEFAULTED检查 GetState,区分默认资源与正常解析资源
有源的类型解析管理器先标 DS_PARSED,未用 Parse 返回值统一回退非空指针不证明类型数据有效;失败语义需继续查具体 Decl
新关卡开始可清理的声明默认化并回到 DS_UNPARSED旧指针会表现得像默认声明,应重新 FindType

前两项依据见上段;关卡开始时 BeginLevelLoad 对声明执行 Purge,而 Purge 保留关卡外引用,其他声明默认化后设为未解析。neo/framework/DeclManager.cpp:937-948 neo/framework/DeclManager.cpp:2412-2423 源码文件头也明确警告:对 purged decl 再次 FindType 会重载,但旧指针看起来像默认声明。neo/framework/DeclManager.cpp:35-44

修改资源时的实用检查#

  • 找不到文件:先用 path/fs_debug 查实际搜索顺序,再区分目录优先级与容器优先级。
  • 开发时覆盖容器资源:根据需求设置 fs_resourceLoadPriority=0,让普通目录先命中;该开关默认值的语义见上。
  • 新增声明类型:同时提供枚举、allocator 注册、目录/扩展注册和类型特定 Parse/DefaultDefinition,只添加数据文件不会建立运行时装配。
  • 新增后台加载:预先在主线程 FindType,或重新设计资源上传边界;不要绕过 FindType 的线程检查。
  • 跨关卡缓存声明指针:改为持有名称并在需要时重新查找,或证明该对象满足关卡外引用条件。

返回总入口。