use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
use image::DynamicImage;
use crate::{
context::AppContext,
error::{DoomError, Result},
logos::ensure_default_logo_assets,
placements::{composite_logo, load_config},
};
pub fn apply_logos_to_armor(
ctx: &AppContext,
armor_texture: impl AsRef<Path>,
output_path: impl AsRef<Path>,
placement_config: impl AsRef<Path>,
logo_dir: impl AsRef<Path>,
target: &str,
) -> Result<()> {
let armor_path = ctx.repo().repo_path(armor_texture);
let config_path = ctx.repo().repo_path(placement_config);
let logo_root = ctx.repo().repo_path(logo_dir);
let output_file = ctx.repo().repo_path(output_path);
if !armor_path.exists() {
return Err(DoomError::message(format!(
"Missing armor texture: {}",
armor_path.display()
)));
}
if !logo_root.exists() {
ensure_default_logo_assets(ctx, &logo_root)?;
}
let config = load_config(ctx, config_path)?;
let placements = config
.placements
.into_iter()
.filter(|placement| placement.target == target && placement.is_enabled())
.collect::<Vec<_>>();
let mut result = image::open(&armor_path)?.to_rgba8();
for placement in &placements {
let logo_path = logo_root.join(&placement.logo);
if !logo_path.exists() {
return Err(DoomError::message(format!(
"Missing prepared logo: {}",
logo_path.display()
)));
}
let logo = image::open(&logo_path)?.to_rgba8();
composite_logo(&mut result, &logo, placement);
println!("Applied {} with {}", placement.id, placement.logo);
}
if let Some(parent) = output_file.parent() {
fs::create_dir_all(parent)?;
}
DynamicImage::ImageRgba8(result).save(&output_file)?;
println!("Wrote {}", output_file.display());
Ok(())
}
pub fn build_workpack(
ctx: &AppContext,
output_path: impl AsRef<Path>,
armor_texture: Option<PathBuf>,
target: &str,
) -> Result<()> {
let workpack_dir = ctx.repo().root().join("build").join("workpack");
let output_file = ctx.repo().repo_path(output_path);
ensure_default_logo_assets(ctx, PathBuf::from("build/logos"))?;
if let Some(armor_texture) = armor_texture {
apply_logos_to_armor(
ctx,
armor_texture,
PathBuf::from("build/textures/slayer_armor_albedo_xylex.png"),
PathBuf::from("config/armor-logo-placements.json"),
PathBuf::from("build/logos"),
target,
)?;
}
if workpack_dir.exists() {
assert_under_repo(ctx, &workpack_dir)?;
fs::remove_dir_all(&workpack_dir)?;
}
fs::create_dir_all(&workpack_dir)?;
fs::copy(ctx.repo().root().join("README.md"), workpack_dir.join("README.md"))?;
fs::copy(
ctx.repo().root().join("config").join("armor-logo-placements.json"),
workpack_dir.join("armor-logo-placements.json"),
)?;
fs::copy(
ctx.repo().root().join("mod").join("EternalMod.json"),
workpack_dir.join("EternalMod.json"),
)?;
copy_contents(&ctx.repo().root().join("build").join("logos"), &workpack_dir.join("logos"))?;
copy_contents(&ctx.repo().root().join("docs"), &workpack_dir.join("docs"))?;
copy_contents(
&ctx.repo().root().join("build").join("textures"),
&workpack_dir.join("textures"),
)?;
zip_directory(&workpack_dir, &output_file)?;
println!("Wrote {}", output_file.display());
Ok(())
}
fn copy_contents(source_dir: &Path, destination_dir: &Path) -> Result<()> {
if !source_dir.exists() {
return Ok(());
}
fs::create_dir_all(destination_dir)?;
for entry in fs::read_dir(source_dir)? {
let entry = entry?;
let source_path = entry.path();
let destination = destination_dir.join(entry.file_name());
if source_path.is_dir() {
copy_dir_recursive(&source_path, &destination)?;
} else {
fs::copy(&source_path, &destination)?;
}
}
Ok(())
}
fn copy_dir_recursive(source: &Path, destination: &Path) -> Result<()> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let child_source = entry.path();
let child_destination = destination.join(entry.file_name());
if child_source.is_dir() {
copy_dir_recursive(&child_source, &child_destination)?;
} else {
if let Some(parent) = child_destination.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&child_source, &child_destination)?;
}
}
Ok(())
}
fn zip_directory(source_dir: &Path, output_file: &Path) -> Result<()> {
if let Some(parent) = output_file.parent() {
fs::create_dir_all(parent)?;
}
if output_file.exists() {
fs::remove_file(output_file)?;
}
let file = fs::File::create(output_file)?;
let mut archive = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
for entry in walkdir::WalkDir::new(source_dir).into_iter().filter_map(|entry| entry.ok()) {
if !entry.file_type().is_file() {
continue;
}
let relative = entry.path().strip_prefix(source_dir)?;
archive.start_file(relative.to_string_lossy().replace('\\', "/"), options)?;
archive.write_all(&fs::read(entry.path())?)?;
}
archive.finish()?;
Ok(())
}
fn assert_under_repo(ctx: &AppContext, path: &Path) -> Result<()> {
let resolved_path = ctx.repo().normalize_path(path)?;
let resolved_root = ctx.repo().normalize_path(ctx.repo().root())?;
if !resolved_path.starts_with(&resolved_root) {
return Err(DoomError::message(format!(
"Refusing to modify path outside repo: {}",
resolved_path.display()
)));
}
Ok(())
}