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",
|
"dtmt-shared",
|
||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"nexusmods",
|
||||||
"oodle-sys",
|
"oodle-sys",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"sdk",
|
"sdk",
|
||||||
|
|
|
@ -15,6 +15,7 @@ dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
||||||
sdk = { path = "../../lib/sdk", version = "*" }
|
sdk = { path = "../../lib/sdk", version = "*" }
|
||||||
|
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
||||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
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::im::Vector;
|
||||||
use druid::{FileInfo, ImageBuf};
|
use druid::{FileInfo, ImageBuf};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
use nexusmods::Api as NexusApi;
|
||||||
use tokio::fs::{self, DirEntry};
|
use tokio::fs::{self, DirEntry};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipArchive;
|
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 crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||||
|
|
||||||
use super::read_sjson_file;
|
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()))?;
|
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
|
||||||
let data = Cursor::new(data);
|
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")?;
|
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) {
|
||||||
|
@ -137,11 +149,19 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
.extract(Arc::as_ref(&mod_dir))
|
.extract(Arc::as_ref(&mod_dir))
|
||||||
.wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
|
.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
|
let packages = files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(mod_cfg, packages, image);
|
let info = ModInfo::new(mod_cfg, packages, image, nexus);
|
||||||
|
|
||||||
Ok(info)
|
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> {
|
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||||
let entry = res?;
|
let entry = res?;
|
||||||
let config_path = entry.path().join("dtmt.cfg");
|
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 index_path = entry.path().join("files.sjson");
|
||||||
|
|
||||||
let cfg: ModConfig = read_sjson_file(&config_path)
|
let cfg: ModConfig = read_sjson_file(&config_path)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
|
.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)
|
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
|
.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()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(cfg, packages, image);
|
let info = ModInfo::new(cfg, packages, image, nexus);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ use druid::{
|
||||||
Data, ImageBuf, Lens, WindowHandle, WindowId,
|
Data, ImageBuf, Lens, WindowHandle, WindowId,
|
||||||
};
|
};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::SelectedModLens;
|
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)]
|
#[derive(Clone, Data, Debug, Lens)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -87,6 +97,7 @@ pub(crate) struct ModInfo {
|
||||||
#[data(ignore)]
|
#[data(ignore)]
|
||||||
pub resources: ModResourceInfo,
|
pub resources: ModResourceInfo,
|
||||||
pub depends: Vector<ModDependency>,
|
pub depends: Vector<ModDependency>,
|
||||||
|
pub nexus: Option<NexusInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
|
@ -94,6 +105,7 @@ impl ModInfo {
|
||||||
cfg: ModConfig,
|
cfg: ModConfig,
|
||||||
packages: Vector<Arc<PackageInfo>>,
|
packages: Vector<Arc<PackageInfo>>,
|
||||||
image: Option<ImageBuf>,
|
image: Option<ImageBuf>,
|
||||||
|
nexus: Option<NexusInfo>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: cfg.id,
|
id: cfg.id,
|
||||||
|
@ -112,6 +124,7 @@ impl ModInfo {
|
||||||
localization: cfg.resources.localization,
|
localization: cfg.resources.localization,
|
||||||
},
|
},
|
||||||
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
|
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
|
||||||
|
nexus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue