Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions crates/bm_asset/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -37,44 +37,57 @@ impl Service {
converter.convert(&data_version, path, format)
}

pub fn map(&self, version: VersionKey, territory: &str, index: &str) -> Result<Vec<u8>> {
pub fn map(
&self,
version: VersionKey,
territory: &str,
index: &str,
format: Format,
) -> Result<Vec<u8>> {
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(
&self,
ironworks: &Ironworks,
territory: &str,
index: &str,
) -> Result<ImageBuffer<Rgb<u8>, Vec<u8>>> {
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
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::<FileExists>(&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)?,
};

Expand Down
35 changes: 28 additions & 7 deletions crates/bm_http/src/api1/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -212,16 +212,31 @@ 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)
}
Comment thread
ackwell marked this conversation as resolved.

fn map_docs(operation: TransformOperation) -> TransformOperation {
operation
.summary("compose a map")
.description(
"Retrieve the specified map, composing it from split source files if necessary.",
)
.response_with::<200, Vec<u8>, _>(|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"))
Expand All @@ -231,15 +246,21 @@ fn map_docs(operation: TransformOperation) -> TransformOperation {
async fn map(
Path(MapPath { territory, index }): Path<MapPath>,
VersionQuery(version_key): VersionQuery,
Query(MapQuery {
format: SchemaFormat(format),
}): Query<MapQuery>,
State(Service { asset, .. }): State<Service>,
) -> Result<impl IntoApiResponse> {
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,
);
Expand Down