From 560e5496bc3d17ce5249a0a83095eca49fc3b27d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 11:27:25 +0100 Subject: [PATCH] feat(dtmm): Implement importing mod archives --- Cargo.lock | 1 + crates/dtmm/Cargo.toml | 1 + crates/dtmm/src/engine.rs | 81 ++++++++++++++++++++++++++++++++- crates/dtmm/src/main.rs | 21 +++++++++ crates/dtmm/src/main_window.rs | 14 ++++-- crates/dtmm/src/state.rs | 59 +++++++++++++++++------- crates/dtmt/src/cmd/build.rs | 3 ++ crates/dtmt/src/mods/archive.rs | 19 +++++++- 8 files changed, 177 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02408f0..908f449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,6 +671,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "zip", ] [[package]] diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index c554118..b6ae5e3 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -20,3 +20,4 @@ tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +zip = "0.6.4" diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 818bd8f..bc676c2 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -1,11 +1,13 @@ +use std::collections::HashMap; use std::ffi::CString; -use std::io::{Cursor, ErrorKind}; +use std::io::{Cursor, ErrorKind, Read}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use color_eyre::eyre::Context; use color_eyre::{eyre, Result}; +use druid::FileInfo; use futures::stream; use futures::StreamExt; use sdk::filetype::lua; @@ -14,11 +16,13 @@ use sdk::murmur::Murmur64; use sdk::{ Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; +use serde::Deserialize; use tokio::io::AsyncWriteExt; use tokio::{fs, try_join}; use tracing::Instrument; +use zip::ZipArchive; -use crate::state::{PackageInfo, State}; +use crate::state::{ModInfo, PackageInfo, State}; const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; @@ -371,3 +375,76 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { tracing::info!("Finished deploying mods"); Ok(()) } + +#[derive(Debug, Default, Deserialize)] +struct ModConfig { + name: String, + #[serde(default)] + description: String, +} + +#[tracing::instrument(skip(state))] +pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { + 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")?; + + for f in archive.file_names() { + tracing::debug!("{}", f); + } + + let dir_name = { + let f = archive.by_index(0).wrap_err("archive is empty")?; + + if !f.is_dir() { + eyre::bail!("archive does not have a top-level directory"); + } + + 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() + }; + + 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")? + }; + + let files: HashMap> = { + 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")? + }; + + let mod_dir = state.get_game_dir().join(&mod_cfg.name); + + 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.name, mod_cfg.description, packages); + + Ok(info) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index ba3548a..0ee5ffa 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -9,7 +9,10 @@ use color_eyre::Report; use color_eyre::Result; use druid::AppLauncher; use druid::ExtEventSink; +use druid::SingleUse; use druid::Target; +use engine::import_mod; +use state::ACTION_FINISH_ADD_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; @@ -48,6 +51,24 @@ fn work_thread( .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .expect("failed to send command"); }), + AsyncAction::AddMod((state, info)) => tokio::spawn(async move { + match import_mod(state, info).await { + Ok(mod_info) => { + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_ADD_MOD, + SingleUse::new(mod_info), + Target::Auto, + ) + .expect("failed to send command"); + } + Err(err) => { + tracing::error!("Failed to import mod: {:?}", err); + } + } + }), }; } }); diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 040d796..ecf6c2e 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -3,7 +3,7 @@ use druid::widget::{ Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, TextBox, ViewSwitcher, }; -use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; +use druid::{lens, FileDialogOptions, FileSpec, Insets, LensExt, Widget, WidgetExt, WindowDesc}; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_ADD_MOD}; use crate::state::{ @@ -151,8 +151,16 @@ fn build_mod_details() -> impl Widget { .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); - let button_add_mod = Button::new("Add Mod") - .on_click(|ctx, _state: &mut State, _env| ctx.submit_command(ACTION_ADD_MOD)); + let button_add_mod = Button::new("Add Mod").on_click(|ctx, _state: &mut State, _env| { + let zip = FileSpec::new("Zip file", &["zip"]); + let opts = FileDialogOptions::new() + .allowed_types(vec![zip]) + .default_type(zip) + .name_label("Mod Archive") + .title("Choose a mod to add") + .accept_command(ACTION_ADD_MOD); + ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(opts)) + }); let button_delete_mod = Button::new("Delete Mod") .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_DELETE_SELECTED_MOD)) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index e3eb35b..a59ffd3 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -3,18 +3,25 @@ use std::sync::Arc; use druid::im::Vector; use druid::text::Formatter; -use druid::{AppDelegate, Command, Data, DelegateCtx, Env, Handled, Lens, Selector, Target}; +use druid::{ + AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse, + Target, +}; use tokio::sync::mpsc::UnboundedSender; -pub const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); -pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); -pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); -pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); +pub(crate) const ACTION_SELECT_MOD: Selector = 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_DELETE_SELECTED_MOD: Selector = + Selector::new("dtmm.action.delete-selected-mod"); -pub const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); -pub const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); +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 const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); +pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); +pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = + Selector::new("dtmm.action.finish-add-mod"); #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { @@ -36,6 +43,10 @@ pub struct PackageInfo { } impl PackageInfo { + pub fn new(name: String, files: Vector) -> Self { + Self { name, files } + } + pub fn get_name(&self) -> &String { &self.name } @@ -55,12 +66,12 @@ pub(crate) struct ModInfo { } impl ModInfo { - pub fn new() -> Self { + pub fn new(name: String, description: String, packages: Vector) -> Self { Self { - name: format!("Test Mod: {:?}", std::time::SystemTime::now()), - description: Arc::new(String::from("A test dummy")), + name, + description: Arc::new(description), + packages, enabled: false, - packages: Vector::new(), } } @@ -236,6 +247,7 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { pub(crate) enum AsyncAction { DeployMods(State), + AddMod((State, FileInfo)), } pub(crate) struct Delegate { @@ -321,13 +333,28 @@ impl AppDelegate for Delegate { Handled::Yes } cmd if cmd.is(ACTION_ADD_MOD) => { - // TODO: Implement properly - let info = ModInfo::new(); - state.add_mod(info); + 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 => { - tracing::debug!("Unknown command: {:?}", cmd); + tracing::warn!("Unknown command: {:?}", cmd); Handled::No } } diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index 618d9d2..c2a2045 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -218,6 +218,8 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> fs::read(path).await? }; + let config_file = fs::read(cfg.dir.join("dtmt.cfg")).await?; + { let dest = dest.clone(); let name = cfg.name.clone(); @@ -225,6 +227,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> let mut archive = Archive::new(name); archive.add_mod_file(mod_file); + archive.add_config(config_file); for bundle in bundles { archive.add_bundle(bundle); diff --git a/crates/dtmt/src/mods/archive.rs b/crates/dtmt/src/mods/archive.rs index 16d74d8..683314e 100644 --- a/crates/dtmt/src/mods/archive.rs +++ b/crates/dtmt/src/mods/archive.rs @@ -13,6 +13,7 @@ pub struct Archive { name: String, bundles: Vec, mod_file: Option>, + config_file: Option>, } impl Archive { @@ -21,6 +22,7 @@ impl Archive { name, bundles: Vec::new(), mod_file: None, + config_file: None, } } @@ -32,6 +34,10 @@ impl Archive { self.mod_file = Some(content); } + pub fn add_config(&mut self, content: Vec) { + self.config_file = Some(content); + } + pub fn write

(&self, path: P) -> Result<()> where P: AsRef, @@ -39,7 +45,12 @@ impl Archive { let mod_file = self .mod_file .as_ref() - .ok_or_else(|| eyre::eyre!("Mod file is missing from mod archive"))?; + .ok_or_else(|| eyre::eyre!("Mod file is missing in mod archive"))?; + + let config_file = self + .config_file + .as_ref() + .ok_or_else(|| eyre::eyre!("Config file is missing in mod archive"))?; let f = File::create(path.as_ref()).wrap_err_with(|| { format!( @@ -60,6 +71,12 @@ impl Archive { zip.write_all(mod_file)?; } + { + let name = base_path.join("dtmt.cfg"); + zip.start_file(name.to_string_lossy(), Default::default())?; + zip.write_all(config_file)?; + } + let mut file_map = HashMap::new(); for bundle in self.bundles.iter() {