Indicate when a deployment is necessary #49

Merged
lucas merged 3 commits from issue/32 into master 2023-03-06 16:04:25 +01:00
9 changed files with 106 additions and 80 deletions

View file

@ -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

View file

@ -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<ModInfo>
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<DirEntry>) -> Result<ModInfo> {
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<Vector<ModInfo>>
pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result<Vector<Arc<ModInfo>>>
where
S: Iterator<Item = &'a LoadOrderEntry>,
P: AsRef<Path> + 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
}

View file

@ -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");

View file

@ -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<String>,
@ -29,14 +29,14 @@ impl PackageInfo {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ModResourceInfo {
pub init: PathBuf,
pub data: Option<PathBuf>,
pub localization: Option<PathBuf>,
}
#[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<PackageInfo>,
pub packages: Vector<Arc<PackageInfo>>,
#[lens(ignore)]
#[data(ignore)]
pub resources: ModResourceInfo,
}
impl ModInfo {
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
pub fn new(cfg: ModConfig, packages: Vector<Arc<PackageInfo>>) -> 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<ModInfo>,
pub mods: Vector<Arc<ModInfo>>,
pub selected_mod_index: Option<usize>,
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<ModInfo>) {
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 {

View file

@ -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<usize> = 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<SingleUse<ModInfo>> =
pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector<SingleUse<Arc<ModInfo>>> =
Selector::new("dtmm.action.srart-delete-selected-mod");
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<ModInfo>> =
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<Arc<ModInfo>>> =
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<FileInfo> = Selector::new("dtmm.action.add-mod");
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
Selector::new("dtmm.action.finish-add-mod");
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = 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<ModInfo>)),
SaveSettings(State),
}
@ -81,6 +85,7 @@ impl AppDelegate<State> 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<State> for Delegate {
};
state.mods.remove(index);
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
Handled::Yes
}
@ -188,7 +192,6 @@ impl AppDelegate<State> 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<State> 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);

View file

@ -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<State, Option<ModInfo>> for SelectedModLens {
impl Lens<State, Option<Arc<ModInfo>>> for SelectedModLens {
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
fn with<V, F: FnOnce(&Option<Arc<ModInfo>>) -> 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<State, Option<ModInfo>> for SelectedModLens {
}
#[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)]
fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
fn with_mut<V, F: FnOnce(&mut Option<Arc<ModInfo>>) -> 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;
}

View file

@ -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<T: Data, W: Widget<T>> Controller<T, Scroll<T, W>> 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<W: Widget<State>> Controller<State, W> for SaveSettingsController {
/// A controller that tracks state changes for certain fields and submits commands to handle them.
pub struct DirtyStateController;
impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
fn update(
&mut self,
child: &mut W,
@ -70,13 +75,14 @@ impl<W: Widget<State>> Controller<State, W> 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)
}
}

View file

@ -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<State> {
}
fn build_top_bar() -> impl Widget<State> {
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<State> {
fn build_mod_list() -> impl Widget<State> {
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<ModInfo>, bool), 1).then(ModInfo::enabled.in_arc()));
let name =
Label::raw().lens(lens!((usize, Arc<ModInfo>, bool), 1).then(ModInfo::name.in_arc()));
Flex::row()
.must_fill_main_axis(true)
@ -109,8 +118,10 @@ fn build_mod_list() -> impl Widget<State> {
.collect::<Vector<_>>()
},
|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<State> {
.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<ModInfo>, _env: &druid::Env| info.is_none())
.disabled_if(|info: &Option<Arc<ModInfo>>, _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<State> {
});
let button_delete_mod = Button::new("Delete Mod")
.on_click(|ctx, data: &mut Option<ModInfo>, _env| {
.on_click(|ctx, data: &mut Option<Arc<ModInfo>>, _env| {
if let Some(info) = data {
ctx.submit_command(
ACTION_START_DELETE_SELECTED_MOD.with(SingleUse::new(info.clone())),
);
}
})
.disabled_if(|info: &Option<ModInfo>, _env: &druid::Env| info.is_none())
.disabled_if(|info: &Option<Arc<ModInfo>>, _env: &druid::Env| info.is_none())
.lens(State::selected_mod);
Flex::column()
@ -203,10 +214,10 @@ fn build_mod_details_info() -> impl Widget<State> {
// 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<State> {
.with_child(build_top_bar())
.with_flex_child(build_main(), 1.0)
.with_child(build_log_view())
.controller(SaveSettingsController)
.controller(DirtyStateController)
}

View file

@ -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(),
}