turtlegame/build.rs
2024-07-06 18:45:54 +02:00

210 lines
5.4 KiB
Rust

use heck::ToSnakeCase as _;
use resvg::{tiny_skia, usvg};
use std::{
collections::BTreeMap,
env,
fs::{self, File},
hash::{DefaultHasher, Hash, Hasher},
io::{self, Write as _},
path::{Path, PathBuf}
};
const TILE_SIZE: u32 = 64;
#[derive(Default)]
struct Assets {
/// Assets directly contained within this asset group, mapping their name to the name
/// of the constant storing their png.
assets: BTreeMap<String, String>,
/// Asset groups contained within this asset group, mapping their name to the assets
/// that group contains.
groups: BTreeMap<String, Box<Assets>>
}
struct AssetsWriter {
file: File,
root: Assets
}
impl AssetsWriter {
fn new<P: AsRef<Path>>(path: P) -> Self {
let mut file = File::create(path).expect("Failed to create assets file");
writeln!(file, "// @generated").unwrap();
Self {
file,
root: Assets::default()
}
}
fn add_png<P: AsRef<Path>>(
&mut self,
canonical_path: P,
png: tiny_skia::Pixmap
) -> String {
let mut hasher = DefaultHasher::new();
canonical_path.as_ref().hash(&mut hasher);
let hash = hasher.finish();
let const_name = format!("ASSET_{hash:X}");
writeln!(self.file, "// {}", canonical_path.as_ref().display()).unwrap();
write!(self.file, "const {const_name}: &'static [u8] = &[").unwrap();
for byte in png.encode_png().expect("Failed to encode png") {
write!(self.file, "{byte}, ").unwrap();
}
writeln!(self.file, "];").unwrap();
const_name
}
fn finish(mut self) {
fn write_assets_struct(
file: &mut File,
root: &Assets,
indent: &str
) -> io::Result<()> {
for (group_name, group) in &root.groups {
writeln!(file, "{indent}mod {group_name} {{")?;
writeln!(file, "{indent}\t#[allow(clippy::wildcard_imports)]")?;
writeln!(file, "{indent}\tuse super::*;")?;
write_assets_struct(file, group, &format!("{indent}\t"))?;
writeln!(file, "}}")?;
}
writeln!(file, "{indent}pub struct Assets {{")?;
for asset_name in root.assets.keys() {
writeln!(
file,
"pub {indent}\t{}: comfy::TextureHandle,",
asset_name.to_snake_case()
)?;
}
for group_name in root.groups.keys() {
writeln!(
file,
"pub {indent}\t{group_name}: &'static {group_name}::Assets,"
)?;
}
writeln!(file, "{indent}}}")?;
writeln!(file, "{indent}impl Assets {{")?;
writeln!(
file,
"{indent}\tpub fn load(c: &mut comfy::EngineContext<'_>) {{"
)?;
for asset_const_name in root.assets.values() {
writeln!(file, "{indent}\t\tc.load_texture_from_bytes({asset_const_name:?}, {asset_const_name});")?;
}
for group_name in root.groups.keys() {
writeln!(file, "{indent}\t\t{group_name}::Assets::load(c);")?;
}
writeln!(file, "{indent}\t}}")?;
writeln!(file, "{indent}}}")?;
writeln!(
file,
"{indent}pub static ASSETS: comfy::Lazy<Assets> = comfy::Lazy::new(|| Assets {{"
)?;
for (asset_name, asset_const_name) in &root.assets {
writeln!(
file,
"{indent}\t{}: comfy::texture_id({asset_const_name:?}),",
asset_name.to_snake_case()
)?;
}
for group_name in root.groups.keys() {
writeln!(
file,
"{indent}\t{group_name}: comfy::Lazy::force(&{group_name}::ASSETS),"
)?;
}
writeln!(file, "{indent}}});")?;
Ok(())
}
write_assets_struct(&mut self.file, &self.root, "").unwrap();
}
}
fn main() {
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed=assets/");
let out_dir = env::var_os("OUT_DIR").unwrap();
let assets = PathBuf::from(out_dir).join("assets.rs");
println!("cargo::warning=Writing assets to {}", assets.display());
println!("cargo::rustc-env=ASSETS_RS={}", assets.display());
let mut writer = AssetsWriter::new(assets);
process_dir("assets", &mut writer, &mut Vec::new());
writer.finish();
}
fn process_dir<P: AsRef<Path> + Copy>(
dir: P,
writer: &mut AssetsWriter,
groups: &mut Vec<String>
) {
for entry in fs::read_dir(dir).expect("Cannot read dir") {
let entry = entry.expect("Cannot read dir entry");
let path = entry.path();
if entry
.metadata()
.expect("Cannot read dir entry metadata")
.is_dir()
{
groups.push(
path.file_name()
.unwrap()
.to_str()
.expect("Non-UTF8 file names aren't allowed")
.into()
);
process_dir(&path, writer, groups);
groups.pop();
} else if path.extension().map(|ext| ext == "svg").unwrap_or(false) {
process_svg(&path, dir, writer, groups);
}
}
}
fn process_svg<P: AsRef<Path> + Copy, Q: AsRef<Path>>(
file: P,
dir: Q,
writer: &mut AssetsWriter,
groups: &[String]
) {
let bytes = fs::read(file).expect("Cannot read svg file");
let tree = usvg::Tree::from_data(&bytes, &usvg::Options {
resources_dir: Some(dir.as_ref().to_owned()),
..Default::default()
})
.unwrap_or_else(|err| {
panic!("Cannot parse svg file {}: {err}", file.as_ref().display())
});
let mut pixmap = tiny_skia::Pixmap::new(TILE_SIZE, TILE_SIZE).unwrap();
resvg::render(&tree, Default::default(), &mut pixmap.as_mut());
let const_name = writer.add_png(
file.as_ref()
.canonicalize()
.expect("Failed to canonicalize"),
pixmap
);
let mut group = &mut writer.root;
for group_name in groups {
if !group.groups.contains_key(group_name) {
group.groups.insert(group_name.to_owned(), Box::default());
}
group = group.groups.get_mut(group_name).unwrap();
}
group.assets.insert(
file.as_ref()
.file_stem()
.expect("File doesn't have a stem")
.to_str()
.expect("Non-UTF8 file names aren't allowed")
.into(),
const_name
);
}