Fetch mod image from Nexus #146
4 changed files with 81 additions and 23 deletions
|
@ -16,6 +16,7 @@
|
||||||
- dtmt: add utility to migrate mod projects
|
- dtmt: add utility to migrate mod projects
|
||||||
- dtmm: reset dtkit-patch installations
|
- dtmm: reset dtkit-patch installations
|
||||||
- sdk: implement decompiling Lua files
|
- sdk: implement decompiling Lua files
|
||||||
|
- dtmm: fetch cover image for Nexus mods
|
||||||
|
|
||||||
=== Fixed
|
=== Fixed
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,16 @@ fn find_archive_file<R: Read + Seek>(
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn image_data_to_buffer(data: impl AsRef<[u8]>) -> Result<ImageBuf> {
|
||||||
|
// Druid somehow doesn't return an error compatible with eyre, here.
|
||||||
|
// So we have to wrap through `Display` manually.
|
||||||
|
ImageBuf::from_data(data.as_ref()).map_err(|err| {
|
||||||
|
Report::msg(err.to_string())
|
||||||
|
.wrap_err("Invalid image data")
|
||||||
|
.suggestion("Supported formats are: PNG, JPEG, Bitmap and WebP")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Runs the content of a `.mod` file to extract what data we can get
|
// Runs the content of a `.mod` file to extract what data we can get
|
||||||
// from legacy mods.
|
// from legacy mods.
|
||||||
// 1. Create a global function `new_mod` that stores
|
// 1. Create a global function `new_mod` that stores
|
||||||
|
@ -234,6 +244,8 @@ fn extract_mod_config<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<(Mo
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::debug!(?legacy_mod_data);
|
||||||
|
|
||||||
if let Some(name) = find_archive_file(archive, "dtmt.cfg") {
|
if let Some(name) = find_archive_file(archive, "dtmt.cfg") {
|
||||||
let mut f = archive
|
let mut f = archive
|
||||||
.by_name(&name)
|
.by_name(&name)
|
||||||
|
@ -266,6 +278,24 @@ fn extract_mod_config<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<(Mo
|
||||||
|
|
||||||
Ok((cfg, root))
|
Ok((cfg, root))
|
||||||
}
|
}
|
||||||
|
} else if let Some((mod_id, resources, root)) = legacy_mod_data {
|
||||||
|
let cfg = ModConfig {
|
||||||
|
bundled: false,
|
||||||
|
dir: PathBuf::new(),
|
||||||
|
id: mod_id.clone(),
|
||||||
|
name: mod_id,
|
||||||
|
summary: "A mod for the game Warhammer 40,000: Darktide".into(),
|
||||||
|
version: "N/A".into(),
|
||||||
|
description: None,
|
||||||
|
author: None,
|
||||||
|
image: None,
|
||||||
|
categories: Vec::new(),
|
||||||
|
packages: Vec::new(),
|
||||||
|
resources,
|
||||||
|
depends: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((cfg, root))
|
||||||
} else {
|
} else {
|
||||||
eyre::bail!(
|
eyre::bail!(
|
||||||
"Mod needs a config file or `.mod` file. \
|
"Mod needs a config file or `.mod` file. \
|
||||||
|
@ -404,6 +434,8 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::trace!(?nexus);
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
|
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
|
||||||
|
|
||||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||||
|
@ -418,7 +450,13 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
let (mut mod_cfg, root) =
|
let (mut mod_cfg, root) =
|
||||||
extract_mod_config(&mut archive).wrap_err("Failed to extract mod configuration")?;
|
extract_mod_config(&mut archive).wrap_err("Failed to extract mod configuration")?;
|
||||||
tracing::info!("Importing mod {} ({})", mod_cfg.name, mod_cfg.id);
|
tracing::info!("Importing mod {} ({})", mod_cfg.name, mod_cfg.id);
|
||||||
tracing::debug!(root, ?mod_cfg);
|
|
||||||
|
let mod_dir = state.data_dir.join(state.mod_dir.as_ref());
|
||||||
|
let dest = mod_dir.join(&mod_cfg.id);
|
||||||
|
tracing::trace!("Creating mods directory {}", dest.display());
|
||||||
|
fs::create_dir_all(&dest)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to create data directory '{}'", dest.display()))?;
|
||||||
|
|
||||||
let image = if let Some(path) = &mod_cfg.image {
|
let image = if let Some(path) = &mod_cfg.image {
|
||||||
let name = archive
|
let name = archive
|
||||||
|
@ -434,33 +472,40 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
f.read_to_end(&mut buf)
|
f.read_to_end(&mut buf)
|
||||||
.wrap_err("Failed to read file index from archive")?;
|
.wrap_err("Failed to read file index from archive")?;
|
||||||
|
|
||||||
// Druid somehow doesn't return an error compatible with eyre, here.
|
let img = image_data_to_buffer(buf)?;
|
||||||
// So we have to wrap through `Display` manually.
|
|
||||||
let img = match ImageBuf::from_data(&buf) {
|
|
||||||
Ok(img) => img,
|
|
||||||
Err(err) => {
|
|
||||||
let err = Report::msg(err.to_string())
|
|
||||||
.wrap_err("Invalid image data")
|
|
||||||
.note("Supported formats are: PNG, JPEG, Bitmap and WebP")
|
|
||||||
.suggestion("Contact the mod author to fix this");
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(img)
|
Some(img)
|
||||||
|
} else if let Some((nexus, _)) = &nexus {
|
||||||
|
let api = NexusApi::new(state.nexus_api_key.to_string())?;
|
||||||
|
let url = nexus.picture_url.as_ref();
|
||||||
|
let data = api
|
||||||
|
.picture(url)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to download Nexus image from '{}'", url))?;
|
||||||
|
|
||||||
|
let img = image_data_to_buffer(&data)?;
|
||||||
|
|
||||||
|
let name = "image.bin";
|
||||||
|
let path = dest.join(name);
|
||||||
|
match fs::write(&path, &data).await {
|
||||||
|
Ok(_) => {
|
||||||
|
mod_cfg.image = Some(name.into());
|
||||||
|
Some(img)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let err = Report::new(err).wrap_err(format!(
|
||||||
|
"Failed to write Nexus picture to file '{}'",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
tracing::error!("{:?}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::trace!(?image);
|
tracing::trace!(?image);
|
||||||
|
tracing::debug!(root, ?mod_cfg);
|
||||||
let mod_dir = state.data_dir.join(state.mod_dir.as_ref());
|
|
||||||
let dest = mod_dir.join(&mod_cfg.id);
|
|
||||||
|
|
||||||
tracing::trace!("Creating mods directory {}", dest.display());
|
|
||||||
fs::create_dir_all(&dest)
|
|
||||||
.await
|
|
||||||
.wrap_err_with(|| format!("Failed to create data directory '{}'", dest.display()))?;
|
|
||||||
|
|
||||||
let packages = if mod_cfg.bundled {
|
let packages = if mod_cfg.bundled {
|
||||||
extract_bundled_mod(&mut archive, root, &mod_dir).wrap_err("Failed to extract mod")?
|
extract_bundled_mod(&mut archive, root, &mod_dir).wrap_err("Failed to extract mod")?
|
||||||
|
|
|
@ -78,6 +78,7 @@ pub(crate) struct NexusInfo {
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub summary: Arc<String>,
|
pub summary: Arc<String>,
|
||||||
pub description: Arc<String>,
|
pub description: Arc<String>,
|
||||||
|
pub picture_url: Arc<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<NexusMod> for NexusInfo {
|
impl From<NexusMod> for NexusInfo {
|
||||||
|
@ -89,6 +90,7 @@ impl From<NexusMod> for NexusInfo {
|
||||||
author: value.author,
|
author: value.author,
|
||||||
summary: Arc::new(value.summary),
|
summary: Arc::new(value.summary),
|
||||||
description: Arc::new(value.description),
|
description: Arc::new(value.description),
|
||||||
|
picture_url: Arc::new(value.picture_url.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::convert::Infallible;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
||||||
use reqwest::{Client, RequestBuilder, Url};
|
use reqwest::{Client, IntoUrl, RequestBuilder, Url};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -102,6 +102,16 @@ impl Api {
|
||||||
self.send(req).await
|
self.send(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn picture(&self, url: impl IntoUrl + std::fmt::Debug) -> Result<Vec<u8>> {
|
||||||
|
let res = self.client.get(url).send().await?.error_for_status()?;
|
||||||
|
|
||||||
|
res.bytes()
|
||||||
|
.await
|
||||||
|
.map(|bytes| bytes.to_vec())
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_file_name<S: AsRef<str>>(
|
pub fn parse_file_name<S: AsRef<str>>(
|
||||||
name: S,
|
name: S,
|
||||||
) -> Option<(String, u64, String, OffsetDateTime)> {
|
) -> Option<(String, u64, String, OffsetDateTime)> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue