Steam VDF / KeyValues 格式的 Java 解析库。
面向 items_game.txt、csgo_english.txt 等 Valve 游戏配置文件:流式读取、树模型查询、#include / #base 宏展开,以及继承式的节点合并。
日常使用只需要和门面 Vdf 打交道——它持有不可变的 VdfConfig,每次 parse 时按配置组装三层解析管线,内部实现对外完全透明:
┌─────────────────────────────────────────────┐
│ Vdf(门面) │
│ └─ VdfConfig(纯数据:字符集/转义/缓冲/工厂) │
│ └─ 每次 parse(in) 现场组装 ↓ │
│ VdfScanner ─→ VdfTokenizer ─→ VdfParser │
│ (扫描字符) (解析词元) (语法+宏) │
└─────────────────────────────────────────────┘
VdfResourceResolver(宏资源来源)
Vdf:门面,parse(in)一招走天下;实例可复用(解析多个流)VdfConfig:不可变配置,通过 builder 构建,也可整体传入再逐项覆盖- 三层管线:都是接口,通过工厂注入即可替换任意一层(见扩展)
VdfResourceResolver:#include/#base引用的资源从哪读,默认文件系统,可换 classpath / 内存等
VdfVirtualNode root = Vdf.defaults().parse("items_game.txt");
// 查询: key = value
VdfNode name = root.getOne("name");
String text = name.asText();
int price = name.asInteger();
// 重复 key 的同名块, 合并成一个容器
VdfNode items = root.mergeOne("items");
// 不存在时静默返回空, 链式安全
boolean has = root.has("prefab");Vdf vdf = Vdf.builder()
.charset(StandardCharsets.UTF_8) // 输入字符集, 默认 UTF-8
.escapeSequences(true) // 开启引号内转义 (\n \t 等), 默认关闭
.bufferSize(2048) // 扫描器快照容量, 默认 1024
.build();
VdfVirtualNode root = vdf.parse("items_game.txt"); // 路径重载, 流由门面管理
VdfVirtualNode root2 = vdf.parse(inputStream); // 流重载, 同一实例可解析多个输入配置不可变;需要变更时从现有配置派生,未覆盖的项全部继承:
Vdf base = Vdf.builder().escapeSequences(true).bufferSize(2048).build();
Vdf lite = Vdf.builder().config(base.config()) // 整体传入
.bufferSize(64) // 只覆盖需要的项
.build();VdfResourceResolver 只有一个 open(path) 方法,相对路径基准由实现自行管理——默认的文件系统实现以"当前解析文件所在目录"为基准(嵌套 include 自动正确),顶层流以工作目录为基准:
Vdf.builder()
.resolver(new FileVdfResourceResolver(mainFileDir)) // 指定顶层基准目录
.build()
.parse(in);也可以自定义 resolver 从 classpath、内存等任意来源提供宏引用的内容:
VdfResourceResolver resolver = path -> ClassLoader.getSystemResourceAsStream(path);注意:resolver 实例非线程安全,同一实例不要并发解析多个文档。
// 深拷贝合并: 已有属性不被覆盖, 缺失属性从 parent 继承, 原树不受影响
VdfObjectNode merged = child.extend(parent);
// 自定义冲突策略: 叶子冲突时继承父类
child.extend(parent, VdfConflictStrategy.MERGE_INHERIT);
// 路径感知策略: 只在指定子树里采用父类属性
child.extend(parent, (ours, p, ctx) ->
"sub".equals(ctx.getPath()) ? VdfInheritDecision.INHERIT : VdfInheritDecision.OVERRIDE);扫描 / 词法 / 语法三层都是接口(VdfScanner / VdfTokenizer / VdfParser),通过工厂注入门面即可整体替换某一层;嵌套宏文件的解析会复用同一份配置和工厂:
VdfVirtualNode root = Vdf.builder()
.scannerFactory((in, size, charset) -> new MyScanner(in, size, charset))
.tokenizerFactory((scanner, escape) -> new MyTokenizer(scanner, escape))
.parserFactory((tokenizer, config) -> new MyParser(tokenizer, config))
.build()
.parse(in);三个工厂接口(cn.roylion.factory 包)都是 @FunctionalInterface,lambda 即工厂;不设置时使用默认的 Default* 实现。
以下示例可直接运行,测试类见 src/test/java/cn/roylion/demo/VdfDemoTest.java(数据文件在项目根目录 data/ 下)。
VdfNode itemsGame = Vdf.defaults().parse("items_game.txt").getOne("items_game");
// 数值转换
int firstValidClass = itemsGame.getOne("game_info").getOne("first_valid_class").asInteger();
// 物品表: 编号 -> 属性; 武器自身很薄, 详细属性通过 prefab 引用
VdfNode deagle = itemsGame.getOne("items").getOne("1");
deagle.getOne("name").asText(); // "weapon_deagle"
deagle.getOne("prefab").asText(); // "weapon_deagle_prefab"VdfNode enTokens = Vdf.defaults().parse("csgo_english.txt").getOne("lang").getOne("Tokens");
VdfNode zhTokens = Vdf.defaults().parse("csgo_schinese.txt").getOne("lang").getOne("Tokens");
enTokens.getOne("SFUI_WPNHUD_Pistol").asText(); // "Pistol"
zhTokens.getOne("SFUI_WPNHUD_Pistol").asText(); // "手枪"items_game 里的武器节点很薄,显示名等属性在 prefabs 块里——用 extend 继承补全,再拿 item_name 的 token 去本地化表翻译,就是游戏里真实的武器名解析流程:
VdfNode itemsGame = Vdf.defaults().parse("items_game.txt").getOne("items_game");
VdfObjectNode deagle = (VdfObjectNode) itemsGame.getOne("items").getOne("1");
VdfObjectNode prefab = (VdfObjectNode) itemsGame.getOne("prefabs")
.getOne(deagle.getOne("prefab").asText());
// 深拷贝继承: 原树不受影响
VdfObjectNode full = deagle.extend(prefab);
String token = full.getOne("item_name").asText().substring(1); // "SFUI_WPNHUD_DesertEagle"
enTokens.getOne(token).asText(); // "Desert Eagle"
zhTokens.getOne(token).asText(); // "沙漠之鹰"- 流式解析:基于
InputStream逐字符扫描(环形缓冲 + 快照回退),大文件不占双份内存;UTF-8 解码,支持指定字符集 - 完整词法:带引号/裸字符串、
//注释、转义序列(可选开启)、[$condition]条件标签、#宏 - 错误定位:解析失败抛出带
[行, 列]位置的异常,直接指向出错处 - 树模型查询:
get/getOne/mergeOne,重复 key 全部保留、按需取首个或合并 - 类型转换:
asText/asInteger/asLong/asDouble/asBoolean等按需转换 - 宏支持:
#include(追加)、#base(子类优先的递归合并,符合 Valve 语义) - 继承式合并:
extend深拷贝合并,支持自定义冲突策略(重写 / 继承 / 递归合并)和路径感知 - 条件表达式:
[$WIN32&&!$PS3]解析为可求值的组合子树(And / Or / Not) - 零第三方依赖,Java 8+
#include/#base默认按进程工作目录解析相对路径,需要以主文件为基准时请传入FileVdfResourceResolver(主文件目录)- 转义序列(
\n\t等)默认关闭,与 Valve 官方实现一致;但\"和\\始终识别——否则 value 里的转义引号会破坏分词(Valve 本地化文件大量使用) - UTF-8 BOM 自动跳过(Valve 本地化文件常见)
mvn test