diff --git a/crates/bm_asset/src/service.rs b/crates/bm_asset/src/service.rs index c623aa1..015f163 100644 --- a/crates/bm_asset/src/service.rs +++ b/crates/bm_asset/src/service.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; use bm_version::VersionKey; -use image::{ImageBuffer, Pixel, Rgb}; +use image::{GenericImageView, ImageBuffer, Pixel, Rgba}; use ironworks::Ironworks; use super::{ @@ -37,17 +37,29 @@ impl Service { converter.convert(&data_version, path, format) } - pub fn map(&self, version: VersionKey, territory: &str, index: &str) -> Result> { + pub fn map( + &self, + version: VersionKey, + territory: &str, + index: &str, + format: Format, + ) -> Result> { let version = self .data .version(version) .with_context(|| format!("data for {version} not ready"))?; + let output_format = match format { + Format::Jpeg => image::ImageFormat::Jpeg, + Format::Png => image::ImageFormat::Png, + Format::Webp => image::ImageFormat::WebP, + }; + let ironworks = version.ironworks(); let image = self.compose_map(&ironworks, territory, index)?; - texture::write(image, image::ImageFormat::Jpeg) + texture::write(image, output_format) } fn compose_map( @@ -55,26 +67,27 @@ impl Service { ironworks: &Ironworks, territory: &str, index: &str, - ) -> Result, Vec>> { + ) -> Result, Vec>> { let path = format!("ui/map/{territory}/{index}/{territory}{index}"); - let mut buffer_map = texture::read(&ironworks, &format!("{path}_m.tex"))?.into_rgb8(); - - // NOTE: The presence of `m` alone is not enough to confirm that a map needs - // composition, ref. `f1h1/02`, which contains a fully pre-composed map, and - // a pure black `m`. To bodge around this, we're checking if the `d` texture - // is also present - if not, we shouldn't try composing. - match ironworks.file::(&format!("{path}d.tex")) { - Ok(_) => {} - Err(ironworks::Error::NotFound(ironworks::ErrorValue::Path(_))) => { - return Ok(buffer_map); - } - Err(error) => return Err(Error::Failure(error.into())), - } + let mut buffer_map = texture::read(&ironworks, &format!("{path}_m.tex"))?.into_rgba8(); let buffer_background = match texture::read(&ironworks, &format!("{path}m_m.tex")) { // If the background texture wasn't found, we can assume the map texture is pre-composed. Err(Error::NotFound(_)) => return Ok(buffer_map), - Ok(image) => image.into_rgb8(), + Ok(image) => { + // Some maps have a fully black & transparent `m` texture and are pre-composited. + // A pixel from the center of the texture is checked since some maps have a transparent border. + let dimensions = image.dimensions(); + if image + .get_pixel(dimensions.0 / 2, dimensions.1 / 2) + .channels() + .iter() + .all(|c| *c == 0) + { + return Ok(buffer_map); + } + image.into_rgba8() + } Err(error) => Err(error)?, }; diff --git a/crates/bm_http/src/api1/asset.rs b/crates/bm_http/src/api1/asset.rs index 8a82c68..8fc82ef 100644 --- a/crates/bm_http/src/api1/asset.rs +++ b/crates/bm_http/src/api1/asset.rs @@ -39,7 +39,7 @@ use super::{ }; // NOTE: Bump this if changing any behavior that impacts output binary data for assets, to ensure ETag is cache-broken. -const ASSET_ETAG_VERSION: usize = 3; +const ASSET_ETAG_VERSION: usize = 4; #[derive(Debug, Clone, Deserialize)] pub struct Config { @@ -212,6 +212,16 @@ fn example_index() -> &'static str { "00" } +#[derive(Deserialize, JsonSchema)] +struct MapQuery { + #[serde(default = "default_map_format")] + format: SchemaFormat, +} + +fn default_map_format() -> SchemaFormat { + SchemaFormat(Format::Jpeg) +} + fn map_docs(operation: TransformOperation) -> TransformOperation { operation .summary("compose a map") @@ -219,9 +229,14 @@ fn map_docs(operation: TransformOperation) -> TransformOperation { "Retrieve the specified map, composing it from split source files if necessary.", ) .response_with::<200, Vec, _>(|mut response| { - let content = &mut response.inner().content; - content.clear(); - content.insert(mime::IMAGE_JPEG.to_string(), openapi::MediaType::default()); + response.inner().content = Format::iter() + .map(|format| { + ( + format_mime(format).to_string(), + openapi::MediaType::default(), + ) + }) + .collect(); response }) .response_with::<304, (), _>(|res| res.description("not modified")) @@ -231,15 +246,21 @@ fn map_docs(operation: TransformOperation) -> TransformOperation { async fn map( Path(MapPath { territory, index }): Path, VersionQuery(version_key): VersionQuery, + Query(MapQuery { + format: SchemaFormat(format), + }): Query, State(Service { asset, .. }): State, ) -> Result { - let bytes = asset.map(version_key, &territory, &index)?; + let bytes = asset.map(version_key, &territory, &index, format)?; let response = ( - TypedHeader(ContentType::jpeg()), + TypedHeader(ContentType::from(format_mime(format))), [( header::CONTENT_DISPOSITION, - format!("inline; filename=\"{territory}_{index}.jpg\""), + format!( + "inline; filename=\"{territory}_{index}.{extension}\"", + extension = format.extension() + ), )], bytes, );