#二进制格式 #底层 #保存文件 #CK3 #铁人 #EU4 #克劳塞维茨

bin+lib jomini

针对EU4、CK3、HOI4、Vic3、Imperator和其他PDS游戏标题的保存和游戏文件的底层、性能导向解析器

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 游戏开发

Download history 174/week @ 2024-05-04 117/week @ 2024-05-11 121/week @ 2024-05-18 118/week @ 2024-05-25 143/week @ 2024-06-01 132/week @ 2024-06-08 310/week @ 2024-06-15 137/week @ 2024-06-22 134/week @ 2024-06-29 148/week @ 2024-07-06 108/week @ 2024-07-13 105/week @ 2024-07-20 129/week @ 2024-07-27 135/week @ 2024-08-03 155/week @ 2024-08-10 114/week @ 2024-08-17

548每月下载量
用于 5 个库(4 个直接使用)

MIT 协议

700KB
17K SLoC

ci Version

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 脚本进行分析。

以下是任意计算机上基准测试生成的图形。

jomini-bench-throughput.png

依赖项

~0–410KB