Compare commits
4 commits
6c511a30f4
...
52959a3d5d
Author | SHA1 | Date | |
---|---|---|---|
52959a3d5d | |||
4c6ad1aaed | |||
031c03480d | |||
6f848bb837 |
11 changed files with 296 additions and 43 deletions
|
@ -18,6 +18,7 @@
|
||||||
- sdk: implement decompiling Lua files
|
- sdk: implement decompiling Lua files
|
||||||
- dtmm: fetch cover image for Nexus mods
|
- dtmm: fetch cover image for Nexus mods
|
||||||
- dtmm: fetch file version for Nexus mods
|
- dtmm: fetch file version for Nexus mods
|
||||||
|
- dtmm: handle `nxm://` URIs via IPC and import the corresponding mod
|
||||||
|
|
||||||
=== Fixed
|
=== Fixed
|
||||||
|
|
||||||
|
|
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -218,6 +218,15 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.64.0"
|
version = "0.64.0"
|
||||||
|
@ -895,6 +904,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi-parser",
|
"ansi-parser",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
|
"bincode",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"clap",
|
"clap",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
@ -904,6 +914,7 @@ dependencies = [
|
||||||
"druid-widget-nursery",
|
"druid-widget-nursery",
|
||||||
"dtmt-shared",
|
"dtmt-shared",
|
||||||
"futures",
|
"futures",
|
||||||
|
"interprocess",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"luajit2-sys",
|
"luajit2-sys",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
|
@ -1833,6 +1844,19 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"rustc_version",
|
||||||
|
"to_method",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "intl-memoizer"
|
name = "intl-memoizer"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -3517,6 +3541,12 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "to_method"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.33.0"
|
version = "1.33.0"
|
||||||
|
|
|
@ -6,33 +6,35 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ansi-parser = "0.9.0"
|
||||||
|
async-recursion = "1.0.5"
|
||||||
|
bincode = "1.3.3"
|
||||||
bitflags = "1.3.2"
|
bitflags = "1.3.2"
|
||||||
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
|
colors-transform = "0.2.11"
|
||||||
confy = "0.5.1"
|
confy = "0.5.1"
|
||||||
druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp", "svg"] }
|
druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp", "svg"] }
|
||||||
|
druid-widget-nursery = "0.1"
|
||||||
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
oodle = { path = "../../lib/oodle", version = "*" }
|
interprocess = { version = "1.2.1", default-features = false }
|
||||||
sdk = { path = "../../lib/sdk", version = "*" }
|
lazy_static = "1.4.0"
|
||||||
|
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
|
||||||
|
minijinja = "1.0.10"
|
||||||
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
||||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
oodle = { path = "../../lib/oodle", version = "*" }
|
||||||
|
path-slash = "0.2.1"
|
||||||
|
sdk = { path = "../../lib/sdk", version = "*" }
|
||||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||||
|
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||||
|
string_template = "0.2.1"
|
||||||
|
strip-ansi-escapes = "0.1.1"
|
||||||
|
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
|
||||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
||||||
|
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
zip = "0.6.4"
|
|
||||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
|
||||||
path-slash = "0.2.1"
|
|
||||||
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
|
|
||||||
strip-ansi-escapes = "0.1.1"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
colors-transform = "0.2.11"
|
|
||||||
usvg = "0.25.0"
|
usvg = "0.25.0"
|
||||||
druid-widget-nursery = "0.1"
|
zip = "0.6.4"
|
||||||
ansi-parser = "0.9.0"
|
|
||||||
string_template = "0.2.1"
|
|
||||||
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
|
|
||||||
async-recursion = "1.0.5"
|
|
||||||
minijinja = "1.0.10"
|
|
||||||
|
|
10
crates/dtmm/assets/dtmm.desktop
Normal file
10
crates/dtmm/assets/dtmm.desktop
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=DTMM
|
||||||
|
GenericName=Mod Manager
|
||||||
|
Comment=A graphical mod manager for Warhammer 40,000: Darktide
|
||||||
|
Exec=dtmm %u
|
||||||
|
Type=Application
|
||||||
|
Keywords=Mod;
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Utility;
|
||||||
|
MimeType=x-scheme-handler/nxm;
|
|
@ -405,11 +405,10 @@ fn extract_legacy_mod<R: Read + Seek>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<ModInfo> {
|
pub(crate) async fn import_from_file(state: ActionState, info: FileInfo) -> Result<ModInfo> {
|
||||||
let data = fs::read(&info.path)
|
let data = fs::read(&info.path)
|
||||||
.await
|
.await
|
||||||
.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 nexus = if let Some((_, id, version, timestamp)) = info
|
let nexus = if let Some((_, id, version, timestamp)) = info
|
||||||
.path
|
.path
|
||||||
|
@ -450,6 +449,32 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
|
|
||||||
tracing::trace!(?nexus);
|
tracing::trace!(?nexus);
|
||||||
|
|
||||||
|
import_mod(state, nexus, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub(crate) async fn import_from_nxm(state: ActionState, uri: String) -> Result<ModInfo> {
|
||||||
|
let url = uri
|
||||||
|
.parse()
|
||||||
|
.wrap_err_with(|| format!("Invalid Uri '{}'", uri))?;
|
||||||
|
|
||||||
|
let api = NexusApi::new(state.nexus_api_key.to_string())?;
|
||||||
|
let (mod_info, file_info, data) = api
|
||||||
|
.handle_nxm(url)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to download mod from NXM uri '{}'", uri))?;
|
||||||
|
|
||||||
|
let nexus = NexusInfo::from(mod_info);
|
||||||
|
import_mod(state, Some((nexus, file_info.version)), data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state, data), fields(data = data.len()))]
|
||||||
|
pub(crate) async fn import_mod(
|
||||||
|
state: ActionState,
|
||||||
|
nexus: Option<(NexusInfo, String)>,
|
||||||
|
data: Vec<u8>,
|
||||||
|
) -> Result<ModInfo> {
|
||||||
|
let data = Cursor::new(data);
|
||||||
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) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use tokio::sync::RwLock;
|
||||||
use crate::controller::app::*;
|
use crate::controller::app::*;
|
||||||
use crate::controller::deploy::deploy_mods;
|
use crate::controller::deploy::deploy_mods;
|
||||||
use crate::controller::game::*;
|
use crate::controller::game::*;
|
||||||
use crate::controller::import::import_mod;
|
use crate::controller::import::*;
|
||||||
use crate::state::AsyncAction;
|
use crate::state::AsyncAction;
|
||||||
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
||||||
use crate::state::ACTION_FINISH_LOAD_INITIAL;
|
use crate::state::ACTION_FINISH_LOAD_INITIAL;
|
||||||
|
@ -57,7 +57,7 @@ async fn handle_action(
|
||||||
.expect("failed to send command");
|
.expect("failed to send command");
|
||||||
}),
|
}),
|
||||||
AsyncAction::AddMod(state, info) => tokio::spawn(async move {
|
AsyncAction::AddMod(state, info) => tokio::spawn(async move {
|
||||||
match import_mod(state, info)
|
match import_from_file(state, info)
|
||||||
.await
|
.await
|
||||||
.wrap_err("Failed to import mod")
|
.wrap_err("Failed to import mod")
|
||||||
{
|
{
|
||||||
|
@ -186,6 +186,28 @@ async fn handle_action(
|
||||||
let _ = f.write_all(&line).await;
|
let _ = f.write_all(&line).await;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
AsyncAction::NxmDownload(state, uri) => tokio::spawn(async move {
|
||||||
|
match import_from_nxm(state, uri)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to handle NXM URI")
|
||||||
|
{
|
||||||
|
Ok(mod_info) => {
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(
|
||||||
|
ACTION_FINISH_ADD_MOD,
|
||||||
|
SingleUse::new(Arc::new(mod_info)),
|
||||||
|
Target::Auto,
|
||||||
|
)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{:?}", err);
|
||||||
|
send_error(event_sink.clone(), err).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,13 @@ use std::sync::Arc;
|
||||||
use clap::parser::ValueSource;
|
use clap::parser::ValueSource;
|
||||||
use clap::{command, value_parser, Arg};
|
use clap::{command, value_parser, Arg};
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result, Section};
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
|
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::controller::worker::work_thread;
|
use crate::controller::worker::work_thread;
|
||||||
use crate::state::AsyncAction;
|
use crate::state::{AsyncAction, ACTION_HANDLE_NXM};
|
||||||
use crate::state::{Delegate, State};
|
use crate::state::{Delegate, State};
|
||||||
use crate::ui::theme;
|
use crate::ui::theme;
|
||||||
use crate::util::log::LogLevel;
|
use crate::util::log::LogLevel;
|
||||||
|
@ -29,6 +30,37 @@ mod util {
|
||||||
}
|
}
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
// As explained in https://docs.rs/interprocess/latest/interprocess/local_socket/enum.NameTypeSupport.html
|
||||||
|
// namespaces are supported on both platforms we care about: Windows and Linux.
|
||||||
|
const IPC_ADDRESS: &str = "@dtmm.sock";
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn notify_nxm_download(
|
||||||
|
uri: impl AsRef<str> + std::fmt::Debug,
|
||||||
|
level: Option<LogLevel>,
|
||||||
|
) -> Result<()> {
|
||||||
|
util::log::create_tracing_subscriber(level, None);
|
||||||
|
|
||||||
|
tracing::debug!("Received Uri '{}', sending to main process.", uri.as_ref());
|
||||||
|
|
||||||
|
let mut stream = LocalSocketStream::connect(IPC_ADDRESS)
|
||||||
|
.wrap_err_with(|| format!("Failed to connect to '{}'", IPC_ADDRESS))
|
||||||
|
.suggestion("Make sure the main window is open.")?;
|
||||||
|
|
||||||
|
tracing::debug!("Connected to main process at '{}'", IPC_ADDRESS);
|
||||||
|
|
||||||
|
bincode::serialize_into(&mut stream, uri.as_ref()).wrap_err("Failed to send URI")?;
|
||||||
|
|
||||||
|
// We don't really care what the message is, we just need an acknowledgement.
|
||||||
|
let _: String = bincode::deserialize_from(&mut stream).wrap_err("Failed to receive reply")?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Notified DTMM with uri '{}'. Check the main window.",
|
||||||
|
uri.as_ref()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
@ -53,15 +85,25 @@ fn main() -> Result<()> {
|
||||||
.value_parser(value_parser!(LogLevel))
|
.value_parser(value_parser!(LogLevel))
|
||||||
.default_value("info"),
|
.default_value("info"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("nxm")
|
||||||
|
.help("An `nxm://` URI to download")
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
||||||
let level = if matches.value_source("log-level") == Some(ValueSource::DefaultValue) {
|
let level = if matches.value_source("log-level") == Some(ValueSource::DefaultValue) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
matches.get_one::<LogLevel>("log-level").cloned()
|
matches.get_one::<LogLevel>("log-level").cloned()
|
||||||
};
|
};
|
||||||
util::log::create_tracing_subscriber(log_tx, level);
|
|
||||||
|
if let Some(uri) = matches.get_one::<String>("nxm") {
|
||||||
|
return notify_nxm_download(uri, level).wrap_err("Failed to send NXM Uri to main window.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
util::log::create_tracing_subscriber(level, Some(log_tx));
|
||||||
|
|
||||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
@ -84,6 +126,59 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
let event_sink = launcher.get_external_handle();
|
let event_sink = launcher.get_external_handle();
|
||||||
|
|
||||||
|
{
|
||||||
|
let span = tracing::info_span!(IPC_ADDRESS, "nxm-socket");
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
|
let event_sink = event_sink.clone();
|
||||||
|
let server =
|
||||||
|
LocalSocketListener::bind(IPC_ADDRESS).wrap_err("Failed to create IPC listener")?;
|
||||||
|
|
||||||
|
tracing::debug!("IPC server listening on '{}'", IPC_ADDRESS);
|
||||||
|
|
||||||
|
// Drop the guard here, so that we can re-enter the same span in the thread.
|
||||||
|
drop(_guard);
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("nxm-socket".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let res = server.accept().wrap_err_with(|| {
|
||||||
|
format!("IPC server failed to listen on '{}'", IPC_ADDRESS)
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
let res = bincode::deserialize_from(&mut stream)
|
||||||
|
.wrap_err("Failed to read message")
|
||||||
|
.and_then(|uri: String| {
|
||||||
|
tracing::trace!(uri, "Received NXM uri");
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.submit_command(ACTION_HANDLE_NXM, uri, druid::Target::Auto)
|
||||||
|
.wrap_err("Failed to start NXM download")
|
||||||
|
});
|
||||||
|
match res {
|
||||||
|
Ok(()) => {
|
||||||
|
let _ = bincode::serialize_into(&mut stream, "Ok");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{:?}", err);
|
||||||
|
let _ = bincode::serialize_into(&mut stream, "Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to receive client connection: {:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrap_err("Failed to create thread")?;
|
||||||
|
}
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("work-thread".into())
|
.name("work-thread".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
@ -97,7 +192,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrap_err("Work thread panicked")?;
|
.wrap_err("Failed to create thread")?;
|
||||||
|
|
||||||
launcher.launch(State::new()).map_err(Report::new)
|
launcher.launch(State::new()).map_err(Report::new)
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ impl From<NexusMod> for NexusInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug, Lens)]
|
#[derive(Clone, Data, Lens)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -118,6 +118,39 @@ pub(crate) struct ModInfo {
|
||||||
pub nexus: Option<NexusInfo>,
|
pub nexus: Option<NexusInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ModInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ModInfo")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("summary", &self.summary)
|
||||||
|
.field(
|
||||||
|
"description",
|
||||||
|
&(match &self.description {
|
||||||
|
Some(desc) => format!("Some(String[0..{}])", desc.len()),
|
||||||
|
None => "None".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.field("categories", &self.categories)
|
||||||
|
.field("author", &self.author)
|
||||||
|
.field(
|
||||||
|
"image",
|
||||||
|
&(match &self.image {
|
||||||
|
Some(image) => format!("Some(ImageBuf[{}x{}])", image.width(), image.height()),
|
||||||
|
None => "None".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("enabled", &self.enabled)
|
||||||
|
.field("packages", &format!("Vec[0..{}]", self.packages.len()))
|
||||||
|
.field("resources", &self.resources)
|
||||||
|
.field("depends", &self.depends)
|
||||||
|
.field("bundled", &self.bundled)
|
||||||
|
.field("nexus", &self.nexus)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
cfg: ModConfig,
|
cfg: ModConfig,
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector =
|
||||||
pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector =
|
pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector =
|
||||||
Selector::new("dtmm.action.finish-reset-deployment");
|
Selector::new("dtmm.action.finish-reset-deployment");
|
||||||
|
|
||||||
|
pub(crate) const ACTION_HANDLE_NXM: Selector<String> = Selector::new("dtmm.action.handle-nxm");
|
||||||
pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action.add-mod");
|
pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action.add-mod");
|
||||||
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
||||||
Selector::new("dtmm.action.finish-add-mod");
|
Selector::new("dtmm.action.finish-add-mod");
|
||||||
|
@ -97,6 +98,7 @@ pub(crate) enum AsyncAction {
|
||||||
CheckUpdates(ActionState),
|
CheckUpdates(ActionState),
|
||||||
LoadInitial((PathBuf, bool)),
|
LoadInitial((PathBuf, bool)),
|
||||||
Log((ActionState, Vec<u8>)),
|
Log((ActionState, Vec<u8>)),
|
||||||
|
NxmDownload(ActionState, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for AsyncAction {
|
impl std::fmt::Debug for AsyncAction {
|
||||||
|
@ -116,6 +118,9 @@ impl std::fmt::Debug for AsyncAction {
|
||||||
path, is_default
|
path, is_default
|
||||||
),
|
),
|
||||||
AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"),
|
AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"),
|
||||||
|
AsyncAction::NxmDownload(_, uri) => {
|
||||||
|
write!(f, "AsyncAction::NxmDownload(_state, {})", uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +255,20 @@ impl AppDelegate<State> for Delegate {
|
||||||
|
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
|
cmd if cmd.is(ACTION_HANDLE_NXM) => {
|
||||||
|
let uri = cmd
|
||||||
|
.get(ACTION_HANDLE_NXM)
|
||||||
|
.expect("command type match but didn't contain the expected value");
|
||||||
|
|
||||||
|
if self
|
||||||
|
.sender
|
||||||
|
.send(AsyncAction::NxmDownload(state.clone().into(), uri.clone()))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to queue action to download NXM mod");
|
||||||
|
}
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
cmd if cmd.is(ACTION_ADD_MOD) => {
|
cmd if cmd.is(ACTION_ADD_MOD) => {
|
||||||
let info = cmd
|
let info = cmd
|
||||||
.get(ACTION_ADD_MOD)
|
.get(ACTION_ADD_MOD)
|
||||||
|
|
|
@ -8,7 +8,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[derive(Clone, Copy, ValueEnum)]
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||||
pub enum LogLevel {
|
pub enum LogLevel {
|
||||||
Trace,
|
Trace,
|
||||||
Debug,
|
Debug,
|
||||||
|
@ -55,7 +55,7 @@ impl std::io::Write for ChannelWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>, level: Option<LogLevel>) {
|
pub fn create_tracing_subscriber(level: Option<LogLevel>, tx: Option<UnboundedSender<Vec<u8>>>) {
|
||||||
let mut env_layer = if let Some(level) = level {
|
let mut env_layer = if let Some(level) = level {
|
||||||
EnvFilter::from(level)
|
EnvFilter::from(level)
|
||||||
} else if cfg!(debug_assertions) {
|
} else if cfg!(debug_assertions) {
|
||||||
|
@ -78,11 +78,13 @@ pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>, level: Option<Log
|
||||||
|
|
||||||
let stdout_layer = fmt::layer().pretty();
|
let stdout_layer = fmt::layer().pretty();
|
||||||
|
|
||||||
let channel_layer = fmt::layer()
|
let channel_layer = tx.map(|tx| {
|
||||||
.event_format(dtmt_shared::Formatter)
|
fmt::layer()
|
||||||
.fmt_fields(debug_fn(dtmt_shared::format_fields))
|
.event_format(dtmt_shared::Formatter)
|
||||||
.with_writer(move || ChannelWriter::new(tx.clone()))
|
.fmt_fields(debug_fn(dtmt_shared::format_fields))
|
||||||
.with_filter(FilterFn::new(dtmt_shared::filter_fields));
|
.with_writer(move || ChannelWriter::new(tx.clone()))
|
||||||
|
.with_filter(FilterFn::new(dtmt_shared::filter_fields))
|
||||||
|
});
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(env_layer)
|
.with(env_layer)
|
||||||
|
|
|
@ -136,6 +136,13 @@ impl Api {
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn get_file_by_id(&self, mod_id: u64, file_id: u64) -> Result<File> {
|
||||||
|
let url = BASE_URL_GAME.join(&format!("mods/{mod_id}/files/{file_id}.json"))?;
|
||||||
|
let req = self.client.get(url);
|
||||||
|
self.send(req).await
|
||||||
|
}
|
||||||
|
|
||||||
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)> {
|
||||||
|
@ -174,7 +181,7 @@ impl Api {
|
||||||
self.send(req).await
|
self.send(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_nxm(&self, url: Url) -> Result<(Mod, Vec<u8>)> {
|
pub async fn handle_nxm(&self, url: Url) -> Result<(Mod, File, Vec<u8>)> {
|
||||||
let nxm = Self::parse_nxm(url.clone())?;
|
let nxm = Self::parse_nxm(url.clone())?;
|
||||||
|
|
||||||
let user = self.user_validate().await?;
|
let user = self.user_validate().await?;
|
||||||
|
@ -183,8 +190,9 @@ impl Api {
|
||||||
return Err(Error::InvalidNXM("user_id mismtach", url));
|
return Err(Error::InvalidNXM("user_id mismtach", url));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mod_data, download_info) = futures::try_join!(
|
let (mod_data, file_info, download_info) = futures::try_join!(
|
||||||
self.mods_id(nxm.mod_id),
|
self.mods_id(nxm.mod_id),
|
||||||
|
self.get_file_by_id(nxm.mod_id, nxm.file_id),
|
||||||
self.mods_download_link(nxm.mod_id, nxm.file_id, nxm.key, nxm.expires)
|
self.mods_download_link(nxm.mod_id, nxm.file_id, nxm.key, nxm.expires)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -195,7 +203,7 @@ impl Api {
|
||||||
let req = self.client.get(download_url);
|
let req = self.client.get(download_url);
|
||||||
let data = req.send().await?.bytes().await?;
|
let data = req.send().await?.bytes().await?;
|
||||||
|
|
||||||
Ok((mod_data, data.to_vec()))
|
Ok((mod_data, file_info, data.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_nxm(nxm: Url) -> Result<Nxm> {
|
pub fn parse_nxm(nxm: Url) -> Result<Nxm> {
|
||||||
|
@ -204,17 +212,20 @@ impl Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now it makes sense, why Nexus calls this field `game_domain_name`, when it's just
|
// Now it makes sense, why Nexus calls this field `game_domain_name`, when it's just
|
||||||
// another path segmentin the regular API calls.
|
// another path segment in the regular API calls.
|
||||||
if nxm.host_str() != Some(GAME_ID) {
|
if nxm.host_str() != Some(GAME_ID) {
|
||||||
return Err(Error::InvalidNXM("Invalid game domain name", nxm));
|
return Err(Error::InvalidNXM("Invalid game domain name", nxm));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(mut segments) = nxm.path_segments() else {
|
let Some(mut segments) = nxm.path_segments() else {
|
||||||
return Err(Error::InvalidNXM("Cannot be a base", nxm));
|
return Err(Error::InvalidNXM("Missing path segments", nxm));
|
||||||
};
|
};
|
||||||
|
|
||||||
if segments.next() != Some("mods") {
|
if segments.next() != Some("mods") {
|
||||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
return Err(Error::InvalidNXM(
|
||||||
|
"Unexpected path segment, expected 'mods'",
|
||||||
|
nxm,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(mod_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
let Some(mod_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||||
|
@ -222,7 +233,10 @@ impl Api {
|
||||||
};
|
};
|
||||||
|
|
||||||
if segments.next() != Some("files") {
|
if segments.next() != Some("files") {
|
||||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
return Err(Error::InvalidNXM(
|
||||||
|
"Unexpected path segment, expected 'files'",
|
||||||
|
nxm,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(file_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
let Some(file_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||||
|
@ -237,7 +251,7 @@ impl Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(key) = query.get("key") else {
|
let Some(key) = query.get("key") else {
|
||||||
return Err(Error::InvalidNXM("Missing 'key'", nxm));
|
return Err(Error::InvalidNXM("Missing query field 'key'", nxm));
|
||||||
};
|
};
|
||||||
|
|
||||||
let expires = query
|
let expires = query
|
||||||
|
@ -245,12 +259,12 @@ impl Api {
|
||||||
.and_then(|expires| expires.parse().ok())
|
.and_then(|expires| expires.parse().ok())
|
||||||
.and_then(|expires| OffsetDateTime::from_unix_timestamp(expires).ok());
|
.and_then(|expires| OffsetDateTime::from_unix_timestamp(expires).ok());
|
||||||
let Some(expires) = expires else {
|
let Some(expires) = expires else {
|
||||||
return Err(Error::InvalidNXM("Missing 'expires'", nxm));
|
return Err(Error::InvalidNXM("Missing query field 'expires'", nxm));
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = query.get("user_id").and_then(|id| id.parse().ok());
|
let user_id = query.get("user_id").and_then(|id| id.parse().ok());
|
||||||
let Some(user_id) = user_id else {
|
let Some(user_id) = user_id else {
|
||||||
return Err(Error::InvalidNXM("Missing 'user_id'", nxm));
|
return Err(Error::InvalidNXM("Missing query field 'user_id'", nxm));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Nxm {
|
Ok(Nxm {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue