diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e32ff71..c25777f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ - dtmt: split `build` into `build` and `package` - dtmt: implement deploying built bundles +- dtmm: indicate when a deployment is necessary == 2023-03-01 diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index b9903cf..aba2442 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::io::{Cursor, ErrorKind, Read}; use std::path::Path; +use std::sync::Arc; use color_eyre::eyre::{self, Context}; use color_eyre::{Help, Result}; @@ -107,7 +108,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result let packages = files .into_iter() - .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) + .map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect()))) .collect(); let info = ModInfo::new(mod_cfg, packages); @@ -171,14 +172,14 @@ async fn read_mod_dir_entry(res: Result) -> Result { let packages = files .into_iter() - .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) + .map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect()))) .collect(); let info = ModInfo::new(cfg, packages); Ok(info) } #[tracing::instrument(skip(mod_order))] -pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result> +pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result>> where S: Iterator, P: AsRef + std::fmt::Debug, @@ -214,7 +215,7 @@ where .filter_map(|entry| { if let Some(mut info) = mods.remove(&entry.id) { info.enabled = entry.enabled; - Some(info) + Some(Arc::new(info)) } else { None } diff --git a/crates/dtmm/src/controller/worker.rs b/crates/dtmm/src/controller/worker.rs index 80abf53..49785b4 100644 --- a/crates/dtmm/src/controller/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -41,7 +41,7 @@ async fn handle_action( .await .submit_command( ACTION_FINISH_ADD_MOD, - SingleUse::new(mod_info), + SingleUse::new(Arc::new(mod_info)), Target::Auto, ) .expect("failed to send command"); diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index c8dc3aa..65156a3 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -17,7 +17,7 @@ impl Default for View { } } -#[derive(Clone, Data, Debug)] +#[derive(Clone, Data, Debug, PartialEq)] pub struct PackageInfo { pub name: String, pub files: Vector, @@ -29,14 +29,14 @@ impl PackageInfo { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct ModResourceInfo { pub init: PathBuf, pub data: Option, pub localization: Option, } -#[derive(Clone, Data, Debug, Lens)] +#[derive(Clone, Data, Debug, Lens, PartialEq)] pub(crate) struct ModInfo { pub id: String, pub name: String, @@ -44,14 +44,14 @@ pub(crate) struct ModInfo { pub enabled: bool, #[lens(ignore)] #[data(ignore)] - pub packages: Vector, + pub packages: Vector>, #[lens(ignore)] #[data(ignore)] pub resources: ModResourceInfo, } impl ModInfo { - pub fn new(cfg: ModConfig, packages: Vector) -> Self { + pub fn new(cfg: ModConfig, packages: Vector>) -> Self { Self { id: cfg.id, name: cfg.name, @@ -67,17 +67,12 @@ impl ModInfo { } } -impl PartialEq for ModInfo { - fn eq(&self, other: &Self) -> bool { - self.name.eq(&other.name) - } -} - #[derive(Clone, Data, Lens)] pub(crate) struct State { pub current_view: View, - pub mods: Vector, + pub mods: Vector>, pub selected_mod_index: Option, + pub dirty: bool, pub is_deployment_in_progress: bool, pub is_reset_in_progress: bool, pub is_save_in_progress: bool, @@ -106,6 +101,7 @@ impl State { current_view: View::default(), mods: Vector::new(), selected_mod_index: None, + dirty: false, is_deployment_in_progress: false, is_reset_in_progress: false, is_save_in_progress: false, @@ -121,8 +117,8 @@ impl State { self.selected_mod_index = Some(index); } - pub fn add_mod(&mut self, info: ModInfo) { - if let Some(pos) = self.mods.index_of(&info) { + pub fn add_mod(&mut self, info: Arc) { + if let Some(pos) = self.mods.iter().position(|i| i.id == info.id) { self.mods.set(pos, info); self.selected_mod_index = Some(pos); } else { diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 08d17b0..6b05b96 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use druid::{ AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, }; @@ -9,9 +11,9 @@ pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); -pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector> = +pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector>> = Selector::new("dtmm.action.srart-delete-selected-mod"); -pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> = +pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector>> = Selector::new("dtmm.action.finish-delete-selected-mod"); pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); @@ -23,7 +25,7 @@ pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector = Selector::new("dtmm.action.finish-reset-deployment"); pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); -pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = +pub(crate) const ACTION_FINISH_ADD_MOD: Selector>> = Selector::new("dtmm.action.finish-add-mod"); pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); @@ -33,11 +35,13 @@ pub(crate) const ACTION_START_SAVE_SETTINGS: Selector = pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector = Selector::new("dtmm.action.finish-save-settings"); +pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty"); + pub(crate) enum AsyncAction { DeployMods(State), ResetDeployment(State), AddMod((State, FileInfo)), - DeleteMod((State, ModInfo)), + DeleteMod((State, Arc)), SaveSettings(State), } @@ -81,6 +85,7 @@ impl AppDelegate for Delegate { } cmd if cmd.is(ACTION_FINISH_DEPLOY) => { state.is_deployment_in_progress = false; + state.dirty = false; Handled::Yes } cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { @@ -165,7 +170,6 @@ impl AppDelegate for Delegate { }; state.mods.remove(index); - // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } @@ -188,7 +192,6 @@ impl AppDelegate for Delegate { .expect("command type matched but didn't contain the expected value"); if let Some(info) = info.take() { state.add_mod(info); - // ctx.submit_command(ACTION_START_SAVE_SETTINGS); } Handled::Yes } @@ -226,6 +229,10 @@ impl AppDelegate for Delegate { Handled::Yes } + cmd if cmd.is(ACTION_SET_DIRTY) => { + state.dirty = true; + Handled::Yes + } cmd => { if cfg!(debug_assertions) { tracing::warn!("Unknown command: {:?}", cmd); diff --git a/crates/dtmm/src/state/lens.rs b/crates/dtmm/src/state/lens.rs index 6c457a4..1156f52 100644 --- a/crates/dtmm/src/state/lens.rs +++ b/crates/dtmm/src/state/lens.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use druid::im::Vector; use druid::{Data, Lens}; @@ -5,9 +7,9 @@ use super::{ModInfo, State}; pub(crate) struct SelectedModLens; -impl Lens> for SelectedModLens { +impl Lens>> for SelectedModLens { #[tracing::instrument(name = "SelectedModLens::with", skip_all)] - fn with) -> V>(&self, data: &State, f: F) -> V { + fn with>) -> V>(&self, data: &State, f: F) -> V { let info = data .selected_mod_index .and_then(|i| data.mods.get(i).cloned()); @@ -16,16 +18,16 @@ impl Lens> for SelectedModLens { } #[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)] - fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { + fn with_mut>) -> V>(&self, data: &mut State, f: F) -> V { match data.selected_mod_index { Some(i) => { let mut info = data.mods.get_mut(i).cloned(); let ret = f(&mut info); - if let Some(info) = info { + if let Some(new) = info { // TODO: Figure out a way to check for equality and // only update when needed - data.mods.set(i, info); + data.mods.set(i, new); } else { data.selected_mod_index = None; } diff --git a/crates/dtmm/src/ui/widget/controller.rs b/crates/dtmm/src/ui/widget/controller.rs index ce18d5b..1bd7f6a 100644 --- a/crates/dtmm/src/ui/widget/controller.rs +++ b/crates/dtmm/src/ui/widget/controller.rs @@ -1,7 +1,7 @@ use druid::widget::{Button, Controller, Scroll}; use druid::{Data, Env, Event, EventCtx, Rect, UpdateCtx, Widget}; -use crate::state::{State, ACTION_START_SAVE_SETTINGS}; +use crate::state::{State, ACTION_SET_DIRTY, ACTION_START_SAVE_SETTINGS}; pub struct DisabledButtonController; @@ -57,11 +57,16 @@ impl> Controller> for AutoScrollController } } -/// A controller that submits the command to save settings every time its widget's -/// data changes. -pub struct SaveSettingsController; +macro_rules! compare_state_fields { + ($old:ident, $new:ident, $($field:ident),+) => { + $($old.$field != $new.$field) || + + } +} -impl> Controller for SaveSettingsController { +/// A controller that tracks state changes for certain fields and submits commands to handle them. +pub struct DirtyStateController; + +impl> Controller for DirtyStateController { fn update( &mut self, child: &mut W, @@ -70,13 +75,14 @@ impl> Controller for SaveSettingsController { data: &State, env: &Env, ) { - // Only filter for the values that actually go into the settings file. - if old_data.mods != data.mods - || old_data.game_dir != data.game_dir - || old_data.data_dir != data.data_dir - { + if compare_state_fields!(old_data, data, mods, game_dir, data_dir) { ctx.submit_command(ACTION_START_SAVE_SETTINGS); } + + if compare_state_fields!(old_data, data, mods, game_dir) { + ctx.submit_command(ACTION_SET_DIRTY); + } + child.update(ctx, old_data, data, env) } } diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index a0ccaa2..d500610 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use druid::im::Vector; use druid::lens; use druid::widget::{ @@ -15,7 +17,7 @@ use crate::state::{ ACTION_START_RESET_DEPLOYMENT, }; use crate::ui::theme; -use crate::ui::widget::controller::{AutoScrollController, SaveSettingsController}; +use crate::ui::widget::controller::{AutoScrollController, DirtyStateController}; use crate::ui::widget::PathBufFormatter; const TITLE: &str = "Darktide Mod Manager"; @@ -31,43 +33,48 @@ pub(crate) fn new() -> WindowDesc { } fn build_top_bar() -> impl Widget { + let mods_button = Button::new("Mods") + .on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods); + + let settings_button = Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { + state.current_view = View::Settings; + }); + + let deploy_button = { + Button::dynamic(|state: &State, _| { + let mut s = String::new(); + if state.dirty { + s.push_str("! "); + } + s.push_str("Deploy Mods"); + s + }) + .on_click(|ctx, _state: &mut State, _env| { + ctx.submit_command(ACTION_START_DEPLOY); + }) + .disabled_if(|data, _| data.is_deployment_in_progress || data.is_reset_in_progress) + }; + + let reset_button = Button::new("Reset Game") + .on_click(|ctx, _state: &mut State, _env| { + ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); + }) + .disabled_if(|data, _| data.is_deployment_in_progress || data.is_reset_in_progress); + Flex::row() .must_fill_main_axis(true) .main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child( Flex::row() - .with_child( - Button::new("Mods") - .on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods), - ) + .with_child(mods_button) .with_default_spacer() - .with_child( - Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { - state.current_view = View::Settings; - }), - ), + .with_child(settings_button), ) .with_child( Flex::row() - .with_child( - Button::new("Deploy Mods") - .on_click(|ctx, _state: &mut State, _env| { - ctx.submit_command(ACTION_START_DEPLOY); - }) - .disabled_if(|data, _| { - data.is_deployment_in_progress || data.is_reset_in_progress - }), - ) + .with_child(deploy_button) .with_default_spacer() - .with_child( - Button::new("Reset Game") - .on_click(|ctx, _state: &mut State, _env| { - ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); - }) - .disabled_if(|data, _| { - data.is_deployment_in_progress || data.is_reset_in_progress - }), - ), + .with_child(reset_button), ) .padding(theme::TOP_BAR_INSETS) .background(theme::TOP_BAR_BACKGROUND_COLOR) @@ -77,9 +84,11 @@ fn build_top_bar() -> impl Widget { fn build_mod_list() -> impl Widget { let list = List::new(|| { - let checkbox = - Checkbox::new("").lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::enabled)); - let name = Label::raw().lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::name)); + let checkbox = Checkbox::new("") + .lens(lens!((usize, Arc, bool), 1).then(ModInfo::enabled.in_arc())); + + let name = + Label::raw().lens(lens!((usize, Arc, bool), 1).then(ModInfo::name.in_arc())); Flex::row() .must_fill_main_axis(true) @@ -109,8 +118,10 @@ fn build_mod_list() -> impl Widget { .collect::>() }, |state, infos| { - infos.into_iter().for_each(|(i, info, _)| { - state.mods.set(i, info); + infos.into_iter().for_each(|(i, new, _)| { + if state.mods.get(i).cloned() != Some(new.clone()) { + state.mods.set(i, new); + } }); }, )); @@ -142,12 +153,12 @@ fn build_mod_details_buttons() -> impl Widget { .on_click(|_ctx, enabled: &mut bool, _env| { *enabled = !(*enabled); }) - .lens(ModInfo::enabled) + .lens(ModInfo::enabled.in_arc()) }, // TODO: Gray out || Button::new("Enable Mod"), ) - .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) + .disabled_if(|info: &Option>, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); let button_add_mod = Button::new("Add Mod").on_click(|ctx, _state: &mut State, _env| { @@ -162,14 +173,14 @@ fn build_mod_details_buttons() -> impl Widget { }); let button_delete_mod = Button::new("Delete Mod") - .on_click(|ctx, data: &mut Option, _env| { + .on_click(|ctx, data: &mut Option>, _env| { if let Some(info) = data { ctx.submit_command( ACTION_START_DELETE_SELECTED_MOD.with(SingleUse::new(info.clone())), ); } }) - .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) + .disabled_if(|info: &Option>, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); Flex::column() @@ -203,10 +214,10 @@ fn build_mod_details_info() -> impl Widget { // Force the label to take up the entire details' pane width, // so that we can center-align it. .expand_width() - .lens(ModInfo::name); + .lens(ModInfo::name.in_arc()); let description = Label::raw() .with_line_break_mode(LineBreaking::WordWrap) - .lens(ModInfo::description); + .lens(ModInfo::description.in_arc()); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) @@ -312,5 +323,5 @@ fn build_window() -> impl Widget { .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) .with_child(build_log_view()) - .controller(SaveSettingsController) + .controller(DirtyStateController) } diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 86812b3..2c942ea 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -1,5 +1,6 @@ use std::io::ErrorKind; use std::path::PathBuf; +use std::sync::Arc; use std::{fs, path::Path}; use clap::{parser::ValueSource, ArgMatches}; @@ -38,6 +39,7 @@ impl<'a> From<&'a State> for ConfigSerialize<'a> { mod_order: state .mods .iter() + .map(Arc::as_ref) .map(LoadOrderEntrySerialize::from) .collect(), }