Skip to content

Commit c8c089d

Browse files
committed
refactor: 自动生成配置 schema
- 从 config_section! Rust 声明解析 section、字段、默认值与 UI 元数据 - 删除手写 schema.toml 并在构建时生成 manifest 和默认配置 - 将生成的 schema 编码进 winhttp.dll 的 .acmani section - 保留选项、范围、分组、隐藏导出与描述元数据 - 添加 Rust 声明生成 schema 和 PE 容器回归测试
1 parent 4b15677 commit c8c089d

42 files changed

Lines changed: 791 additions & 996 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
3434
## 配置
3535

36-
所有功能通过游戏目录下的 `AppleChu.toml` 控制。每个功能 section 使用统一的 `enable = true/false` 开关,缺失的 section 和字段都会使用代码默认值。用户文件只保存覆盖项,程序不会因 schema 变化自动重写已有配置。
36+
所有功能通过游戏目录下的 `AppleChu.toml` 控制。每个功能 section 使用统一的 `Enable = true/false` 开关,缺失的 section 和字段都会使用代码默认值。用户文件只保存覆盖项,程序不会因 schema 变化自动重写已有配置。
3737

3838
> [!NOTE]
3939
> 如需图形化编辑,可使用 [ChuChartManager](https://github.com/MuNET-OSS/ChuChartManager)
@@ -88,6 +88,8 @@ AppleChu 内置一套与 segatools 配置和 API 完全兼容的游戏侧 IO 仿
8888

8989
需要 Rust nightly 工具链与 `i686-pc-windows-msvc``x86_64-pc-windows-msvc` 目标:
9090

91+
配置 schema 由 `config_section!` Rust 声明在构建时自动生成并嵌入 `winhttp.dll``.acmani` section,无需维护独立 schema 文件。
92+
9193
```bash
9294
rustup target add i686-pc-windows-msvc
9395
rustup target add x86_64-pc-windows-msvc

packages/applechu-schema/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ edition.workspace = true
55
license.workspace = true
66

77
[dependencies]
8-
once_cell.workspace = true
98
sha2.workspace = true
9+
syn.workspace = true
1010
toml.workspace = true

packages/applechu-schema/src/bin/export.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ use std::fs;
22
use std::path::PathBuf;
33

44
fn main() -> Result<(), Box<dyn std::error::Error>> {
5-
let output = std::env::args_os()
6-
.nth(1)
5+
let mut arguments = std::env::args_os().skip(1);
6+
let source = arguments
7+
.next()
78
.map(PathBuf::from)
8-
.ok_or("用法: applechu-schema-export <输出目录>")?;
9+
.ok_or("用法: applechu-schema-export <Rust 源目录> <输出目录>")?;
10+
let output = arguments
11+
.next()
12+
.map(PathBuf::from)
13+
.ok_or("用法: applechu-schema-export <Rust 源目录> <输出目录>")?;
914
fs::create_dir_all(&output)?;
10-
fs::write(
11-
output.join("acmani.bin"),
12-
applechu_schema::SCHEMA.encode_acmani()?,
13-
)?;
15+
let schema = applechu_schema::generate_from_rust_dir(source)?;
16+
fs::write(output.join("acmani.bin"), schema.encode_acmani()?)?;
1417
Ok(())
1518
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use syn::LitStr;
2+
3+
use super::parser::{FieldDecl, SectionDecl};
4+
use super::value::{expression_value, schema_type};
5+
use crate::{canonical_key, SchemaError};
6+
7+
pub(super) fn build(sections: &[SectionDecl]) -> Result<String, SchemaError> {
8+
let mut document = toml::Table::new();
9+
document.insert("mod".to_owned(), toml::Value::Table(mod_metadata()));
10+
document.insert("ui".to_owned(), toml::Value::Table(ui_metadata(sections)));
11+
let visible = sections
12+
.iter()
13+
.filter(|section| should_export(section))
14+
.map(section_value)
15+
.collect::<Result<Vec<_>, _>>()?;
16+
let mut config = toml::Table::new();
17+
config.insert("sections".to_owned(), toml::Value::Array(visible));
18+
document.insert("config".to_owned(), toml::Value::Table(config));
19+
toml::to_string_pretty(&document).map_err(SchemaError::Serialize)
20+
}
21+
22+
fn mod_metadata() -> toml::Table {
23+
let mut table = toml::Table::new();
24+
for (key, value) in [
25+
("id", "applesaber.applechu"),
26+
("name", "AppleChu"),
27+
("version", env!("CARGO_PKG_VERSION")),
28+
("homepage", "https://github.com/MuNET-OSS/AppleChu"),
29+
("license", "Apache-2.0"),
30+
("min_loader_version", "1.0.0"),
31+
] {
32+
table.insert(key.to_owned(), toml::Value::String(value.to_owned()));
33+
}
34+
table.insert(
35+
"authors".to_owned(),
36+
toml::Value::Array(vec![toml::Value::String("Applesaber".to_owned())]),
37+
);
38+
table.insert(
39+
"game_versions".to_owned(),
40+
toml::Value::Array(vec![toml::Value::String("2.45".to_owned())]),
41+
);
42+
table.insert(
43+
"description".to_owned(),
44+
localized("CHUNITHM Mod", Some("CHUNITHM Mod")),
45+
);
46+
table
47+
}
48+
49+
fn ui_metadata(sections: &[SectionDecl]) -> toml::Table {
50+
let definitions = [
51+
("common", "常用", "Common"),
52+
("gameplay", "游戏", "Gameplay"),
53+
("display", "显示", "Display"),
54+
("audio", "音频", "Audio"),
55+
("network", "网络", "Network"),
56+
("io", "IO", "IO"),
57+
("compatibility", "兼容", "Compatibility"),
58+
];
59+
let groups = definitions
60+
.into_iter()
61+
.filter_map(|(id, label, label_en)| {
62+
let members = sections
63+
.iter()
64+
.filter(|section| {
65+
should_export(section)
66+
&& section
67+
.group
68+
.as_ref()
69+
.map_or(id == "common", |group| group.value() == id)
70+
})
71+
.map(|section| toml::Value::String(section.name.value()))
72+
.collect::<Vec<_>>();
73+
if members.is_empty() {
74+
return None;
75+
}
76+
let mut group = toml::Table::new();
77+
group.insert("id".to_owned(), toml::Value::String(id.to_owned()));
78+
group.insert("label".to_owned(), localized(label, Some(label_en)));
79+
group.insert("sections".to_owned(), toml::Value::Array(members));
80+
Some(toml::Value::Table(group))
81+
})
82+
.collect();
83+
let mut ui = toml::Table::new();
84+
ui.insert("groups".to_owned(), toml::Value::Array(groups));
85+
ui
86+
}
87+
88+
fn section_value(section: &SectionDecl) -> Result<toml::Value, SchemaError> {
89+
let mut table = toml::Table::new();
90+
table.insert("id".to_owned(), toml::Value::String(section.name.value()));
91+
table.insert(
92+
"default_enabled".to_owned(),
93+
toml::Value::Boolean(section.default_on.value),
94+
);
95+
if section.always_enabled.value {
96+
table.insert("always_enabled".to_owned(), toml::Value::Boolean(true));
97+
}
98+
if section.hidden.value {
99+
table.insert("hidden".to_owned(), toml::Value::Boolean(true));
100+
}
101+
if section.community {
102+
table.insert("community".to_owned(), toml::Value::Boolean(true));
103+
}
104+
table.insert(
105+
"label".to_owned(),
106+
localized(&section.comment.value(), Some(&section.name.value())),
107+
);
108+
if let Some(description) = &section.description {
109+
table.insert(
110+
"description".to_owned(),
111+
localized(
112+
&description.value(),
113+
section
114+
.description_en
115+
.as_ref()
116+
.map(LitStr::value)
117+
.as_deref(),
118+
),
119+
);
120+
}
121+
table.insert(
122+
"entries".to_owned(),
123+
toml::Value::Array(
124+
section
125+
.fields
126+
.iter()
127+
.map(field_value)
128+
.collect::<Result<Vec<_>, _>>()?,
129+
),
130+
);
131+
Ok(toml::Value::Table(table))
132+
}
133+
134+
fn field_value(field: &FieldDecl) -> Result<toml::Value, SchemaError> {
135+
let raw_key = field
136+
.key
137+
.as_ref()
138+
.map_or_else(|| field.name.to_string(), LitStr::value);
139+
let canonical_key = canonical_key(&raw_key);
140+
let mut table = toml::Table::new();
141+
table.insert("key".to_owned(), toml::Value::String(canonical_key.clone()));
142+
table.insert(
143+
"type".to_owned(),
144+
toml::Value::String(
145+
field
146+
.schema_type
147+
.as_ref()
148+
.map_or_else(|| schema_type(&field.value_type), |value| Ok(value.value()))?,
149+
),
150+
);
151+
table.insert(
152+
"default".to_owned(),
153+
expression_value(field.schema_default.as_ref().unwrap_or(&field.default))?,
154+
);
155+
table.insert(
156+
"emit_default".to_owned(),
157+
toml::Value::Boolean(field.emit_default),
158+
);
159+
table.insert(
160+
"label".to_owned(),
161+
localized(&field.comment.value(), Some(&canonical_key)),
162+
);
163+
if let Some(description) = &field.description {
164+
table.insert(
165+
"description".to_owned(),
166+
localized(
167+
&description.value(),
168+
field.description_en.as_ref().map(LitStr::value).as_deref(),
169+
),
170+
);
171+
}
172+
if field.comment.value().is_empty() {
173+
table.insert("emit_comment".to_owned(), toml::Value::Boolean(false));
174+
}
175+
if let Some(min) = &field.min {
176+
table.insert("min".to_owned(), expression_value(min)?);
177+
}
178+
if let Some(max) = &field.max {
179+
table.insert("max".to_owned(), expression_value(max)?);
180+
}
181+
if !field.options.is_empty() {
182+
table.insert(
183+
"options".to_owned(),
184+
toml::Value::Array(
185+
field
186+
.options
187+
.iter()
188+
.map(expression_value)
189+
.collect::<Result<Vec<_>, _>>()?
190+
.into_iter()
191+
.map(option_value)
192+
.collect(),
193+
),
194+
);
195+
}
196+
Ok(toml::Value::Table(table))
197+
}
198+
199+
fn option_value(value: toml::Value) -> toml::Value {
200+
let label = match &value {
201+
toml::Value::String(value) => value.clone(),
202+
toml::Value::Integer(value) => value.to_string(),
203+
_ => String::new(),
204+
};
205+
let mut option = toml::Table::new();
206+
option.insert("value".to_owned(), value);
207+
option.insert("label".to_owned(), localized(&label, Some(&label)));
208+
toml::Value::Table(option)
209+
}
210+
211+
fn localized(value: &str, en: Option<&str>) -> toml::Value {
212+
let mut label = toml::Table::new();
213+
label.insert("zh".to_owned(), toml::Value::String(value.to_owned()));
214+
label.insert(
215+
"en".to_owned(),
216+
toml::Value::String(en.unwrap_or(value).to_owned()),
217+
);
218+
toml::Value::Table(label)
219+
}
220+
221+
fn should_export(section: &SectionDecl) -> bool {
222+
section.export.unwrap_or(!section.hidden.value)
223+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
mod manifest;
2+
mod parser;
3+
mod value;
4+
5+
use std::path::Path;
6+
7+
use crate::{Schema, SchemaError};
8+
9+
pub fn generate_from_rust_dir(root: impl AsRef<Path>) -> Result<Schema, SchemaError> {
10+
let mut sections = parser::parse_directory(root.as_ref())?;
11+
sections.sort_by_key(|section| section.order.base10_parse::<u16>().unwrap_or(u16::MAX));
12+
Schema::parse(manifest::build(&sections)?)
13+
}

0 commit comments

Comments
 (0)