From 6b168bad6b120a0d64f2b51dba202da9f170e29a Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 9 Mar 2023 11:34:54 +0100 Subject: [PATCH 1/2] fix(dtmm): Fix game directory check when there is no Steam Fixes #60. --- crates/dtmm/src/controller/game.rs | 25 ++++++++++++++--------- crates/dtmm/src/main.rs | 31 ++++++++++++++++++++--------- crates/dtmm/src/ui/window/dialog.rs | 1 + lib/dtmt-shared/src/lib.rs | 16 +++++++-------- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index 9a95be6..c054688 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -533,7 +533,14 @@ pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { } } - let (game_info, deployment_info) = tokio::try_join!( + let (_, game_info, deployment_info) = tokio::try_join!( + async { + let path = state.game_dir.join("bundle"); + fs::metadata(&path) + .await + .wrap_err("Failed to open game bundle directory") + .with_suggestion(|| "Double-check 'Game Directory' in the Settings tab.") + }, async { tokio::task::spawn_blocking(dtmt_shared::collect_game_info) .await @@ -557,16 +564,16 @@ pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { ) .wrap_err("Failed to gather deployment information")?; - let game_info = game_info.wrap_err("Failed to collect Steam info")?; - tracing::debug!(?game_info, ?deployment_info); - if deployment_info - .as_ref() - .map(|i| game_info.last_updated > i.timestamp) - .unwrap_or(false) - { - eyre::bail!("Game was updated since last mod deployment. Please reset first."); + if let Some(game_info) = game_info { + if deployment_info + .as_ref() + .map(|i| game_info.last_updated > i.timestamp) + .unwrap_or(false) + { + eyre::bail!("Game was updated since last mod deployment. Please reset first."); + } } tracing::info!( diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 4a6b153..c3b8cb5 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -8,13 +8,17 @@ use std::sync::Arc; use clap::command; use clap::value_parser; use clap::Arg; +use color_eyre::eyre; use color_eyre::eyre::Context; use color_eyre::{Report, Result}; use druid::AppLauncher; +use druid::SingleUse; +use druid::Target; use tokio::sync::RwLock; use crate::controller::app::load_mods; use crate::controller::worker::work_thread; +use crate::state::ACTION_SHOW_ERROR_DIALOG; use crate::state::{Delegate, State}; mod controller; @@ -57,17 +61,32 @@ fn main() -> Result<()> { oodle_sys::init(matches.get_one::("oodle")); } + let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); + let delegate = Delegate::new(action_tx); + + let launcher = AppLauncher::with_window(ui::window::main::new()).delegate(delegate); + + let event_sink = launcher.get_external_handle(); + let config = util::config::read_config(&default_config_path, &matches) .wrap_err("Failed to read config file")?; - - let game_info = dtmt_shared::collect_game_info()?; + let game_info = dtmt_shared::collect_game_info(); tracing::debug!(?config, ?game_info); + let game_dir = config.game_dir.or_else(|| game_info.map(|i| i.path)); + if game_dir.is_none() { + let err = + eyre::eyre!("No Game Directory set. Head to the 'Settings' tab to set it manually",); + event_sink + .submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto) + .expect("failed to send command"); + } + let initial_state = { let mut state = State::new( config.path, - config.game_dir.unwrap_or(game_info.path), + game_dir.unwrap_or_default(), config.data_dir.unwrap_or_default(), ); state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter()) @@ -75,12 +94,6 @@ fn main() -> Result<()> { state }; - let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); - let delegate = Delegate::new(action_tx); - - let launcher = AppLauncher::with_window(ui::window::main::new()).delegate(delegate); - - let event_sink = launcher.get_external_handle(); std::thread::spawn(move || { let event_sink = Arc::new(RwLock::new(event_sink)); let action_rx = Arc::new(RwLock::new(action_rx)); diff --git a/crates/dtmm/src/ui/window/dialog.rs b/crates/dtmm/src/ui/window/dialog.rs index 3b6802a..64ce88d 100644 --- a/crates/dtmm/src/ui/window/dialog.rs +++ b/crates/dtmm/src/ui/window/dialog.rs @@ -20,6 +20,7 @@ pub fn error(err: Report, parent: WindowHandle) -> WindowDesc { let widget = Flex::column() .main_axis_alignment(MainAxisAlignment::SpaceBetween) + .must_fill_main_axis(true) .cross_axis_alignment(CrossAxisAlignment::End) .with_child(text) .with_spacer(20.) diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs index fa8c407..f15e4e7 100644 --- a/lib/dtmt-shared/src/lib.rs +++ b/lib/dtmt-shared/src/lib.rs @@ -1,8 +1,5 @@ use std::path::PathBuf; -use color_eyre::eyre; -use color_eyre::Result; - mod log; pub use log::*; @@ -40,11 +37,12 @@ pub struct GameInfo { pub last_updated: OffsetDateTime, } -pub fn collect_game_info() -> Result { +pub fn collect_game_info() -> Option { let mut dir = if let Some(dir) = SteamDir::locate() { dir } else { - eyre::bail!("Failed to locate Steam installation") + tracing::debug!("Failed to locate Steam installation"); + return None; }; let found = dir @@ -52,15 +50,17 @@ pub fn collect_game_info() -> Result { .and_then(|app| app.vdf.get("LastUpdated").map(|v| (app.path.clone(), v))); let Some((path, last_updated)) = found else { - eyre::bail!("Failed to find game installation"); + tracing::debug!("Found Steam, but failed to find game installation"); + return None; }; let Some(last_updated) = last_updated .as_value() .and_then(|v| v.to::()) .and_then(|v| OffsetDateTime::from_unix_timestamp(v).ok()) else { - eyre::bail!("Couldn't read 'LastUpdate'."); + tracing::error!("Found Steam game, but couldn't read 'LastUpdate'."); + return None; }; - Ok(GameInfo { path, last_updated }) + Some(GameInfo { path, last_updated }) } -- 2.45.3 From 4995190199d681ead794bb8b3764134682e041ed Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 9 Mar 2023 11:52:52 +0100 Subject: [PATCH 2/2] feat(dtmm): Check mod order Closes #13. --- CHANGELOG.adoc | 2 + crates/dtmm/src/controller/app.rs | 63 +++++++++++++++++++++++++++++- crates/dtmm/src/controller/game.rs | 11 ++---- crates/dtmm/src/state/data.rs | 32 +++++++++++++++ lib/dtmt-shared/src/lib.rs | 21 ++++++++-- 5 files changed, 117 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f61467b..ecfed2b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -10,6 +10,8 @@ - dtmm: check for Steam game update before deployment - dtmm: remove unused bundles from previous deployment - dtmm: show dialog for critical errors +- dtmm: check mod order before deployment +- dtmt: add mod dependencies to config === Fixed diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index ce8f2a2..c36e7f2 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -14,7 +14,7 @@ use tokio_stream::wrappers::ReadDirStream; use tokio_stream::StreamExt; use zip::ZipArchive; -use crate::state::{ActionState, ModInfo, PackageInfo}; +use crate::state::{ActionState, ModInfo, ModOrder, PackageInfo}; use crate::util::config::{ConfigSerialize, LoadOrderEntry}; use super::read_sjson_file; @@ -216,3 +216,64 @@ where Ok::<_, color_eyre::Report>(mods) }) } + +pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> { + { + let first = state.mods.get(0); + if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) { + // TODO: Add a suggestion where to get it, once that's published + eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order"); + } + } + + state + .mods + .iter() + .filter(|i| i.enabled) + .enumerate() + .for_each(|(i, info)| tracing::debug!(i, ?info)); + + for (i, mod_info) in state.mods.iter().filter(|i| i.enabled).enumerate() { + for dep in &mod_info.depends { + let dep_info = state.mods.iter().enumerate().find(|(_, m)| m.id == dep.id); + + match dep_info { + Some((_, dep_info)) if !dep_info.enabled => { + eyre::bail!( + "Dependency '{}' ({}) must be enabled.", + dep_info.name, + dep.id + ); + } + Some((j, dep_info)) if dep.order == ModOrder::Before && j >= i => { + eyre::bail!( + "Dependency '{}' ({}) must be loaded before '{}'", + dep_info.name, + dep.id, + mod_info.name + ); + } + Some((j, dep_info)) if dep.order == ModOrder::After && j <= i => { + eyre::bail!( + "Dependency '{}' ({}) must be loaded after '{}'", + dep_info.name, + dep.id, + mod_info.name + ); + } + None => { + eyre::bail!( + "Missing dependency '{}' for mod '{}'", + dep.id, + mod_info.name + ); + } + Some(_) => { + // All good + } + } + } + } + + Ok(()) +} diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index c054688..68f9269 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -21,6 +21,7 @@ use tokio::io::AsyncWriteExt; use tracing::Instrument; use super::read_sjson_file; +use crate::controller::app::check_mod_order; use crate::state::{ActionState, PackageInfo}; const MOD_BUNDLE_NAME: &str = "packages/mods"; @@ -525,14 +526,6 @@ where pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { let state = Arc::new(state); - { - let first = state.mods.get(0); - if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) { - // TODO: Add a suggestion where to get it, once that's published - eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order"); - } - } - let (_, game_info, deployment_info) = tokio::try_join!( async { let path = state.game_dir.join("bundle"); @@ -576,6 +569,8 @@ pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { } } + check_mod_order(&state)?; + tracing::info!( "Deploying {} mods to {}", state.mods.iter().filter(|i| i.enabled).count(), diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index 8021840..779b67c 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -39,6 +39,36 @@ pub(crate) struct ModResourceInfo { pub localization: Option, } +#[derive(Clone, Data, Debug, PartialEq)] +pub(crate) enum ModOrder { + Before, + After, +} + +#[derive(Clone, Data, Debug, PartialEq)] +pub(crate) struct ModDependency { + pub id: String, + pub order: ModOrder, +} + +impl From for ModDependency { + fn from(value: dtmt_shared::ModDependency) -> Self { + match value { + dtmt_shared::ModDependency::ID(id) => ModDependency { + id, + order: ModOrder::Before, + }, + dtmt_shared::ModDependency::Config { id, order } => ModDependency { + id, + order: match order { + dtmt_shared::ModOrder::Before => ModOrder::Before, + dtmt_shared::ModOrder::After => ModOrder::After, + }, + }, + } + } +} + #[derive(Clone, Data, Debug, Lens, PartialEq)] pub(crate) struct ModInfo { pub id: String, @@ -51,6 +81,7 @@ pub(crate) struct ModInfo { #[lens(ignore)] #[data(ignore)] pub resources: ModResourceInfo, + pub depends: Vector, } impl ModInfo { @@ -66,6 +97,7 @@ impl ModInfo { data: cfg.resources.data, localization: cfg.resources.localization, }, + depends: cfg.depends.into_iter().map(ModDependency::from).collect(), } } } diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs index f15e4e7..17b8d92 100644 --- a/lib/dtmt-shared/src/lib.rs +++ b/lib/dtmt-shared/src/lib.rs @@ -3,10 +3,11 @@ use std::path::PathBuf; mod log; pub use log::*; +use serde::Deserialize; use steamlocate::SteamDir; use time::OffsetDateTime; -#[derive(Clone, Debug, Default, serde::Deserialize)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct ModConfigResources { pub init: PathBuf, #[serde(default)] @@ -15,7 +16,21 @@ pub struct ModConfigResources { pub localization: Option, } -#[derive(Clone, Debug, Default, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ModOrder { + Before, + After, +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(untagged)] +pub enum ModDependency { + ID(String), + Config { id: String, order: ModOrder }, +} + +#[derive(Clone, Debug, Default, Deserialize)] pub struct ModConfig { #[serde(skip)] pub dir: std::path::PathBuf, @@ -26,7 +41,7 @@ pub struct ModConfig { pub packages: Vec, pub resources: ModConfigResources, #[serde(default)] - pub depends: Vec, + pub depends: Vec, } pub const STEAMAPP_ID: u32 = 1361210; -- 2.45.3