64个版本 (25个破坏性版本)
0.26.0 | 2024年6月16日 |
---|---|
0.25.5 | 2024年2月28日 |
0.25.2 | 2023年12月28日 |
0.24.0 | 2023年11月22日 |
0.8.0 | 2020年10月29日 |
#18 in 游戏开发
548每月下载量
用于 5 个库(4 个直接使用)
700KB
17K SLoC
Jomini
针对来自Paradox Development Studio(例如:欧洲全史(EU4)、铁血雄心(HOI4)、十字军之王(CK3)、Imperator、Stellaris和Victoria)游戏标题的保存和游戏文件的底层、性能导向解析器。
要深入了解Paradox Clausewitz格式和尝试支持所有变体可能带来的陷阱,请参阅这篇博文。简而言之,编写一个既灵活又易用的解析器,同时抽象出游戏格式差异和游戏补丁之间的差异,是非常困难的。Jomini在灵活性和易用性之间找到了一个完美的平衡点。
Jomini是在线EU4保存文件分析器的基石。此库还支持Paradox游戏转换器和pdxu。
特性
- ✔ 多功能:处理纯文本和二进制编码的数据
- ✔ 快速:以超过1 GB/s的速度解析数据
- ✔ 小巧:编译时无依赖项
- ✔ 安全:广泛地对潜在的恶意输入进行模糊测试
- ✔ 易用:使用类似serde的宏来自动实现解析逻辑
- ✔ 可嵌入:跨平台本地应用程序、静态编译的服务或通过Wasm在浏览器中使用
快速入门
以下是一个使用serde反序列化纯文本数据的示例。还使用了几个额外的类似serde的属性,以将serde数据模型与这些文件的结构相协调。
use jomini::{
text::{Operator, Property},
JominiDeserialize,
};
#[derive(JominiDeserialize, PartialEq, Debug)]
pub struct Model {
human: bool,
first: Option<u16>,
third: Property<u16>,
#[jomini(alias = "forth")]
fourth: u16,
#[jomini(alias = "core", duplicated)]
cores: Vec<String>,
names: Vec<String>,
#[jomini(take_last)]
checksum: String,
}
let data = br#"
human = yes
third < 5
forth = 10
core = "HAB"
names = { "Johan" "Frederick" }
core = FRA
checksum = "first"
checksum = "second"
"#;
let expected = Model {
human: true,
first: None,
third: Property::new(Operator::LessThan, 5),
fourth: 10,
cores: vec!["HAB".to_string(), "FRA".to_string()],
names: vec!["Johan".to_string(), "Frederick".to_string()],
checksum: "second".to_string(),
};
let actual: Model = jomini::text::de::from_windows1252_slice(data)?;
assert_eq!(actual, expected);
二进制反序列化
以类似的方式反序列化二进制格式编码的数据,但需要调用者提供一些额外的步骤
- 文本应如何解码(通常是Windows-1252或UTF-8)
- 如何解码有理数(浮点数)
- 如何解析令牌,令牌是唯一标识字符串的16位整数
请注意,每个Paradox游戏都有不同的二进制格式,而且二进制格式可能在补丁之间有所不同!
以下是一个定义示例二进制格式并使用哈希表令牌查找的示例。
use jomini::{Encoding, JominiDeserialize, Windows1252Encoding, binary::BinaryFlavor};
use std::{borrow::Cow, collections::HashMap};
#[derive(JominiDeserialize, PartialEq, Debug)]
struct MyStruct {
field1: String,
}
#[derive(Debug, Default)]
pub struct BinaryTestFlavor;
impl jomini::binary::BinaryFlavor for BinaryTestFlavor {
fn visit_f32(&self, data: [u8; 4]) -> f32 {
f32::from_le_bytes(data)
}
fn visit_f64(&self, data: [u8; 8]) -> f64 {
f64::from_le_bytes(data)
}
}
impl Encoding for BinaryTestFlavor {
fn decode<'a>(&self, data: &'a [u8]) -> Cow<'a, str> {
Windows1252Encoding::decode(data)
}
}
let data = [ 0x82, 0x2d, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x00, 0x45, 0x4e, 0x47 ];
let mut map = HashMap::new();
map.insert(0x2d82, "field1");
let actual: MyStruct = BinaryTestFlavor.deserialize_slice(&data[..], &map)?;
assert_eq!(actual, MyStruct { field1: "ENG".to_string() });
正确实现时,可以使用相同的结构来表示明文和二进制数据,无需任何重复。
可以配置当遇到未知令牌时的行为(例如:立即失败或尝试继续)。
使用 token
属性进行直接标识符反序列化
在二进制反序列化过程中,由于令牌通过 TokenResolver
转换为字符串,然后与结构体字段的字符串表示进行匹配,可能会出现一些性能损失。
我们可以通过直接将期望的令牌值编码到结构体中解决这个问题
#[derive(JominiDeserialize, PartialEq, Debug)]
struct MyStruct {
#[jomini(token = 0x2d82)]
field1: String,
}
// Empty token to string resolver
let map = HashMap::<u16, String>::new();
let actual: MyStruct = BinaryDeserializer::builder_flavor(BinaryTestFlavor)
.deserialize_slice(&data[..], &map)?;
assert_eq!(actual, MyStruct { field1: "ENG".to_string() });
注意事项
- 这并不免除对令牌到字符串解析器的需求,因为令牌可能用作值。
- 如果结构体中的一个字段指定了
token
属性,则必须在结构体的所有字段中指定。
注意事项
在调用任何 Jomini API 之前,调用者应
- 事先确定正确的格式(文本或二进制)。
- 去除可能存在的任何头信息(例如:
EU4txt
/EU4bin
) - 提供二进制格式的令牌解析器
- 提供转换,以便解决例如日期可能以整数形式编码在二进制格式中,但在明文中以字符串形式编码的问题。
中级 API
如果自动反序列化通过 JominiDeserialize
太高级,有一个中级 API,可以轻松迭代解析文档并查询字段的信息。
use jomini::TextTape;
let data = b"name=aaa name=bbb core=123 name=ccc name=ddd";
let tape = TextTape::from_slice(data).unwrap();
let reader = tape.windows1252_reader();
for (key, _op, value) in reader.fields() {
println!("{:?}={:?}", key.read_str(), value.read_str().unwrap());
}
对于更底层的解析,请参阅相应的二进制和文本文档。
当启用 json
功能时,中级 API 还提供了将明文 Clausewitz 格式转换为 JSON 的优秀功能。
use jomini::TextTape;
let tape = TextTape::from_slice(b"foo=bar")?;
let reader = tape.windows1252_reader();
let actual = reader.json().to_string()?;
assert_eq!(actual, r#"{"foo":"bar"}"#);
写入 API
写入 API 有两个目标用例。一个是当手头有文本带时。这在需要重新格式化文档时很有用(请注意,注释不会被保留)
use jomini::{TextTape, TextWriterBuilder};
let tape = TextTape::from_slice(b"hello = world")?;
let mut out: Vec<u8> = Vec::new();
let mut writer = TextWriterBuilder::new().from_writer(&mut out);
writer.write_tape(&tape)?;
assert_eq!(&out, b"hello=world");
写入器会规范化任何格式问题。写入器无法无损地写入所有解析的文档,但这些情况仅限于真正罕见的场景,并希望在未来的版本中解决。
另一个用例更多地针对增量写入,这在熔合器或手动编写文档的人中很常见。这些用例需要手动驱动写入器
use jomini::TextWriterBuilder;
let mut out: Vec<u8> = Vec::new();
let mut writer = TextWriterBuilder::new().from_writer(&mut out);
writer.write_unquoted(b"hello")?;
writer.write_unquoted(b"world")?;
writer.write_unquoted(b"foo")?;
writer.write_unquoted(b"bar")?;
assert_eq!(&out, b"hello=world\nfoo=bar");
不支持的语法
由于 Clausewitz 是闭源的性质,这个库永远不能保证与 Clausewitz 的兼容性。没有关于有效输入的规范,我们只有一些在野外收集的示例。根据我们所知,Clausewitz 是非常灵活的:允许每个游戏对象定义其自己的独特语法。
我们只能尽力而为,并在遇到新语法时添加对其的支持。
基准测试
基准测试使用以下命令运行
cargo clean
cargo bench -- parse
find ./target -wholename "*/new/raw.csv" -print0 | xargs -0 xsv cat rows > assets/jomini-benchmarks.csv
可以使用在资产目录中找到的 R 脚本进行分析。
以下是任意计算机上基准测试生成的图形。
依赖项
~0–410KB