diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 48bc35f..f61467b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,7 @@ - dtmm: indicate when a deployment is necessary - dtmm: check for Steam game update before deployment - dtmm: remove unused bundles from previous deployment +- dtmm: show dialog for critical errors === Fixed diff --git a/Cargo.lock b/Cargo.lock index 2709c2c..5f2a35a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,6 +705,7 @@ dependencies = [ "druid", "dtmt-shared", "futures", + "lazy_static", "oodle-sys", "path-slash", "sdk", diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 261a994..5e11a9b 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -26,3 +26,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" diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index c2f5962..3a8adc5 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -14,13 +14,13 @@ use tokio_stream::wrappers::ReadDirStream; use tokio_stream::StreamExt; use zip::ZipArchive; -use crate::state::{ModInfo, PackageInfo, State}; +use crate::state::{ActionState, ModInfo, PackageInfo}; use crate::util::config::{ConfigSerialize, LoadOrderEntry}; use super::read_sjson_file; #[tracing::instrument(skip(state))] -pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { +pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result { let data = fs::read(&info.path) .await .wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; @@ -95,16 +95,16 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result tracing::trace!(?files); - let mod_dir = state.get_mod_dir(); + let mod_dir = state.mod_dir; tracing::trace!("Creating mods directory {}", mod_dir.display()); - fs::create_dir_all(&mod_dir) + fs::create_dir_all(Arc::as_ref(&mod_dir)) .await .wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; tracing::trace!("Extracting mod archive to {}", mod_dir.display()); archive - .extract(&mod_dir) + .extract(Arc::as_ref(&mod_dir)) .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; let packages = files @@ -117,8 +117,8 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result } #[tracing::instrument(skip(state))] -pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { - let mod_dir = state.get_mod_dir().join(&info.id); +pub(crate) async fn delete_mod(state: ActionState, info: &ModInfo) -> Result<()> { + let mod_dir = state.mod_dir.join(&info.id); fs::remove_dir_all(&mod_dir) .await .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; @@ -127,7 +127,7 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { } #[tracing::instrument(skip(state))] -pub(crate) async fn save_settings(state: State) -> Result<()> { +pub(crate) async fn save_settings(state: ActionState) -> Result<()> { let cfg = ConfigSerialize::from(&state); tracing::info!("Saving settings to '{}'", state.config_path.display()); diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index a69691c..9260908 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -22,7 +22,7 @@ use tokio::io::AsyncWriteExt; use tracing::Instrument; use super::read_sjson_file; -use crate::state::{PackageInfo, State}; +use crate::state::{ActionState, PackageInfo}; const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; @@ -100,7 +100,7 @@ where } #[tracing::instrument(skip_all)] -async fn patch_game_settings(state: Arc) -> Result<()> { +async fn patch_game_settings(state: Arc) -> Result<()> { let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH); let settings = read_file_with_backup(&settings_path) @@ -146,7 +146,7 @@ fn make_package(info: &PackageInfo) -> Result { Ok(pkg) } -fn build_mod_data_lua(state: Arc) -> String { +fn build_mod_data_lua(state: Arc) -> String { let mut lua = String::from("return {\n"); // DMF is handled explicitely by the loading procedures, as it actually drives most of that @@ -203,7 +203,7 @@ fn build_mod_data_lua(state: Arc) -> String { } #[tracing::instrument(skip_all)] -async fn build_bundles(state: Arc) -> Result> { +async fn build_bundles(state: Arc) -> Result> { let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string()); let mut tasks = Vec::new(); @@ -227,7 +227,7 @@ async fn build_bundles(state: Arc) -> Result> { let span = tracing::trace_span!("building mod packages", name = mod_info.name); let _enter = span.enter(); - let mod_dir = state.get_mod_dir().join(&mod_info.id); + let mod_dir = state.mod_dir.join(&mod_info.id); for pkg_info in &mod_info.packages { let span = tracing::trace_span!("building package", name = pkg_info.name); let _enter = span.enter(); @@ -320,7 +320,7 @@ async fn build_bundles(state: Arc) -> Result> { } #[tracing::instrument(skip_all)] -async fn patch_boot_bundle(state: Arc) -> Result> { +async fn patch_boot_bundle(state: Arc) -> Result> { let bundle_dir = Arc::new(state.game_dir.join("bundle")); let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); @@ -381,7 +381,7 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let bundle_name = Murmur64::hash(&pkg_info.name) .to_string() .to_ascii_lowercase(); - let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name); + let src = state.mod_dir.join(&mod_info.id).join(&bundle_name); { let bin = fs::read(&src) @@ -461,7 +461,7 @@ async fn patch_boot_bundle(state: Arc) -> Result> { } #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] -async fn patch_bundle_database(state: Arc, bundles: B) -> Result<()> +async fn patch_bundle_database(state: Arc, bundles: B) -> Result<()> where B: AsRef<[Bundle]>, { @@ -499,7 +499,7 @@ where } #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] -async fn write_deployment_data(state: Arc, bundles: B) -> Result<()> +async fn write_deployment_data(state: Arc, bundles: B) -> Result<()> where B: AsRef<[Bundle]>, { @@ -525,7 +525,7 @@ where game_dir = %state.game_dir.display(), mods = state.mods.len() ))] -pub(crate) async fn deploy_mods(state: State) -> Result<()> { +pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { let state = Arc::new(state); { @@ -643,7 +643,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { } #[tracing::instrument(skip(state))] -pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { +pub(crate) async fn reset_mod_deployment(state: ActionState) -> Result<()> { let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH]; let bundle_dir = state.game_dir.join("bundle"); diff --git a/crates/dtmm/src/controller/worker.rs b/crates/dtmm/src/controller/worker.rs index 49785b4..294af0e 100644 --- a/crates/dtmm/src/controller/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -1,5 +1,8 @@ use std::sync::Arc; +use color_eyre::eyre::Context; +use color_eyre::Help; +use color_eyre::Report; use color_eyre::Result; use druid::{ExtEventSink, SingleUse, Target}; use tokio::runtime::Runtime; @@ -10,11 +13,19 @@ use crate::controller::app::*; use crate::controller::game::*; use crate::state::AsyncAction; use crate::state::ACTION_FINISH_SAVE_SETTINGS; +use crate::state::ACTION_SHOW_ERROR_DIALOG; use crate::state::{ ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY, ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG, }; +async fn send_error(sink: Arc>, err: Report) { + sink.write() + .await + .submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto) + .expect("failed to send command"); +} + async fn handle_action( event_sink: Arc>, action_queue: Arc>>, @@ -23,8 +34,9 @@ async fn handle_action( let event_sink = event_sink.clone(); match action { AsyncAction::DeployMods(state) => tokio::spawn(async move { - if let Err(err) = deploy_mods(state).await { - tracing::error!("Failed to deploy mods: {:?}", err); + if let Err(err) = deploy_mods(state).await.wrap_err("failed to deploy mods") { + tracing::error!("{:?}", err); + send_error(event_sink.clone(), err).await; } event_sink @@ -33,8 +45,11 @@ async fn handle_action( .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .expect("failed to send command"); }), - AsyncAction::AddMod((state, info)) => tokio::spawn(async move { - match import_mod(state, info).await { + AsyncAction::AddMod(state, info) => tokio::spawn(async move { + match import_mod(state, info) + .await + .wrap_err("failed to import mod") + { Ok(mod_info) => { event_sink .write() @@ -47,18 +62,22 @@ async fn handle_action( .expect("failed to send command"); } Err(err) => { - tracing::error!("Failed to import mod: {:?}", err); + tracing::error!("{:?}", err); + send_error(event_sink.clone(), err).await; } } }), - AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { - if let Err(err) = delete_mod(state, &info).await { - tracing::error!( - "Failed to delete mod files. \ - You might want to clean up the data directory manually. \ - Reason: {:?}", - err - ); + AsyncAction::DeleteMod(state, info) => tokio::spawn(async move { + let mod_dir = state.mod_dir.join(&info.id); + if let Err(err) = delete_mod(state, &info) + .await + .wrap_err("failed to delete mod files") + .with_suggestion(|| { + format!("Clean the folder '{}' manually", mod_dir.display()) + }) + { + tracing::error!("{:?}", err); + send_error(event_sink.clone(), err).await; } event_sink @@ -72,8 +91,12 @@ async fn handle_action( .expect("failed to send command"); }), AsyncAction::ResetDeployment(state) => tokio::spawn(async move { - if let Err(err) = reset_mod_deployment(state).await { - tracing::error!("Failed to reset mod deployment: {:?}", err); + if let Err(err) = reset_mod_deployment(state) + .await + .wrap_err("failed to reset mod deployment") + { + tracing::error!("{:?}", err); + send_error(event_sink.clone(), err).await; } event_sink @@ -83,8 +106,12 @@ async fn handle_action( .expect("failed to send command"); }), AsyncAction::SaveSettings(state) => tokio::spawn(async move { - if let Err(err) = save_settings(state).await { - tracing::error!("Failed to save settings: {:?}", err); + if let Err(err) = save_settings(state) + .await + .wrap_err("failed to save settings") + { + tracing::error!("{:?}", err); + send_error(event_sink.clone(), err).await; } event_sink diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index 65156a3..8021840 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -1,6 +1,9 @@ use std::{path::PathBuf, sync::Arc}; -use druid::{im::Vector, Data, Lens}; +use druid::{ + im::{HashMap, Vector}, + Data, Lens, WindowHandle, WindowId, +}; use dtmt_shared::ModConfig; use super::SelectedModLens; @@ -86,6 +89,9 @@ pub(crate) struct State { pub config_path: Arc, #[lens(ignore)] #[data(ignore)] + pub windows: HashMap, + #[lens(ignore)] + #[data(ignore)] pub ctx: Arc, } @@ -110,6 +116,7 @@ impl State { game_dir: Arc::new(game_dir), data_dir: Arc::new(data_dir), log: Arc::new(String::new()), + windows: HashMap::new(), } } diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 6b05b96..506cbf8 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -1,10 +1,14 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; +use color_eyre::Report; use druid::{ - AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, + im::Vector, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, + Target, WindowHandle, WindowId, }; use tokio::sync::mpsc::UnboundedSender; +use crate::ui::window; + use super::{ModInfo, State}; pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); @@ -37,12 +41,42 @@ pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector = pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty"); +pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector> = + Selector::new("dtmm.action.show-error-dialog"); + +pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector> = + Selector::new("dtmm.action.set-window-handle"); + +// A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are +// `Send + Sync` +pub(crate) struct ActionState { + pub mods: Vector>, + pub game_dir: Arc, + pub data_dir: Arc, + pub mod_dir: Arc, + pub config_path: Arc, + pub ctx: Arc, +} + +impl From for ActionState { + fn from(state: State) -> Self { + Self { + mods: state.mods, + game_dir: state.game_dir, + mod_dir: Arc::new(state.data_dir.join("mods")), + data_dir: state.data_dir, + config_path: state.config_path, + ctx: state.ctx, + } + } +} + pub(crate) enum AsyncAction { - DeployMods(State), - ResetDeployment(State), - AddMod((State, FileInfo)), - DeleteMod((State, Arc)), - SaveSettings(State), + DeployMods(ActionState), + ResetDeployment(ActionState), + AddMod(ActionState, FileInfo), + DeleteMod(ActionState, Arc), + SaveSettings(ActionState), } pub(crate) struct Delegate { @@ -73,7 +107,7 @@ impl AppDelegate for Delegate { cmd if cmd.is(ACTION_START_DEPLOY) => { if self .sender - .send(AsyncAction::DeployMods(state.clone())) + .send(AsyncAction::DeployMods(state.clone().into())) .is_ok() { state.is_deployment_in_progress = true; @@ -91,7 +125,7 @@ impl AppDelegate for Delegate { cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { if self .sender - .send(AsyncAction::ResetDeployment(state.clone())) + .send(AsyncAction::ResetDeployment(state.clone().into())) .is_ok() { state.is_reset_in_progress = true; @@ -147,11 +181,12 @@ impl AppDelegate for Delegate { cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { let info = cmd .get(ACTION_START_DELETE_SELECTED_MOD) - .and_then(|info| info.take()) + .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); + if self .sender - .send(AsyncAction::DeleteMod((state.clone(), info))) + .send(AsyncAction::DeleteMod(state.clone().into(), info)) .is_err() { tracing::error!("Failed to queue action to deploy mods"); @@ -162,8 +197,9 @@ impl AppDelegate for Delegate { cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { let info = cmd .get(ACTION_FINISH_DELETE_SELECTED_MOD) - .and_then(|info| info.take()) + .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); + let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id); let Some((index, _)) = found else { return Handled::No; @@ -177,9 +213,10 @@ impl AppDelegate for Delegate { let info = cmd .get(ACTION_ADD_MOD) .expect("command type matched but didn't contain the expected value"); + if self .sender - .send(AsyncAction::AddMod((state.clone(), info.clone()))) + .send(AsyncAction::AddMod(state.clone().into(), info.clone())) .is_err() { tracing::error!("Failed to queue action to add mod"); @@ -190,9 +227,11 @@ impl AppDelegate for Delegate { let info = cmd .get(ACTION_FINISH_ADD_MOD) .expect("command type matched but didn't contain the expected value"); + if let Some(info) = info.take() { state.add_mod(info); } + Handled::Yes } cmd if cmd.is(ACTION_LOG) => { @@ -209,7 +248,7 @@ impl AppDelegate for Delegate { state.is_next_save_pending = true; } else if self .sender - .send(AsyncAction::SaveSettings(state.clone())) + .send(AsyncAction::SaveSettings(state.clone().into())) .is_ok() { state.is_save_in_progress = true; @@ -233,6 +272,31 @@ impl AppDelegate for Delegate { state.dirty = true; Handled::Yes } + cmd if cmd.is(ACTION_SHOW_ERROR_DIALOG) => { + let err = cmd + .get(ACTION_SHOW_ERROR_DIALOG) + .and_then(SingleUse::take) + .expect("command type matched but didn't contain the expected value"); + + let window = state + .windows + .get(&window::main::WINDOW_ID) + .expect("root window does not exist"); + + let dialog = window::dialog::error::(err, window.clone()); + ctx.new_window(dialog); + + Handled::Yes + } + cmd if cmd.is(ACTION_SET_WINDOW_HANDLE) => { + let (id, handle) = cmd + .get(ACTION_SET_WINDOW_HANDLE) + .and_then(SingleUse::take) + .expect("command type matched but didn't contain the expected value"); + + state.windows.insert(id, handle); + Handled::Yes + } cmd => { if cfg!(debug_assertions) { tracing::warn!("Unknown command: {:?}", cmd); @@ -241,4 +305,19 @@ impl AppDelegate for Delegate { } } } + + fn window_added( + &mut self, + id: WindowId, + handle: WindowHandle, + data: &mut State, + _: &Env, + _: &mut DelegateCtx, + ) { + data.windows.insert(id, handle); + } + + fn window_removed(&mut self, id: WindowId, data: &mut State, _: &Env, _: &mut DelegateCtx) { + data.windows.remove(&id); + } } diff --git a/crates/dtmm/src/ui/mod.rs b/crates/dtmm/src/ui/mod.rs index cf8554f..12f66c9 100644 --- a/crates/dtmm/src/ui/mod.rs +++ b/crates/dtmm/src/ui/mod.rs @@ -1,5 +1,6 @@ pub mod theme; pub mod widget; pub mod window { + pub mod dialog; pub mod main; } diff --git a/crates/dtmm/src/ui/window/dialog.rs b/crates/dtmm/src/ui/window/dialog.rs new file mode 100644 index 0000000..3b6802a --- /dev/null +++ b/crates/dtmm/src/ui/window/dialog.rs @@ -0,0 +1,36 @@ +use color_eyre::Report; +use druid::widget::{Button, CrossAxisAlignment, Flex, Label, LineBreaking, MainAxisAlignment}; +use druid::{Data, WidgetExt, WindowDesc, WindowHandle, WindowLevel, WindowSizePolicy}; + +const ERROR_DIALOG_SIZE: (f64, f64) = (750., 400.); + +pub fn error(err: Report, parent: WindowHandle) -> WindowDesc { + let msg = format!("A critical error ocurred: {:?}", err); + let stripped = + strip_ansi_escapes::strip(msg.as_bytes()).expect("failed to strip ANSI in error"); + let msg = String::from_utf8_lossy(&stripped); + + let text = Label::new(msg.to_string()).with_line_break_mode(LineBreaking::WordWrap); + + let button = Button::new("Ok") + .on_click(|ctx, _, _| { + ctx.window().close(); + }) + .align_right(); + + let widget = Flex::column() + .main_axis_alignment(MainAxisAlignment::SpaceBetween) + .cross_axis_alignment(CrossAxisAlignment::End) + .with_child(text) + .with_spacer(20.) + .with_child(button) + .padding(10.); + + WindowDesc::new(widget) + .title("Error") + .with_min_size(ERROR_DIALOG_SIZE) + .resizable(false) + .window_size_policy(WindowSizePolicy::Content) + .set_always_on_top(true) + .set_level(WindowLevel::Modal(parent)) +} diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index d500610..f48b5c3 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -1,25 +1,30 @@ use std::sync::Arc; use druid::im::Vector; -use druid::lens; use druid::widget::{ Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; +use druid::{lens, LifeCycleCtx}; use druid::{ Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, - TextAlignment, Widget, WidgetExt, WindowDesc, + TextAlignment, Widget, WidgetExt, WindowDesc, WindowId, }; +use lazy_static::lazy_static; use crate::state::{ ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, - ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, - ACTION_START_RESET_DEPLOYMENT, + ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD, + ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT, }; use crate::ui::theme; use crate::ui::widget::controller::{AutoScrollController, DirtyStateController}; use crate::ui::widget::PathBufFormatter; +lazy_static! { + pub static ref WINDOW_ID: WindowId = WindowId::next(); +} + const TITLE: &str = "Darktide Mod Manager"; const WINDOW_SIZE: (f64, f64) = (1080., 720.); const MOD_DETAILS_MIN_WIDTH: f64 = 325.; @@ -324,4 +329,9 @@ fn build_window() -> impl Widget { .with_flex_child(build_main(), 1.0) .with_child(build_log_view()) .controller(DirtyStateController) + .on_added(|_, ctx: &mut LifeCycleCtx, _, _| { + ctx.submit_command( + ACTION_SET_WINDOW_HANDLE.with(SingleUse::new((*WINDOW_ID, ctx.window().clone()))), + ); + }) } diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 2c942ea..41fef73 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -7,7 +7,7 @@ use clap::{parser::ValueSource, ArgMatches}; use color_eyre::{eyre::Context, Result}; use serde::{Deserialize, Serialize}; -use crate::state::{ModInfo, State}; +use crate::state::{ActionState, ModInfo}; #[derive(Clone, Debug, Serialize)] pub(crate) struct LoadOrderEntrySerialize<'a> { @@ -31,8 +31,8 @@ pub(crate) struct ConfigSerialize<'a> { mod_order: Vec>, } -impl<'a> From<&'a State> for ConfigSerialize<'a> { - fn from(state: &'a State) -> Self { +impl<'a> From<&'a ActionState> for ConfigSerialize<'a> { + fn from(state: &'a ActionState) -> Self { Self { game_dir: &state.game_dir, data_dir: &state.data_dir,