use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use serde_json::Value;
fn main() {
if std::env::var("DOCS_RS").is_ok() {
return;
}
let monitored_files = [
"BaseTerrain.json",
"Belief.json",
"Building.json",
"CityStateType.json",
"Difficulty.json",
"Era.json",
"Feature.json",
"Nation.json",
"NaturalWonder.json",
"Resource.json",
"TerrainType.json",
"TileImprovement.json",
"Unit.json",
"UnitPromotion.json",
"UnitType.json",
];
for file in &monitored_files {
println!(
"cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/{}",
file
);
}
let json_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("jsons")
.join("Civ V - Gods & Kings");
let enums_dir = Path::new("src/ruleset/enums");
let enum_mappings: Vec<(&str, String, String)> = monitored_files
.iter()
.map(|&json_file| {
let enum_name = json_file.strip_suffix(".json").unwrap();
let rust_file = to_snake_case(enum_name);
(
json_file,
format!("{}.rs", rust_file),
enum_name.to_string(),
)
})
.collect();
for (json_file, rust_file, enum_name) in enum_mappings.iter() {
let json_path = json_dir.join(json_file);
let rust_path = enums_dir.join(rust_file);
if !json_path.exists() {
panic!("JSON file not found: {}", json_path.display());
}
create_enum_from_json(
json_path
.to_str()
.expect("JSON path contains invalid UTF-8"),
rust_path
.to_str()
.expect("Rust path contains invalid UTF-8"),
enum_name.as_str(),
);
}
generate_mod_file(enums_dir, &enum_mappings);
}
fn generate_mod_file(output_dir: &Path, mappings: &[(&str, String, String)]) {
let mod_path = output_dir.join("mod.rs");
let mut file = File::create(&mod_path)
.unwrap_or_else(|e| panic!("Failed to create {}: {}", mod_path.display(), e));
writeln!(file, "// Auto-generated file. Do not edit manually.").unwrap();
writeln!(file, "// Re-exports all generated enum types").unwrap();
writeln!(file).unwrap();
for (_, rust_file, enum_name) in mappings {
let module_name = rust_file.strip_suffix(".rs").unwrap();
writeln!(file, "pub mod {};", module_name).unwrap();
writeln!(file, "pub use {}::{};", module_name, enum_name).unwrap();
}
}
fn to_snake_case(name: &str) -> String {
let mut result = String::with_capacity(name.len() * 2);
let mut prev_lower = false;
for (i, c) in name.char_indices() {
if c.is_uppercase() {
if i > 0 && prev_lower {
result.push('_');
}
result.push(c.to_ascii_lowercase());
prev_lower = false;
} else {
result.push(c);
prev_lower = true;
}
}
result.trim_matches('_').to_string()
}
fn create_enum_from_json(json_path: &str, dest_path: &str, enum_name: &str) {
let json_string_without_comment = load_json_file_and_strip_json_comments(json_path);
let value_list: Vec<Value> = serde_json::from_str(&json_string_without_comment)
.unwrap_or_else(|_| panic!("{}'{}'", "Can't serde ", json_path));
let names: Vec<&str> = value_list
.iter()
.map(|terrain| terrain.get("name").and_then(|v| v.as_str()).unwrap_or(""))
.collect();
let mut output = String::new();
output.push_str("// Auto-generated by build.rs, DO NOT EDIT\n");
output.push_str("use enum_map::Enum;\n");
output.push_str("use serde::{Deserialize, Serialize};\n");
output.push('\n');
output.push_str(
"#[derive(Enum, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize, Debug)]\n",
);
output.push_str(&format!("pub enum {} {{\n", enum_name));
let enum_variants: Vec<String> = names
.iter()
.map(|name| {
let variant: String = name
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
})
.collect::<Vec<String>>()
.join("")
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect();
variant
})
.collect();
for variant in enum_variants.iter() {
output.push_str(&format!(" {},\n", variant));
}
output.push_str("}\n\n");
output.push_str(&format!("impl {} {{\n", enum_name));
output.push_str(" pub fn as_str(&self) -> &'static str {\n");
output.push_str(" match self {\n");
for (variant, name) in enum_variants.iter().zip(names.iter()) {
output.push_str(&format!(
" {}::{} => \"{}\",\n",
enum_name, variant, name
));
}
output.push_str(" }\n");
output.push_str(" }\n");
output.push_str("}\n");
let mut file = File::create(dest_path).expect("Could not create output file");
file.write_all(output.as_bytes())
.expect("Could not write to file");
}
fn load_json_file_and_strip_json_comments(path: &str) -> String {
let json_string_with_comment = fs::read_to_string(path).unwrap();
strip_json_comments(&json_string_with_comment, true)
}
pub fn strip_json_comments(json_with_comments: &str, preserve_locations: bool) -> String {
let mut json_without_comments = String::new();
let mut block_comment_depth: u8 = 0;
let mut is_in_string: bool = false;
for line in json_with_comments.split('\n') {
let mut last_char: Option<char> = None;
for cur_char in line.chars() {
if block_comment_depth == 0 && last_char != Some('\\') && cur_char == '"' {
is_in_string = !is_in_string;
}
if !is_in_string && last_char == Some('/') && cur_char == '/' {
last_char = None;
if preserve_locations {
json_without_comments.push_str(" ");
}
break; }
if !is_in_string && last_char == Some('/') && cur_char == '*' {
block_comment_depth += 1;
last_char = None;
if preserve_locations {
json_without_comments.push_str(" ");
}
} else if !is_in_string && last_char == Some('*') && cur_char == '/' {
if block_comment_depth > 0 {
block_comment_depth = block_comment_depth.saturating_sub(1);
}
last_char = None;
if preserve_locations {
json_without_comments.push_str(" ");
}
} else {
if block_comment_depth != 0 {
if preserve_locations {
json_without_comments.push(' ');
}
} else if let Some(last_char) = last_char {
json_without_comments.push(last_char);
}
last_char = Some(cur_char);
}
}
if let Some(last_char) = last_char {
if block_comment_depth == 0 {
json_without_comments.push(last_char);
} else if preserve_locations {
json_without_comments.push(' ');
}
}
while json_without_comments.ends_with(' ') {
json_without_comments.pop();
}
json_without_comments.push('\n');
}
json_without_comments
}