Implement Nexus integration #54

Merged
lucas merged 15 commits from feat/nexus into master 2023-03-15 21:43:37 +01:00
4 changed files with 51 additions and 3 deletions
Showing only changes of commit c7203127bb - Show all commits

1
Cargo.lock generated
View file

@ -756,6 +756,7 @@ dependencies = [
"dtmt-shared",
"futures",
"lazy_static",
"nexusmods",
"oodle-sys",
"path-slash",
"sdk",

View file

@ -15,6 +15,7 @@ dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
futures = "0.3.25"
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
sdk = { path = "../../lib/sdk", version = "*" }
nexusmods = { path = "../../lib/nexusmods", version = "*" }
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
serde = { version = "1.0.152", features = ["derive", "rc"] }
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }

View file

@ -8,13 +8,14 @@ use color_eyre::{Help, Report, Result};
use druid::im::Vector;
use druid::{FileInfo, ImageBuf};
use dtmt_shared::ModConfig;
use nexusmods::Api as NexusApi;
use tokio::fs::{self, DirEntry};
use tokio::runtime::Runtime;
use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt;
use zip::ZipArchive;
use crate::state::{ActionState, ModInfo, ModOrder, PackageInfo};
use crate::state::{ActionState, ModInfo, ModOrder, NexusInfo, PackageInfo};
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
use super::read_sjson_file;
@ -26,6 +27,17 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
let data = Cursor::new(data);
let nexus = info
.path
.file_name()
.and_then(|s| s.to_str())
.and_then(NexusApi::parse_file_name)
.map(|(_, id, version, updated)| NexusInfo {
id,
version: Some(version),
updated,
});
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
if tracing::enabled!(tracing::Level::DEBUG) {
@ -137,11 +149,19 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.extract(Arc::as_ref(&mod_dir))
.wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
if let Some(nexus) = &nexus {
let data = serde_sjson::to_string(nexus).wrap_err("Failed to serialize Nexus info")?;
let path = mod_dir.join(&mod_cfg.id).join("nexus.sjson");
fs::write(&path, data.as_bytes())
.await
.wrap_err_with(|| format!("Failed to write Nexus info to '{}'", path.display()))?;
}
let packages = files
.into_iter()
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
.collect();
let info = ModInfo::new(mod_cfg, packages, image);
let info = ModInfo::new(mod_cfg, packages, image, nexus);
Ok(info)
}
@ -181,12 +201,25 @@ pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
let entry = res?;
let config_path = entry.path().join("dtmt.cfg");
let nexus_path = entry.path().join("nexus.sjson");
let index_path = entry.path().join("files.sjson");
let cfg: ModConfig = read_sjson_file(&config_path)
.await
.wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
let nexus: Option<NexusInfo> = match read_sjson_file(&nexus_path)
.await
.wrap_err_with(|| format!("Failed to read Nexus info '{}'", nexus_path.display()))
{
Ok(nexus) => Some(nexus),
Err(err) if err.is::<std::io::Error>() => match err.downcast_ref::<std::io::Error>() {
Some(err) if err.kind() == std::io::ErrorKind::NotFound => None,
_ => return Err(err),
},
Err(err) => return Err(err),
};
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
.await
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
@ -222,7 +255,7 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
.into_iter()
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
.collect();
let info = ModInfo::new(cfg, packages, image);
let info = ModInfo::new(cfg, packages, image, nexus);
Ok(info)
}

View file

@ -5,6 +5,7 @@ use druid::{
Data, ImageBuf, Lens, WindowHandle, WindowId,
};
use dtmt_shared::ModConfig;
use time::OffsetDateTime;
use super::SelectedModLens;
@ -69,6 +70,15 @@ impl From<dtmt_shared::ModDependency> for ModDependency {
}
}
#[derive(Clone, Data, Debug, Lens, serde::Serialize, serde::Deserialize)]
pub(crate) struct NexusInfo {
pub id: u64,
pub version: Option<String>,
#[data(ignore)]
#[serde(with = "time::serde::timestamp")]
pub updated: OffsetDateTime,
}
#[derive(Clone, Data, Debug, Lens)]
pub(crate) struct ModInfo {
pub id: String,
@ -87,6 +97,7 @@ pub(crate) struct ModInfo {
#[data(ignore)]
pub resources: ModResourceInfo,
pub depends: Vector<ModDependency>,
pub nexus: Option<NexusInfo>,
}
impl ModInfo {
@ -94,6 +105,7 @@ impl ModInfo {
cfg: ModConfig,
packages: Vector<Arc<PackageInfo>>,
image: Option<ImageBuf>,
nexus: Option<NexusInfo>,
) -> Self {
Self {
id: cfg.id,
@ -112,6 +124,7 @@ impl ModInfo {
localization: cfg.resources.localization,
},
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
nexus,
}
}
}