dtmt/crates/dtmm/src/state/delegate.rs
Lucas Schwiderski 707a3ead8b
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
feat(dtmm): Guard certain Lua libraries behind a setting
Libraries like `io`, `os` and `ffi` allow practically unrestricted
access to the system's files and running arbitrary operations.
The base game removes them for this reason, and while we don't want to
disable them permanently, very few mods should ever have a need for
them.

So we hide them behind a setting, worded so that people only enable it
when absolutely needed.

Closes #112.
2023-04-24 16:45:49 +02:00

439 lines
16 KiB
Rust

use std::path::PathBuf;
use std::sync::Arc;
use color_eyre::Report;
use druid::im::Vector;
use druid::{
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target,
WindowHandle, WindowId,
};
use tokio::sync::mpsc::UnboundedSender;
use crate::ui::window;
use crate::util::ansi::ansi_to_rich_text;
use crate::util::config::Config;
use super::{ModInfo, State};
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
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<Arc<ModInfo>>> =
Selector::new("dtmm.action.srart-delete-selected-mod");
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");
pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy");
pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector =
Selector::new("dtmm.action.start-reset-deployment");
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<Arc<ModInfo>>> =
Selector::new("dtmm.action.finish-add-mod");
pub(crate) const ACTION_LOG: Selector<SingleUse<Vec<u8>>> = Selector::new("dtmm.action.log");
pub(crate) const ACTION_START_SAVE_SETTINGS: Selector =
Selector::new("dtmm.action.start-save-settings");
pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
Selector::new("dtmm.action.finish-save-settings");
pub(crate) const ACTION_START_CHECK_UPDATE: Selector =
Selector::new("dtmm.action.start-check-update");
pub(crate) const ACTION_FINISH_CHECK_UPDATE: Selector<SingleUse<Vec<ModInfo>>> =
Selector::new("dtmm.action.finish-check-update");
pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty");
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
Selector::new("dtmm.action.show-error-dialog");
pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector<SingleUse<(WindowId, WindowHandle)>> =
Selector::new("dtmm.action.set-window-handle");
pub(crate) type InitialLoadResult = (Config, Vector<Arc<ModInfo>>);
pub(crate) const ACTION_FINISH_LOAD_INITIAL: Selector<SingleUse<Option<InitialLoadResult>>> =
Selector::new("dtmm.action.finish-load-initial");
// 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<Arc<ModInfo>>,
pub game_dir: Arc<PathBuf>,
pub data_dir: Arc<PathBuf>,
pub mod_dir: Arc<PathBuf>,
pub config_path: Arc<PathBuf>,
pub ctx: Arc<sdk::Context>,
pub nexus_api_key: Arc<String>,
pub is_io_enabled: bool,
}
impl From<State> 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,
nexus_api_key: state.nexus_api_key,
is_io_enabled: state.is_io_enabled,
}
}
}
pub(crate) enum AsyncAction {
DeployMods(ActionState),
ResetDeployment(ActionState),
AddMod(ActionState, FileInfo),
DeleteMod(ActionState, Arc<ModInfo>),
SaveSettings(ActionState),
CheckUpdates(ActionState),
LoadInitial((PathBuf, bool)),
Log((ActionState, Vec<u8>)),
}
impl std::fmt::Debug for AsyncAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AsyncAction::DeployMods(_) => write!(f, "AsyncAction::DeployMods(_state)"),
AsyncAction::ResetDeployment(_) => write!(f, "AsyncAction::ResetDeployment(_state)"),
AsyncAction::AddMod(_, info) => write!(f, "AsyncAction::AddMod(_state, {:?})", info),
AsyncAction::DeleteMod(_, info) => {
write!(f, "AsyncAction::DeleteMod(_state, {:?})", info)
}
AsyncAction::SaveSettings(_) => write!(f, "AsyncAction::SaveSettings(_state)"),
AsyncAction::CheckUpdates(_) => write!(f, "AsyncAction::CheckUpdates(_state)"),
AsyncAction::LoadInitial((path, is_default)) => write!(
f,
"AsyncAction::LoadInitial(({:?}, {:?}))",
path, is_default
),
AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"),
}
}
}
pub(crate) struct Delegate {
sender: UnboundedSender<AsyncAction>,
}
impl Delegate {
pub fn new(sender: UnboundedSender<AsyncAction>) -> Self {
Self { sender }
}
}
impl AppDelegate<State> for Delegate {
#[tracing::instrument(name = "Delegate", skip_all)]
fn command(
&mut self,
ctx: &mut DelegateCtx,
_target: Target,
cmd: &Command,
state: &mut State,
_env: &Env,
) -> Handled {
if cfg!(debug_assertions) && !cmd.is(ACTION_LOG) {
tracing::trace!(?cmd);
}
match cmd {
cmd if cmd.is(ACTION_START_DEPLOY) => {
if self
.sender
.send(AsyncAction::DeployMods(state.clone().into()))
.is_ok()
{
state.is_deployment_in_progress = true;
} else {
tracing::error!("Failed to queue action to deploy mods");
}
Handled::Yes
}
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) => {
if self
.sender
.send(AsyncAction::ResetDeployment(state.clone().into()))
.is_ok()
{
state.is_reset_in_progress = true;
} else {
tracing::error!("Failed to queue action to reset mod deployment");
}
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => {
state.is_reset_in_progress = false;
Handled::Yes
}
cmd if cmd.is(ACTION_SELECT_MOD) => {
let index = cmd
.get(ACTION_SELECT_MOD)
.expect("command type matched but didn't contain the expected value");
state.select_mod(*index);
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
Handled::Yes
}
cmd if cmd.is(ACTION_SELECTED_MOD_UP) => {
let Some(i) = state.selected_mod_index else {
return Handled::No;
};
let len = state.mods.len();
if len == 0 || i == 0 {
return Handled::No;
}
state.mods.swap(i, i - 1);
state.selected_mod_index = Some(i - 1);
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
Handled::Yes
}
cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => {
let Some(i) = state.selected_mod_index else {
return Handled::No;
};
let len = state.mods.len();
if len == 0 || i == usize::MAX || i >= len - 1 {
return Handled::No;
}
state.mods.swap(i, i + 1);
state.selected_mod_index = Some(i + 1);
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
Handled::Yes
}
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
let info = cmd
.get(ACTION_START_DELETE_SELECTED_MOD)
.and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value");
if self
.sender
.send(AsyncAction::DeleteMod(state.clone().into(), info))
.is_err()
{
tracing::error!("Failed to queue action to deploy mods");
}
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => {
let info = cmd
.get(ACTION_FINISH_DELETE_SELECTED_MOD)
.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;
};
state.mods.remove(index);
Handled::Yes
}
cmd if cmd.is(ACTION_ADD_MOD) => {
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().into(), info.clone()))
.is_err()
{
tracing::error!("Failed to queue action to add mod");
}
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_ADD_MOD) => {
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) => {
let line = cmd
.get(ACTION_LOG)
.expect("command type matched but didn't contain the expected value");
if let Some(line) = line.take() {
{
let line = String::from_utf8_lossy(&line);
state.log.push_back(ansi_to_rich_text(line.trim()));
}
if self
.sender
.send(AsyncAction::Log((state.clone().into(), line)))
.is_err()
{
tracing::error!("Failed to queue action to add mod");
}
}
Handled::Yes
}
cmd if cmd.is(ACTION_START_SAVE_SETTINGS) => {
if state.is_save_in_progress {
state.is_next_save_pending = true;
} else if self
.sender
.send(AsyncAction::SaveSettings(state.clone().into()))
.is_ok()
{
state.is_save_in_progress = true;
} else {
tracing::error!("Failed to queue action to save settings");
}
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => {
tracing::trace!(
in_progress = state.is_save_in_progress,
next_pending = state.is_next_save_pending,
"Finished saving settings",
);
state.is_save_in_progress = false;
if state.is_next_save_pending {
state.is_next_save_pending = false;
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
}
Handled::Yes
}
cmd if cmd.is(ACTION_SET_DIRTY) => {
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::<State>(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 cmd.is(ACTION_START_CHECK_UPDATE) => {
if self
.sender
.send(AsyncAction::CheckUpdates(state.clone().into()))
.is_ok()
{
state.is_update_in_progress = true;
} else {
tracing::error!("Failed to queue action to check updates");
}
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_CHECK_UPDATE) => {
let mut updates = cmd
.get(ACTION_FINISH_CHECK_UPDATE)
.and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value");
if tracing::enabled!(tracing::Level::DEBUG) {
let mods: Vec<_> = updates
.iter()
.map(|info| {
format!(
"{}: {} -> {:?}",
info.name,
info.version,
info.nexus.as_ref().map(|n| &n.version)
)
})
.collect();
tracing::info!("Mod updates:\n{}", mods.join("\n"));
}
for mod_info in state.mods.iter_mut() {
if let Some(index) = updates.iter().position(|i2| i2.id == mod_info.id) {
let update = updates.swap_remove(index);
*mod_info = Arc::new(update);
}
}
state.is_update_in_progress = false;
Handled::Yes
}
cmd if cmd.is(ACTION_FINISH_LOAD_INITIAL) => {
let data = cmd
.get(ACTION_FINISH_LOAD_INITIAL)
.and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value");
if let Some((config, mods)) = data {
state.mods = mods;
state.config_path = Arc::new(config.path);
state.data_dir = Arc::new(config.data_dir);
state.game_dir = Arc::new(config.game_dir.unwrap_or_default());
state.is_io_enabled = config.unsafe_io;
}
state.loading = false;
Handled::Yes
}
_ => Handled::No,
}
}
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);
}
}