use serde::Deserialize;
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::Path;
use std::path::PathBuf;
#[derive(Deserialize)]
struct ConsumableLayoutsFile {
#[serde(default)]
layout: Vec<ConsumableLayoutEntry>,
}
#[derive(Deserialize)]
struct ConsumableLayoutEntry {
version: String,
#[serde(default)]
ids: BTreeMap<String, String>,
}
fn generate_consumable_versions() {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let toml_path = manifest_dir.join("consumable_layouts.toml");
println!("cargo:rerun-if-changed={}", toml_path.display());
let data = std::fs::read_to_string(&toml_path).expect("read consumable_layouts.toml");
let parsed: ConsumableLayoutsFile = toml::from_str(&data).expect("parse consumable_layouts.toml");
type ParsedLayout = ((u32, u32, u32), Vec<(i32, String)>);
let mut layouts: Vec<ParsedLayout> = parsed
.layout
.into_iter()
.map(|l| {
let mut parts = l.version.split('.').map(|p| p.parse::<u32>().expect("version component"));
let version = (parts.next().expect("major"), parts.next().unwrap_or(0), parts.next().unwrap_or(0));
let mut ids: Vec<(i32, String)> =
l.ids.into_iter().map(|(k, name)| (k.parse::<i32>().expect("consumable id key"), name)).collect();
ids.sort_by_key(|(id, _)| *id);
(version, ids)
})
.collect();
layouts.sort_by_key(|(version, _)| *version);
let mut out = String::new();
out.push_str("// @generated by build.rs from consumable_layouts.toml -- do not edit.\n\n");
out.push_str("/// A single consumable `id -> internal name` entry.\n");
out.push_str("type ConsumableEntry = (i32, &'static str);\n");
out.push_str("/// One layout: the base version it takes effect at, plus its full id -> name table.\n");
out.push_str("type ConsumableLayout = (Version, &'static [ConsumableEntry]);\n\n");
out.push_str("/// Consumable id -> name layouts, ascending by base version. Each table is the full\n");
out.push_str("/// id -> name map effective from that version until the next supersedes it.\n");
out.push_str("pub static CONSUMABLE_ID_LAYOUTS: &[ConsumableLayout] = &[\n");
for ((major, minor, patch), ids) in &layouts {
let entries: Vec<String> = ids.iter().map(|(id, name)| format!("({id}, {name:?})")).collect();
writeln!(out, " (Version::base({major}, {minor}, {patch}), &[{}]),", entries.join(", "))
.expect("write layout");
}
out.push_str("];\n\n");
out.push_str("/// The consumable id -> name table effective for `version`: the latest layout whose\n");
out.push_str("/// base version is at most `version`. Versions older than every known layout floor\n");
out.push_str("/// to the earliest one (the closest available approximation).\n");
out.push_str("pub fn consumable_ids_for_version(version: Version) -> Option<&'static [ConsumableEntry]> {\n");
out.push_str(" CONSUMABLE_ID_LAYOUTS\n");
out.push_str(" .iter()\n");
out.push_str(" .rev()\n");
out.push_str(" .find(|(start, _)| version.is_at_least(start))\n");
out.push_str(" .or_else(|| CONSUMABLE_ID_LAYOUTS.first())\n");
out.push_str(" .map(|(_, table)| *table)\n");
out.push_str("}\n");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
std::fs::write(out_dir.join("consumable_versions.rs"), out).expect("write generated consumable_versions.rs");
}
#[derive(Deserialize, Default)]
struct Registry {
latest_path: Option<PathBuf>,
#[serde(default)]
builds: BTreeMap<String, RegistryEntry>,
}
#[derive(Deserialize)]
struct RegistryEntry {
#[allow(dead_code)]
version: String,
}
fn find_workspace_root() -> Option<PathBuf> {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").ok()?);
let mut dir = manifest_dir.as_path();
loop {
if dir.join("game_versions.toml").exists() {
return Some(dir.to_path_buf());
}
dir = dir.parent()?;
}
}
fn scan_bin_dir(path: &Path) -> Vec<u32> {
let bin_dir = path.join("bin");
let Ok(entries) = std::fs::read_dir(&bin_dir) else {
return Vec::new();
};
entries
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.filter_map(|e| e.file_name().to_str().and_then(|s| s.parse::<u32>().ok()))
.collect()
}
fn discover_builds(workspace_root: &Path) -> Vec<u32> {
let data_dir = match std::env::var("WOWS_GAME_DATA") {
Ok(d) => PathBuf::from(d),
Err(_) => workspace_root.join("game_data"),
};
let registry_path = data_dir.join("versions.toml");
let registry: Registry =
std::fs::read_to_string(®istry_path).ok().and_then(|s| toml::from_str(&s).ok()).unwrap_or_default();
let mut builds: Vec<u32> = Vec::new();
for key in registry.builds.keys() {
if let Ok(build) = key.parse::<u32>() {
let build_dir = data_dir.join("builds").join(key);
if build_dir.exists() {
builds.push(build);
}
}
}
if let Some(ref latest) = registry.latest_path {
for build in scan_bin_dir(latest) {
if !builds.contains(&build) {
builds.push(build);
}
}
}
let builds_dir = data_dir.join("builds");
if builds_dir.exists()
&& let Ok(entries) = std::fs::read_dir(&builds_dir)
{
for entry in entries.filter_map(|e| e.ok()) {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& let Some(build) = entry.file_name().to_str().and_then(|s| s.parse::<u32>().ok())
&& !builds.contains(&build)
{
builds.push(build);
}
}
}
builds.sort();
builds
}
const KNOWN_TEST_BUILDS: &[u32] = &[
6965290, 9531281, 11965230, ];
fn main() {
generate_consumable_versions();
println!("cargo:rustc-check-cfg=cfg(has_game_data)");
for &build in KNOWN_TEST_BUILDS {
println!("cargo:rustc-check-cfg=cfg(has_build_{build})");
}
let Some(workspace_root) = find_workspace_root() else {
return;
};
let builds = discover_builds(&workspace_root);
for &build in &builds {
if !KNOWN_TEST_BUILDS.contains(&build) {
println!("cargo:rustc-check-cfg=cfg(has_build_{build})");
}
println!("cargo:rustc-cfg=has_build_{build}");
}
if !builds.is_empty() {
println!("cargo:rustc-cfg=has_game_data");
}
let data_dir = match std::env::var("WOWS_GAME_DATA") {
Ok(d) => PathBuf::from(d),
Err(_) => workspace_root.join("game_data"),
};
println!("cargo:rerun-if-changed={}", data_dir.join("versions.toml").display());
println!("cargo:rerun-if-env-changed=WOWS_GAME_DATA");
}