Implement Nexus integration #54
4 changed files with 51 additions and 3 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -756,6 +756,7 @@ dependencies = [
|
|||
"dtmt-shared",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"nexusmods",
|
||||
"oodle-sys",
|
||||
"path-slash",
|
||||
"sdk",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue