Darktide Mod Manager #39
4 changed files with 116 additions and 103 deletions
105
crates/dtmm/src/controller/app.rs
Normal file
105
crates/dtmm/src/controller/app.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
|
use color_eyre::eyre::{self, Context};
|
||||||
|
use color_eyre::{Help, Result};
|
||||||
|
use druid::FileInfo;
|
||||||
|
use dtmt_shared::ModConfig;
|
||||||
|
use tokio::fs;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
use crate::state::{ModInfo, PackageInfo, State};
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
||||||
|
let data = fs::read(&info.path)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to read file {}", info.path.display()))?;
|
||||||
|
let data = Cursor::new(data);
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?;
|
||||||
|
|
||||||
|
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||||
|
let names = archive.file_names().fold(String::new(), |mut s, name| {
|
||||||
|
s.push('\n');
|
||||||
|
s.push_str(name);
|
||||||
|
s
|
||||||
|
});
|
||||||
|
tracing::debug!("Archive contents:{}", names);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_name = {
|
||||||
|
let f = archive.by_index(0).wrap_err("archive is empty")?;
|
||||||
|
|
||||||
|
if !f.is_dir() {
|
||||||
|
let err = eyre::eyre!("archive does not have a top-level directory");
|
||||||
|
return Err(err).with_suggestion(|| "Use 'dtmt build' to create the mod archive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = f.name();
|
||||||
|
// The directory name is returned with a trailing slash, which we don't want
|
||||||
|
name[..(name.len().saturating_sub(1))].to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Importing mod {}", dir_name);
|
||||||
|
|
||||||
|
let mod_cfg: ModConfig = {
|
||||||
|
let mut f = archive
|
||||||
|
.by_name(&format!("{}/{}", dir_name, "dtmt.cfg"))
|
||||||
|
.wrap_err("failed to read mod config from archive")?;
|
||||||
|
let mut buf = Vec::with_capacity(f.size() as usize);
|
||||||
|
f.read_to_end(&mut buf)
|
||||||
|
.wrap_err("failed to read mod config from archive")?;
|
||||||
|
|
||||||
|
let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?;
|
||||||
|
|
||||||
|
serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")?
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!(?mod_cfg);
|
||||||
|
|
||||||
|
let files: HashMap<String, Vec<String>> = {
|
||||||
|
let mut f = archive
|
||||||
|
.by_name(&format!("{}/{}", dir_name, "files.sjson"))
|
||||||
|
.wrap_err("failed to read file index from archive")?;
|
||||||
|
let mut buf = Vec::with_capacity(f.size() as usize);
|
||||||
|
f.read_to_end(&mut buf)
|
||||||
|
.wrap_err("failed to read file index from archive")?;
|
||||||
|
|
||||||
|
let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?;
|
||||||
|
|
||||||
|
serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")?
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::trace!(?files);
|
||||||
|
|
||||||
|
let mod_dir = state.get_mod_dir();
|
||||||
|
|
||||||
|
tracing::trace!("Creating mods directory {}", mod_dir.display());
|
||||||
|
fs::create_dir_all(&mod_dir)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?;
|
||||||
|
|
||||||
|
tracing::trace!("Extracting mod archive to {}", mod_dir.display());
|
||||||
|
archive
|
||||||
|
.extract(&mod_dir)
|
||||||
|
.wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?;
|
||||||
|
|
||||||
|
let packages = files
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, files)| PackageInfo::new(name, files.into_iter().collect()))
|
||||||
|
.collect();
|
||||||
|
let info = ModInfo::new(mod_cfg, packages);
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
||||||
|
let mod_dir = state.get_mod_dir().join(&info.id);
|
||||||
|
fs::remove_dir_all(&mod_dir)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::io::{Cursor, ErrorKind, Read};
|
use std::io::{Cursor, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::eyre::Context;
|
use color_eyre::eyre::Context;
|
||||||
use color_eyre::{eyre, Help, Result};
|
use color_eyre::{eyre, Help, Result};
|
||||||
use druid::FileInfo;
|
|
||||||
use dtmt_shared::ModConfig;
|
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use sdk::filetype::lua;
|
use sdk::filetype::lua;
|
||||||
|
@ -20,9 +17,8 @@ use sdk::{
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
use crate::state::{ModInfo, PackageInfo, State};
|
use crate::state::{PackageInfo, State};
|
||||||
|
|
||||||
const MOD_BUNDLE_NAME: &str = "packages/mods";
|
const MOD_BUNDLE_NAME: &str = "packages/mods";
|
||||||
const BOOT_BUNDLE_NAME: &str = "packages/boot";
|
const BOOT_BUNDLE_NAME: &str = "packages/boot";
|
||||||
|
@ -577,97 +573,3 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
|
||||||
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
|
||||||
let data = fs::read(&info.path)
|
|
||||||
.await
|
|
||||||
.wrap_err_with(|| format!("failed to read file {}", info.path.display()))?;
|
|
||||||
let data = Cursor::new(data);
|
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?;
|
|
||||||
|
|
||||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
|
||||||
let names = archive.file_names().fold(String::new(), |mut s, name| {
|
|
||||||
s.push('\n');
|
|
||||||
s.push_str(name);
|
|
||||||
s
|
|
||||||
});
|
|
||||||
tracing::debug!("Archive contents:{}", names);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir_name = {
|
|
||||||
let f = archive.by_index(0).wrap_err("archive is empty")?;
|
|
||||||
|
|
||||||
if !f.is_dir() {
|
|
||||||
let err = eyre::eyre!("archive does not have a top-level directory");
|
|
||||||
return Err(err).with_suggestion(|| "Use 'dtmt build' to create the mod archive.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = f.name();
|
|
||||||
// The directory name is returned with a trailing slash, which we don't want
|
|
||||||
name[..(name.len().saturating_sub(1))].to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!("Importing mod {}", dir_name);
|
|
||||||
|
|
||||||
let mod_cfg: ModConfig = {
|
|
||||||
let mut f = archive
|
|
||||||
.by_name(&format!("{}/{}", dir_name, "dtmt.cfg"))
|
|
||||||
.wrap_err("failed to read mod config from archive")?;
|
|
||||||
let mut buf = Vec::with_capacity(f.size() as usize);
|
|
||||||
f.read_to_end(&mut buf)
|
|
||||||
.wrap_err("failed to read mod config from archive")?;
|
|
||||||
|
|
||||||
let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?;
|
|
||||||
|
|
||||||
serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")?
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!(?mod_cfg);
|
|
||||||
|
|
||||||
let files: HashMap<String, Vec<String>> = {
|
|
||||||
let mut f = archive
|
|
||||||
.by_name(&format!("{}/{}", dir_name, "files.sjson"))
|
|
||||||
.wrap_err("failed to read file index from archive")?;
|
|
||||||
let mut buf = Vec::with_capacity(f.size() as usize);
|
|
||||||
f.read_to_end(&mut buf)
|
|
||||||
.wrap_err("failed to read file index from archive")?;
|
|
||||||
|
|
||||||
let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?;
|
|
||||||
|
|
||||||
serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")?
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::trace!(?files);
|
|
||||||
|
|
||||||
let mod_dir = state.get_mod_dir();
|
|
||||||
|
|
||||||
tracing::trace!("Creating mods directory {}", mod_dir.display());
|
|
||||||
fs::create_dir_all(&mod_dir)
|
|
||||||
.await
|
|
||||||
.wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?;
|
|
||||||
|
|
||||||
tracing::trace!("Extracting mod archive to {}", mod_dir.display());
|
|
||||||
archive
|
|
||||||
.extract(&mod_dir)
|
|
||||||
.wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?;
|
|
||||||
|
|
||||||
let packages = files
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, files)| PackageInfo::new(name, files.into_iter().collect()))
|
|
||||||
.collect();
|
|
||||||
let info = ModInfo::new(mod_cfg, packages);
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
|
||||||
pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
|
||||||
let mod_dir = state.get_mod_dir().join(&info.id);
|
|
||||||
fs::remove_dir_all(&mod_dir)
|
|
||||||
.await
|
|
||||||
.wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -6,8 +6,13 @@ use tokio::runtime::Runtime;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::controller::engine::*;
|
use crate::controller::app::*;
|
||||||
use crate::state::*;
|
use crate::controller::game::*;
|
||||||
|
use crate::state::AsyncAction;
|
||||||
|
use crate::state::{
|
||||||
|
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
|
||||||
|
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
|
||||||
|
};
|
||||||
|
|
||||||
async fn handle_action(
|
async fn handle_action(
|
||||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||||
|
|
|
@ -16,7 +16,8 @@ use crate::controller::worker::work_thread;
|
||||||
use crate::state::{Delegate, State};
|
use crate::state::{Delegate, State};
|
||||||
|
|
||||||
mod controller {
|
mod controller {
|
||||||
pub mod engine;
|
pub mod app;
|
||||||
|
pub mod game;
|
||||||
pub mod worker;
|
pub mod worker;
|
||||||
}
|
}
|
||||||
mod state;
|
mod state;
|
||||||
|
|
Loading…
Add table
Reference in a new issue