refactor(dtmm): Split files into smaller modules

This commit is contained in:
Lucas Schwiderski 2023-02-28 10:03:56 +01:00
parent 7a063d070d
commit e5a72731dd
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
19 changed files with 539 additions and 603 deletions

View file

@ -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()))?;

View file

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

View file

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

View file

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

View 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);
}
}

View 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
}
}
}
}

View 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
}
}

View 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::*;

View 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))
}
}

View file

@ -0,0 +1,5 @@
pub mod theme;
pub mod widget;
pub mod window {
pub mod main;
}

View file

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

View file

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