refactor(dtmm): Split files into smaller modules
This commit is contained in:
parent
7a063d070d
commit
e5a72731dd
19 changed files with 539 additions and 603 deletions
|
@ -93,7 +93,7 @@ where
|
|||
#[tracing::instrument(skip_all)]
|
||||
async fn patch_game_settings(state: Arc<State>) -> Result<()> {
|
||||
let settings_path = state
|
||||
.get_game_dir()
|
||||
.game_dir
|
||||
.join("bundle/application_settings/settings_common.ini");
|
||||
|
||||
let settings = read_file_with_backup(&settings_path)
|
||||
|
@ -121,11 +121,11 @@ async fn patch_game_settings(state: Arc<State>) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(package = info.get_name()))]
|
||||
#[tracing::instrument(skip_all, fields(package = info.name))]
|
||||
fn make_package(info: &PackageInfo) -> Result<Package> {
|
||||
let mut pkg = Package::new(info.get_name().clone(), PathBuf::new());
|
||||
let mut pkg = Package::new(info.name.clone(), PathBuf::new());
|
||||
|
||||
for f in info.get_files().iter() {
|
||||
for f in &info.files {
|
||||
let mut it = f.rsplit('.');
|
||||
let file_type = it
|
||||
.next()
|
||||
|
@ -144,32 +144,28 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
|
|||
|
||||
// DMF is handled explicitely by the loading procedures, as it actually drives most of that
|
||||
// and should therefore not show up in the load order.
|
||||
for mod_info in state
|
||||
.get_mods()
|
||||
.iter()
|
||||
.filter(|m| m.get_id() != "dml" && m.get_enabled())
|
||||
{
|
||||
for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) {
|
||||
lua.push_str(" {\n name = \"");
|
||||
lua.push_str(mod_info.get_name());
|
||||
lua.push_str(&mod_info.name);
|
||||
|
||||
lua.push_str("\",\n id = \"");
|
||||
lua.push_str(mod_info.get_id());
|
||||
lua.push_str(&mod_info.id);
|
||||
|
||||
lua.push_str("\",\n run = function()\n");
|
||||
|
||||
let resources = mod_info.get_resources();
|
||||
if resources.get_data().is_some() || resources.get_localization().is_some() {
|
||||
let resources = &mod_info.resources;
|
||||
if resources.data.is_some() || resources.localization.is_some() {
|
||||
lua.push_str(" new_mod(\"");
|
||||
lua.push_str(mod_info.get_id());
|
||||
lua.push_str(&mod_info.id);
|
||||
lua.push_str("\", {\n init = \"");
|
||||
lua.push_str(&resources.get_init().to_string_lossy());
|
||||
lua.push_str(&resources.init.to_string_lossy());
|
||||
|
||||
if let Some(data) = resources.get_data() {
|
||||
if let Some(data) = resources.data.as_ref() {
|
||||
lua.push_str("\",\n data = \"");
|
||||
lua.push_str(&data.to_string_lossy());
|
||||
}
|
||||
|
||||
if let Some(localization) = resources.get_localization() {
|
||||
if let Some(localization) = &resources.localization {
|
||||
lua.push_str("\",\n localization = \"");
|
||||
lua.push_str(&localization.to_string_lossy());
|
||||
}
|
||||
|
@ -177,15 +173,15 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
|
|||
lua.push_str("\",\n })\n");
|
||||
} else {
|
||||
lua.push_str(" return dofile(\"");
|
||||
lua.push_str(&resources.get_init().to_string_lossy());
|
||||
lua.push_str(&resources.init.to_string_lossy());
|
||||
lua.push_str("\")\n");
|
||||
}
|
||||
|
||||
lua.push_str(" end,\n packages = {\n");
|
||||
|
||||
for pkg_info in mod_info.get_packages() {
|
||||
for pkg_info in &mod_info.packages {
|
||||
lua.push_str(" \"");
|
||||
lua.push_str(pkg_info.get_name());
|
||||
lua.push_str(&pkg_info.name);
|
||||
lua.push_str("\",\n");
|
||||
}
|
||||
|
||||
|
@ -201,10 +197,10 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME);
|
||||
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string());
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let bundle_dir = Arc::new(state.get_game_dir().join("bundle"));
|
||||
let bundle_dir = Arc::new(state.game_dir.join("bundle"));
|
||||
|
||||
let mut bundles = Vec::new();
|
||||
|
||||
|
@ -220,17 +216,13 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
mod_bundle.add_file(file);
|
||||
}
|
||||
|
||||
for mod_info in state
|
||||
.get_mods()
|
||||
.iter()
|
||||
.filter(|m| m.get_id() != "dml" && m.get_enabled())
|
||||
{
|
||||
let span = tracing::trace_span!("building mod packages", name = mod_info.get_name());
|
||||
for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) {
|
||||
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.get_id());
|
||||
for pkg_info in mod_info.get_packages() {
|
||||
let span = tracing::trace_span!("building package", name = pkg_info.get_name());
|
||||
let mod_dir = state.get_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();
|
||||
|
||||
let pkg = make_package(pkg_info).wrap_err("failed to make package")?;
|
||||
|
@ -239,24 +231,24 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
.to_binary()
|
||||
.wrap_err("failed to serialize package to binary")?;
|
||||
variant.set_data(bin);
|
||||
let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package);
|
||||
let mut file = BundleFile::new(pkg_info.name.clone(), BundleFileType::Package);
|
||||
file.add_variant(variant);
|
||||
|
||||
mod_bundle.add_file(file);
|
||||
|
||||
let bundle_name = Murmur64::hash(pkg_info.get_name())
|
||||
let bundle_name = Murmur64::hash(&pkg_info.name)
|
||||
.to_string()
|
||||
.to_ascii_lowercase();
|
||||
let src = mod_dir.join(&bundle_name);
|
||||
let dest = bundle_dir.join(&bundle_name);
|
||||
let pkg_name = pkg_info.get_name().clone();
|
||||
let mod_name = mod_info.get_name().clone();
|
||||
let pkg_name = pkg_info.name.clone();
|
||||
let mod_name = mod_info.name.clone();
|
||||
|
||||
// Explicitely drop the guard, so that we can move the span
|
||||
// into the async operation
|
||||
drop(_enter);
|
||||
|
||||
let ctx = state.get_ctx().clone();
|
||||
let ctx = state.ctx.clone();
|
||||
|
||||
let task = async move {
|
||||
let bundle = {
|
||||
|
@ -322,7 +314,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||
let bundle_dir = Arc::new(state.get_game_dir().join("bundle"));
|
||||
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())));
|
||||
|
||||
let mut bundles = Vec::with_capacity(2);
|
||||
|
@ -332,7 +324,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
.await
|
||||
.wrap_err("failed to read boot bundle")?;
|
||||
|
||||
Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin)
|
||||
Bundle::from_binary(&state.ctx, BOOT_BUNDLE_NAME.to_string(), bin)
|
||||
.wrap_err("failed to parse boot bundle")
|
||||
}
|
||||
.instrument(tracing::trace_span!("read boot bundle"))
|
||||
|
@ -346,9 +338,9 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
|
||||
let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new());
|
||||
|
||||
for mod_info in state.get_mods() {
|
||||
for pkg_info in mod_info.get_packages() {
|
||||
pkg.add_file(BundleFileType::Package, pkg_info.get_name());
|
||||
for mod_info in &state.mods {
|
||||
for pkg_info in &mod_info.packages {
|
||||
pkg.add_file(BundleFileType::Package, &pkg_info.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -369,32 +361,28 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
|
||||
let mut variant = BundleFileVariant::new();
|
||||
|
||||
let mods = state.get_mods();
|
||||
let mod_info = mods
|
||||
let mod_info = state
|
||||
.mods
|
||||
.iter()
|
||||
.find(|m| m.get_id() == "dml")
|
||||
.find(|m| m.id == "dml")
|
||||
.ok_or_else(|| eyre::eyre!("DML not found in mod list"))?;
|
||||
let pkg_info = mod_info
|
||||
.get_packages()
|
||||
.packages
|
||||
.get(0)
|
||||
.ok_or_else(|| eyre::eyre!("invalid mod package for DML"))
|
||||
.with_suggestion(|| "Re-download and import the newest version.".to_string())?;
|
||||
let bundle_name = Murmur64::hash(pkg_info.get_name())
|
||||
let bundle_name = Murmur64::hash(&pkg_info.name)
|
||||
.to_string()
|
||||
.to_ascii_lowercase();
|
||||
let src = state
|
||||
.get_mod_dir()
|
||||
.join(mod_info.get_id())
|
||||
.join(&bundle_name);
|
||||
let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name);
|
||||
|
||||
{
|
||||
let ctx = state.get_ctx();
|
||||
let bin = fs::read(&src)
|
||||
.await
|
||||
.wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?;
|
||||
let name = Bundle::get_name_from_path(&ctx, &src);
|
||||
let name = Bundle::get_name_from_path(&state.ctx, &src);
|
||||
|
||||
let dml_bundle = Bundle::from_binary(&ctx, name, bin)
|
||||
let dml_bundle = Bundle::from_binary(&state.ctx, name, bin)
|
||||
.wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?;
|
||||
|
||||
bundles.push(dml_bundle);
|
||||
|
@ -402,8 +390,8 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
|
||||
{
|
||||
let dest = bundle_dir.join(&bundle_name);
|
||||
let pkg_name = pkg_info.get_name().clone();
|
||||
let mod_name = mod_info.get_name().clone();
|
||||
let pkg_name = pkg_info.name.clone();
|
||||
let mod_name = mod_info.name.clone();
|
||||
|
||||
tracing::debug!(
|
||||
"Copying bundle {} for mod {}: {} -> {}",
|
||||
|
@ -441,7 +429,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
let span = tracing::debug_span!("Importing mod main script");
|
||||
let _enter = span.enter();
|
||||
|
||||
let lua = include_str!("../assets/mod_main.lua");
|
||||
let lua = include_str!("../../assets/mod_main.lua");
|
||||
let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?;
|
||||
let file =
|
||||
lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?;
|
||||
|
@ -467,7 +455,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
|||
|
||||
#[tracing::instrument(skip_all, fields(bundles = bundles.len()))]
|
||||
async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Result<()> {
|
||||
let bundle_dir = Arc::new(state.get_game_dir().join("bundle"));
|
||||
let bundle_dir = Arc::new(state.game_dir.join("bundle"));
|
||||
let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME);
|
||||
|
||||
let mut db = {
|
||||
|
@ -501,16 +489,15 @@ async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Resul
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(
|
||||
game_dir = %state.get_game_dir().display(),
|
||||
mods = state.get_mods().len()
|
||||
game_dir = %state.game_dir.display(),
|
||||
mods = state.mods.len()
|
||||
))]
|
||||
pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
||||
let state = Arc::new(state);
|
||||
|
||||
{
|
||||
let mods = state.get_mods();
|
||||
let first = mods.get(0);
|
||||
if first.is_none() || !(first.unwrap().get_id() == "dml" && first.unwrap().get_enabled()) {
|
||||
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");
|
||||
}
|
||||
|
@ -518,8 +505,8 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
|||
|
||||
tracing::info!(
|
||||
"Deploying {} mods to {}",
|
||||
state.get_mods().len(),
|
||||
state.get_game_dir().join("bundle").display()
|
||||
state.mods.len(),
|
||||
state.game_dir.join("bundle").display()
|
||||
);
|
||||
|
||||
tracing::info!("Build mod bundles");
|
||||
|
@ -550,7 +537,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
|||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
|
||||
let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME];
|
||||
let bundle_dir = state.get_game_dir().join("bundle");
|
||||
let bundle_dir = state.game_dir.join("bundle");
|
||||
|
||||
tracing::info!("Resetting mod deployment in {}", bundle_dir.display());
|
||||
|
||||
|
@ -664,7 +651,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
|
|||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
||||
let mod_dir = state.get_mod_dir().join(info.get_id());
|
||||
let mod_dir = state.get_mod_dir().join(&info.id);
|
||||
fs::remove_dir_all(&mod_dir)
|
||||
.await
|
||||
.wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?;
|
|
@ -6,7 +6,7 @@ use tokio::runtime::Runtime;
|
|||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::engine::*;
|
||||
use crate::controller::engine::*;
|
||||
use crate::state::*;
|
||||
|
||||
async fn handle_action(
|
|
@ -12,24 +12,25 @@ use color_eyre::{Report, Result};
|
|||
use druid::AppLauncher;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::controller::worker::work_thread;
|
||||
use crate::state::{Delegate, State};
|
||||
use crate::worker::work_thread;
|
||||
|
||||
mod controller;
|
||||
mod engine;
|
||||
mod log;
|
||||
mod main_window;
|
||||
mod controller {
|
||||
pub mod engine;
|
||||
pub mod worker;
|
||||
}
|
||||
mod state;
|
||||
mod theme;
|
||||
mod util;
|
||||
mod widget;
|
||||
mod worker;
|
||||
mod util {
|
||||
pub mod config;
|
||||
pub mod log;
|
||||
}
|
||||
mod ui;
|
||||
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let default_config_path = util::get_default_config_path();
|
||||
let default_config_path = util::config::get_default_config_path();
|
||||
|
||||
tracing::trace!(default_config_path = %default_config_path.display());
|
||||
|
||||
|
@ -51,21 +52,21 @@ fn main() -> Result<()> {
|
|||
.get_matches();
|
||||
|
||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
log::create_tracing_subscriber(log_tx);
|
||||
util::log::create_tracing_subscriber(log_tx);
|
||||
|
||||
unsafe {
|
||||
oodle_sys::init(matches.get_one::<String>("oodle"));
|
||||
}
|
||||
|
||||
let config =
|
||||
util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?;
|
||||
let config = util::config::read_config(&default_config_path, &matches)
|
||||
.wrap_err("failed to read config file")?;
|
||||
|
||||
let initial_state = State::new(config);
|
||||
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let delegate = Delegate::new(action_tx);
|
||||
|
||||
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
|
||||
let launcher = AppLauncher::with_window(ui::window::main::new()).delegate(delegate);
|
||||
|
||||
let event_sink = launcher.get_external_handle();
|
||||
std::thread::spawn(move || {
|
||||
|
|
|
@ -1,510 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use druid::im::Vector;
|
||||
use druid::text::Formatter;
|
||||
use druid::{
|
||||
AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse,
|
||||
Target,
|
||||
};
|
||||
use dtmt_shared::ModConfig;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::util::Config;
|
||||
|
||||
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<ModInfo>> =
|
||||
Selector::new("dtmm.action.srart-delete-selected-mod");
|
||||
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<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<ModInfo>> =
|
||||
Selector::new("dtmm.action.finish-add-mod");
|
||||
|
||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
||||
|
||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||
pub(crate) enum View {
|
||||
Mods,
|
||||
Settings,
|
||||
About,
|
||||
}
|
||||
|
||||
impl Default for View {
|
||||
fn default() -> Self {
|
||||
Self::Mods
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug)]
|
||||
pub struct PackageInfo {
|
||||
name: String,
|
||||
files: Vector<String>,
|
||||
}
|
||||
|
||||
impl PackageInfo {
|
||||
pub fn new(name: String, files: Vector<String>) -> Self {
|
||||
Self { name, files }
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn get_files(&self) -> &Vector<String> {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ModResourceInfo {
|
||||
init: PathBuf,
|
||||
data: Option<PathBuf>,
|
||||
localization: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ModResourceInfo {
|
||||
pub(crate) fn get_init(&self) -> &PathBuf {
|
||||
&self.init
|
||||
}
|
||||
|
||||
pub(crate) fn get_data(&self) -> Option<&PathBuf> {
|
||||
self.data.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn get_localization(&self) -> Option<&PathBuf> {
|
||||
self.localization.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug, Lens)]
|
||||
pub(crate) struct ModInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
description: Arc<String>,
|
||||
enabled: bool,
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
packages: Vector<PackageInfo>,
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
resources: ModResourceInfo,
|
||||
}
|
||||
|
||||
impl ModInfo {
|
||||
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
|
||||
Self {
|
||||
id: cfg.id,
|
||||
name: cfg.name,
|
||||
description: Arc::new(cfg.description),
|
||||
enabled: false,
|
||||
packages,
|
||||
resources: ModResourceInfo {
|
||||
init: cfg.resources.init,
|
||||
data: cfg.resources.data,
|
||||
localization: cfg.resources.localization,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_packages(&self) -> &Vector<PackageInfo> {
|
||||
&self.packages
|
||||
}
|
||||
|
||||
pub(crate) fn get_name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub(crate) fn get_id(&self) -> &String {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub(crate) fn get_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
pub(crate) fn get_resources(&self) -> &ModResourceInfo {
|
||||
&self.resources
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ModInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name.eq(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Lens)]
|
||||
pub(crate) struct State {
|
||||
current_view: View,
|
||||
mods: Vector<ModInfo>,
|
||||
selected_mod_index: Option<usize>,
|
||||
is_deployment_in_progress: bool,
|
||||
is_reset_in_progress: bool,
|
||||
game_dir: Arc<PathBuf>,
|
||||
data_dir: Arc<PathBuf>,
|
||||
ctx: Arc<sdk::Context>,
|
||||
log: Arc<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||
|
||||
pub fn new(config: Config) -> Self {
|
||||
let ctx = sdk::Context::new();
|
||||
|
||||
Self {
|
||||
ctx: Arc::new(ctx),
|
||||
current_view: View::default(),
|
||||
mods: Vector::new(),
|
||||
selected_mod_index: None,
|
||||
is_deployment_in_progress: false,
|
||||
is_reset_in_progress: false,
|
||||
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
||||
log: Arc::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_view(&self) -> View {
|
||||
self.current_view
|
||||
}
|
||||
|
||||
pub fn set_current_view(&mut self, view: View) {
|
||||
self.current_view = view;
|
||||
}
|
||||
|
||||
pub fn get_mods(&self) -> Vector<ModInfo> {
|
||||
self.mods.clone()
|
||||
}
|
||||
|
||||
pub fn select_mod(&mut self, index: usize) {
|
||||
self.selected_mod_index = Some(index);
|
||||
}
|
||||
|
||||
pub fn add_mod(&mut self, info: ModInfo) {
|
||||
if let Some(pos) = self.mods.index_of(&info) {
|
||||
self.mods.set(pos, info);
|
||||
self.selected_mod_index = Some(pos);
|
||||
} else {
|
||||
self.mods.push_back(info);
|
||||
self.selected_mod_index = Some(self.mods.len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_move_mod_down(&self) -> bool {
|
||||
self.selected_mod_index
|
||||
.map(|i| i < (self.mods.len().saturating_sub(1)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn can_move_mod_up(&self) -> bool {
|
||||
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn can_deploy_mods(&self) -> bool {
|
||||
!self.is_deployment_in_progress
|
||||
}
|
||||
|
||||
pub fn can_reset_deployment(&self) -> bool {
|
||||
!self.is_reset_in_progress
|
||||
}
|
||||
|
||||
pub(crate) fn get_game_dir(&self) -> &PathBuf {
|
||||
&self.game_dir
|
||||
}
|
||||
|
||||
pub(crate) fn get_mod_dir(&self) -> PathBuf {
|
||||
self.data_dir.join("mods")
|
||||
}
|
||||
|
||||
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
|
||||
self.ctx.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||
let log = Arc::make_mut(&mut self.log);
|
||||
log.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SelectedModLens;
|
||||
|
||||
impl Lens<State, Option<ModInfo>> for SelectedModLens {
|
||||
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
|
||||
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
|
||||
let info = data
|
||||
.selected_mod_index
|
||||
.and_then(|i| data.mods.get(i).cloned());
|
||||
|
||||
f(&info)
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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 {
|
||||
// TODO: Figure out a way to check for equality and
|
||||
// only update when needed
|
||||
data.mods.set(i, info);
|
||||
} else {
|
||||
data.selected_mod_index = None;
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
None => f(&mut None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
|
||||
/// where each element in the destination vector includes its index in the
|
||||
/// source vector.
|
||||
pub(crate) struct IndexedVectorLens;
|
||||
|
||||
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
||||
#[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
|
||||
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
|
||||
let indexed = values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| (i, val.clone()))
|
||||
.collect();
|
||||
f(&indexed)
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
|
||||
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
|
||||
&self,
|
||||
values: &mut Vector<T>,
|
||||
f: F,
|
||||
) -> V {
|
||||
let mut indexed = values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| (i, val.clone()))
|
||||
.collect();
|
||||
let ret = f(&mut indexed);
|
||||
|
||||
*values = indexed.into_iter().map(|(_i, val)| val).collect();
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum AsyncAction {
|
||||
DeployMods(State),
|
||||
ResetDeployment(State),
|
||||
AddMod((State, FileInfo)),
|
||||
DeleteMod((State, ModInfo)),
|
||||
}
|
||||
|
||||
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 {
|
||||
match cmd {
|
||||
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::DeployMods(state.clone()))
|
||||
.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;
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::ResetDeployment(state.clone()))
|
||||
.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);
|
||||
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);
|
||||
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);
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
||||
let info = cmd
|
||||
.get(ACTION_START_DELETE_SELECTED_MOD)
|
||||
.and_then(|info| info.take())
|
||||
.expect("command type matched but didn't contain the expected value");
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::DeleteMod((state.clone(), info)))
|
||||
.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_DELETE_SELECTED_MOD) => {
|
||||
let info = cmd
|
||||
.get(ACTION_FINISH_DELETE_SELECTED_MOD)
|
||||
.and_then(|info| info.take())
|
||||
.expect("command type matched but didn't contain the expected value");
|
||||
let mods = state.get_mods();
|
||||
let found = mods
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, i)| i.get_id() == info.get_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 let Err(err) = self
|
||||
.sender
|
||||
.send(AsyncAction::AddMod((state.clone(), info.clone())))
|
||||
{
|
||||
tracing::error!("Failed to add mod: {}", err);
|
||||
}
|
||||
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() {
|
||||
state.add_log_line(line);
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("Unknown command: {:?}", cmd);
|
||||
}
|
||||
Handled::No
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PathBufFormatter;
|
||||
|
||||
impl PathBufFormatter {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
||||
fn format(&self, value: &Arc<PathBuf>) -> String {
|
||||
value.display().to_string()
|
||||
}
|
||||
|
||||
fn validate_partial_input(
|
||||
&self,
|
||||
_input: &str,
|
||||
_sel: &druid::text::Selection,
|
||||
) -> druid::text::Validation {
|
||||
druid::text::Validation::success()
|
||||
}
|
||||
|
||||
fn value(&self, input: &str) -> Result<Arc<PathBuf>, druid::text::ValidationError> {
|
||||
let p = PathBuf::from(input);
|
||||
Ok(Arc::new(p))
|
||||
}
|
||||
}
|
144
crates/dtmm/src/state/data.rs
Normal file
144
crates/dtmm/src/state/data.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use druid::{im::Vector, Data, Lens};
|
||||
use dtmt_shared::ModConfig;
|
||||
|
||||
use crate::util::config::Config;
|
||||
|
||||
use super::SelectedModLens;
|
||||
|
||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||
pub(crate) enum View {
|
||||
Mods,
|
||||
Settings,
|
||||
About,
|
||||
}
|
||||
|
||||
impl Default for View {
|
||||
fn default() -> Self {
|
||||
Self::Mods
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug)]
|
||||
pub struct PackageInfo {
|
||||
pub name: String,
|
||||
pub files: Vector<String>,
|
||||
}
|
||||
|
||||
impl PackageInfo {
|
||||
pub fn new(name: String, files: Vector<String>) -> Self {
|
||||
Self { name, files }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ModResourceInfo {
|
||||
pub init: PathBuf,
|
||||
pub data: Option<PathBuf>,
|
||||
pub localization: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug, Lens)]
|
||||
pub(crate) struct ModInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Arc<String>,
|
||||
pub enabled: bool,
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
pub packages: Vector<PackageInfo>,
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
pub resources: ModResourceInfo,
|
||||
}
|
||||
|
||||
impl ModInfo {
|
||||
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
|
||||
Self {
|
||||
id: cfg.id,
|
||||
name: cfg.name,
|
||||
description: Arc::new(cfg.description),
|
||||
enabled: false,
|
||||
packages,
|
||||
resources: ModResourceInfo {
|
||||
init: cfg.resources.init,
|
||||
data: cfg.resources.data,
|
||||
localization: cfg.resources.localization,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 selected_mod_index: Option<usize>,
|
||||
pub is_deployment_in_progress: bool,
|
||||
pub is_reset_in_progress: bool,
|
||||
pub game_dir: Arc<PathBuf>,
|
||||
pub data_dir: Arc<PathBuf>,
|
||||
pub ctx: Arc<sdk::Context>,
|
||||
pub log: Arc<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||
|
||||
pub fn new(config: Config) -> Self {
|
||||
let ctx = sdk::Context::new();
|
||||
|
||||
Self {
|
||||
ctx: Arc::new(ctx),
|
||||
current_view: View::default(),
|
||||
mods: Vector::new(),
|
||||
selected_mod_index: None,
|
||||
is_deployment_in_progress: false,
|
||||
is_reset_in_progress: false,
|
||||
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
||||
log: Arc::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_mod(&mut self, index: usize) {
|
||||
self.selected_mod_index = Some(index);
|
||||
}
|
||||
|
||||
pub fn add_mod(&mut self, info: ModInfo) {
|
||||
if let Some(pos) = self.mods.index_of(&info) {
|
||||
self.mods.set(pos, info);
|
||||
self.selected_mod_index = Some(pos);
|
||||
} else {
|
||||
self.mods.push_back(info);
|
||||
self.selected_mod_index = Some(self.mods.len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_move_mod_down(&self) -> bool {
|
||||
self.selected_mod_index
|
||||
.map(|i| i < (self.mods.len().saturating_sub(1)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn can_move_mod_up(&self) -> bool {
|
||||
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn get_mod_dir(&self) -> PathBuf {
|
||||
self.data_dir.join("mods")
|
||||
}
|
||||
|
||||
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||
let log = Arc::make_mut(&mut self.log);
|
||||
log.push_str(&line);
|
||||
}
|
||||
}
|
197
crates/dtmm/src/state/delegate.rs
Normal file
197
crates/dtmm/src/state/delegate.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
use druid::{
|
||||
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
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<ModInfo>> =
|
||||
Selector::new("dtmm.action.srart-delete-selected-mod");
|
||||
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<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<ModInfo>> =
|
||||
Selector::new("dtmm.action.finish-add-mod");
|
||||
|
||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
||||
|
||||
pub(crate) enum AsyncAction {
|
||||
DeployMods(State),
|
||||
ResetDeployment(State),
|
||||
AddMod((State, FileInfo)),
|
||||
DeleteMod((State, ModInfo)),
|
||||
}
|
||||
|
||||
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 {
|
||||
match cmd {
|
||||
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::DeployMods(state.clone()))
|
||||
.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;
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::ResetDeployment(state.clone()))
|
||||
.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);
|
||||
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);
|
||||
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);
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
||||
let info = cmd
|
||||
.get(ACTION_START_DELETE_SELECTED_MOD)
|
||||
.and_then(|info| info.take())
|
||||
.expect("command type matched but didn't contain the expected value");
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::DeleteMod((state.clone(), info)))
|
||||
.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_DELETE_SELECTED_MOD) => {
|
||||
let info = cmd
|
||||
.get(ACTION_FINISH_DELETE_SELECTED_MOD)
|
||||
.and_then(|info| info.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 let Err(err) = self
|
||||
.sender
|
||||
.send(AsyncAction::AddMod((state.clone(), info.clone())))
|
||||
{
|
||||
tracing::error!("Failed to add mod: {}", err);
|
||||
}
|
||||
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() {
|
||||
state.add_log_line(line);
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("Unknown command: {:?}", cmd);
|
||||
}
|
||||
Handled::No
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
crates/dtmm/src/state/lens.rs
Normal file
73
crates/dtmm/src/state/lens.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use druid::im::Vector;
|
||||
use druid::{Data, Lens};
|
||||
|
||||
use super::{ModInfo, State};
|
||||
|
||||
pub(crate) struct SelectedModLens;
|
||||
|
||||
impl Lens<State, Option<ModInfo>> for SelectedModLens {
|
||||
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
|
||||
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
|
||||
let info = data
|
||||
.selected_mod_index
|
||||
.and_then(|i| data.mods.get(i).cloned());
|
||||
|
||||
f(&info)
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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 {
|
||||
// TODO: Figure out a way to check for equality and
|
||||
// only update when needed
|
||||
data.mods.set(i, info);
|
||||
} else {
|
||||
data.selected_mod_index = None;
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
None => f(&mut None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
|
||||
/// where each element in the destination vector includes its index in the
|
||||
/// source vector.
|
||||
pub(crate) struct IndexedVectorLens;
|
||||
|
||||
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
||||
#[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
|
||||
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
|
||||
let indexed = values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| (i, val.clone()))
|
||||
.collect();
|
||||
f(&indexed)
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
|
||||
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
|
||||
&self,
|
||||
values: &mut Vector<T>,
|
||||
f: F,
|
||||
) -> V {
|
||||
let mut indexed = values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| (i, val.clone()))
|
||||
.collect();
|
||||
let ret = f(&mut indexed);
|
||||
|
||||
*values = indexed.into_iter().map(|(_i, val)| val).collect();
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
9
crates/dtmm/src/state/mod.rs
Normal file
9
crates/dtmm/src/state/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
mod data;
|
||||
mod delegate;
|
||||
mod lens;
|
||||
mod util;
|
||||
|
||||
pub(crate) use data::*;
|
||||
pub(crate) use delegate::*;
|
||||
pub(crate) use lens::*;
|
||||
pub(crate) use util::*;
|
31
crates/dtmm/src/state/util.rs
Normal file
31
crates/dtmm/src/state/util.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use druid::text::Formatter;
|
||||
|
||||
pub(crate) struct PathBufFormatter;
|
||||
|
||||
impl PathBufFormatter {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
||||
fn format(&self, value: &Arc<PathBuf>) -> String {
|
||||
value.display().to_string()
|
||||
}
|
||||
|
||||
fn validate_partial_input(
|
||||
&self,
|
||||
_input: &str,
|
||||
_sel: &druid::text::Selection,
|
||||
) -> druid::text::Validation {
|
||||
druid::text::Validation::success()
|
||||
}
|
||||
|
||||
fn value(&self, input: &str) -> Result<Arc<PathBuf>, druid::text::ValidationError> {
|
||||
let p = PathBuf::from(input);
|
||||
Ok(Arc::new(p))
|
||||
}
|
||||
}
|
5
crates/dtmm/src/ui/mod.rs
Normal file
5
crates/dtmm/src/ui/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod theme;
|
||||
pub mod widget;
|
||||
pub mod window {
|
||||
pub mod main;
|
||||
}
|
|
@ -3,6 +3,7 @@ use druid::{Data, Widget};
|
|||
use self::fill_container::FillContainer;
|
||||
|
||||
pub mod container;
|
||||
pub mod controller;
|
||||
pub mod fill_container;
|
||||
|
||||
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {
|
|
@ -13,8 +13,8 @@ use crate::state::{
|
|||
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
||||
};
|
||||
use crate::theme;
|
||||
use crate::widget::ExtraWidgetExt;
|
||||
use crate::ui::theme;
|
||||
use crate::ui::widget::ExtraWidgetExt;
|
||||
|
||||
const TITLE: &str = "Darktide Mod Manager";
|
||||
const WINDOW_SIZE: (f64, f64) = (800.0, 600.0);
|
||||
|
@ -33,21 +33,19 @@ fn build_top_bar() -> impl Widget<State> {
|
|||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Button::new("Mods").on_click(|_ctx, state: &mut State, _env| {
|
||||
state.set_current_view(View::Mods)
|
||||
}),
|
||||
Button::new("Mods")
|
||||
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods),
|
||||
)
|
||||
.with_default_spacer()
|
||||
.with_child(
|
||||
Button::new("Settings").on_click(|_ctx, state: &mut State, _env| {
|
||||
state.set_current_view(View::Settings)
|
||||
state.current_view = View::Settings;
|
||||
}),
|
||||
)
|
||||
.with_default_spacer()
|
||||
.with_child(
|
||||
Button::new("About").on_click(|_ctx, state: &mut State, _env| {
|
||||
state.set_current_view(View::About)
|
||||
}),
|
||||
Button::new("About")
|
||||
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::About),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
|
@ -57,7 +55,7 @@ fn build_top_bar() -> impl Widget<State> {
|
|||
.on_click(|ctx, _state: &mut State, _env| {
|
||||
ctx.submit_command(ACTION_START_DEPLOY);
|
||||
})
|
||||
.disabled_if(|data, _| !data.can_deploy_mods()),
|
||||
.disabled_if(|data, _| !data.is_deployment_in_progress),
|
||||
)
|
||||
.with_default_spacer()
|
||||
.with_child(
|
||||
|
@ -65,7 +63,7 @@ fn build_top_bar() -> impl Widget<State> {
|
|||
.on_click(|ctx, _state: &mut State, _env| {
|
||||
ctx.submit_command(ACTION_START_RESET_DEPLOYMENT);
|
||||
})
|
||||
.disabled_if(|data, _| !data.can_reset_deployment()),
|
||||
.disabled_if(|data, _| !data.is_reset_in_progress),
|
||||
),
|
||||
)
|
||||
.padding(theme::TOP_BAR_INSETS)
|
||||
|
@ -253,7 +251,7 @@ fn build_view_about() -> impl Widget<State> {
|
|||
|
||||
fn build_main() -> impl Widget<State> {
|
||||
ViewSwitcher::new(
|
||||
|state: &State, _env| state.get_current_view(),
|
||||
|state: &State, _env| state.current_view,
|
||||
|selector, _state, _env| match selector {
|
||||
View::Mods => Box::new(build_view_mods()),
|
||||
View::Settings => Box::new(build_view_settings()),
|
Loading…
Add table
Reference in a new issue